nautilus_polymarket/common/
models.rs1use std::fmt::Display;
19
20use nautilus_common::cache::Cache;
21use nautilus_model::{
22 identifiers::InstrumentId,
23 instruments::{Instrument, InstrumentAny},
24};
25use rust_decimal::Decimal;
26use serde::{Deserialize, Serialize};
27use ustr::Ustr;
28
29use crate::common::{
30 enums::{PolymarketOrderSide, PolymarketOutcome},
31 parse::{deserialize_decimal_from_str, serialize_decimal_as_str},
32};
33
34#[must_use]
36pub(crate) fn is_owned_by_account(
37 maker_address: &str,
38 owner: &str,
39 user_address: &str,
40 api_key: &str,
41) -> bool {
42 maker_address.eq_ignore_ascii_case(user_address) || owner == api_key
43}
44
45#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
57pub struct PolymarketMakerOrder {
58 pub asset_id: Ustr,
59 pub maker_address: String,
60 #[serde(
61 serialize_with = "serialize_decimal_as_str",
62 deserialize_with = "deserialize_decimal_from_str"
63 )]
64 pub matched_amount: Decimal,
65 pub order_id: String,
66 pub outcome: PolymarketOutcome,
67 pub owner: String,
68 #[serde(
69 serialize_with = "serialize_decimal_as_str",
70 deserialize_with = "deserialize_decimal_from_str"
71 )]
72 pub price: Decimal,
73 #[serde(default, skip_serializing_if = "Option::is_none")]
74 pub side: Option<PolymarketOrderSide>,
75}
76
77impl PolymarketMakerOrder {
78 #[must_use]
88 pub(crate) fn is_owned_by(&self, user_address: &str, api_key: &str) -> bool {
89 is_owned_by_account(&self.maker_address, &self.owner, user_address, api_key)
90 }
91}
92
93#[derive(Debug, Clone)]
95pub struct PolymarketLabel {
96 pub description: String,
97 pub outcome: String,
98}
99
100impl Display for PolymarketLabel {
101 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102 write!(f, "{} [{}]", self.description, self.outcome)
103 }
104}
105
106impl PolymarketLabel {
107 pub fn from_instrument(instrument: &InstrumentAny) -> Self {
109 if let InstrumentAny::BinaryOption(opt) = instrument {
110 Self {
111 description: opt
112 .description
113 .map_or_else(|| instrument.id().to_string(), |d| d.to_string()),
114 outcome: opt
115 .outcome
116 .map_or_else(|| "?".to_string(), |o| o.to_string()),
117 }
118 } else {
119 Self {
120 description: instrument.id().to_string(),
121 outcome: "?".to_string(),
122 }
123 }
124 }
125
126 pub fn from_cache(instrument_id: &InstrumentId, cache: &Cache) -> Option<Self> {
129 cache.instrument(instrument_id).map(Self::from_instrument)
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use rstest::rstest;
136 use rust_decimal_macros::dec;
137
138 use super::*;
139 use crate::{common::enums::PolymarketOutcome, http::models::PolymarketTradeReport};
140
141 fn load<T: serde::de::DeserializeOwned>(filename: &str) -> T {
142 let path = format!("test_data/{filename}");
143 let content = std::fs::read_to_string(path).expect("Failed to read test data");
144 serde_json::from_str(&content).expect("Failed to parse test data")
145 }
146
147 fn sample_maker_order_json() -> &'static str {
148 r#"{
149 "asset_id": "71321045679252212594626385532706912750332728571942532289631379312455583992563",
150 "fee_rate_bps": "10",
151 "maker_address": "0x70997970c51812dc3a010c7d01b50e0d17dc79c8",
152 "matched_amount": "50.0000",
153 "order_id": "0xorder001",
154 "outcome": "Yes",
155 "owner": "00000000-0000-0000-0000-000000000002",
156 "price": "0.6000"
157 }"#
158 }
159
160 #[rstest]
161 fn test_maker_order_deserialization() {
162 let order: PolymarketMakerOrder = serde_json::from_str(sample_maker_order_json()).unwrap();
163
164 assert_eq!(
165 order.asset_id.as_str(),
166 "71321045679252212594626385532706912750332728571942532289631379312455583992563"
167 );
168 assert_eq!(
169 order.maker_address.as_str(),
170 "0x70997970c51812dc3a010c7d01b50e0d17dc79c8"
171 );
172 assert_eq!(order.matched_amount, dec!(50.0000));
173 assert_eq!(order.order_id, "0xorder001");
174 assert_eq!(order.outcome, PolymarketOutcome::yes());
175 assert_eq!(order.price, dec!(0.6000));
176 }
177
178 #[rstest]
179 fn test_maker_order_roundtrip() {
180 let order: PolymarketMakerOrder = serde_json::from_str(sample_maker_order_json()).unwrap();
181 let json = serde_json::to_string(&order).unwrap();
182 let order2: PolymarketMakerOrder = serde_json::from_str(&json).unwrap();
183 assert_eq!(order, order2);
184 }
185
186 #[rstest]
187 fn test_maker_order_ownership_ignores_address_case() {
188 let mut order: PolymarketMakerOrder =
189 serde_json::from_str(sample_maker_order_json()).unwrap();
190 let lowercase_address = order.maker_address.clone();
191 let uppercase_variant_address = lowercase_address
194 .to_ascii_uppercase()
195 .replacen("0X", "0x", 1);
196 assert_ne!(uppercase_variant_address, lowercase_address);
197
198 order.maker_address = uppercase_variant_address.clone();
201 assert!(order.is_owned_by(&lowercase_address, "no-such-key"));
202
203 order.maker_address = lowercase_address;
205 assert!(order.is_owned_by(&uppercase_variant_address, "no-such-key"));
206 }
207
208 #[rstest]
209 fn test_maker_order_ownership_matches_exact_api_key() {
210 let order: PolymarketMakerOrder = serde_json::from_str(sample_maker_order_json()).unwrap();
211 let owner = order.owner.clone();
212
213 assert!(order.is_owned_by("0xother", &owner));
214 }
215
216 #[rstest]
217 fn test_maker_order_ownership_requires_exact_api_key() {
218 let mut order: PolymarketMakerOrder =
219 serde_json::from_str(sample_maker_order_json()).unwrap();
220 order.owner = "abcdefab-0000-0000-0000-000000000002".to_string();
221 let case_variant_key = order.owner.to_ascii_uppercase();
222 assert_ne!(case_variant_key, order.owner);
223
224 assert!(!order.is_owned_by("0xother", &case_variant_key));
225 }
226
227 #[rstest]
228 fn test_maker_order_ownership_rejects_foreign_identity() {
229 let order: PolymarketMakerOrder = serde_json::from_str(sample_maker_order_json()).unwrap();
230
231 assert!(!order.is_owned_by("0xother", "no-such-key"));
232 }
233
234 #[rstest]
235 fn test_maker_order_outcome_no() {
236 let json = r#"{
237 "asset_id": "12345",
238 "fee_rate_bps": "0",
239 "maker_address": "0xaddr",
240 "matched_amount": "10.0",
241 "order_id": "order-1",
242 "outcome": "No",
243 "owner": "owner-1",
244 "price": "0.4"
245 }"#;
246 let order: PolymarketMakerOrder = serde_json::from_str(json).unwrap();
247 assert_eq!(order.outcome, PolymarketOutcome::no());
248 }
249
250 #[rstest]
251 fn test_maker_order_decimal_precision() {
252 let order: PolymarketMakerOrder = serde_json::from_str(sample_maker_order_json()).unwrap();
254 let json = serde_json::to_string(&order).unwrap();
255 assert!(
257 json.contains("\"matched_amount\":\"50.0000\"")
258 || json.contains("\"matched_amount\": \"50.0000\"")
259 );
260 }
261
262 #[rstest]
264 fn test_maker_orders_from_trade_report() {
265 let trade: PolymarketTradeReport = load("http_trade_report.json");
266
267 assert_eq!(trade.maker_orders.len(), 2);
268 let m0 = &trade.maker_orders[0];
269 assert_eq!(m0.matched_amount, dec!(25.0000));
270 assert_eq!(m0.outcome, PolymarketOutcome::yes());
271 assert_eq!(m0.side, Some(PolymarketOrderSide::Sell));
272
273 let m1 = &trade.maker_orders[1];
274 assert_eq!(m1.matched_amount, dec!(5.0000));
275 assert_eq!(m1.side, Some(PolymarketOrderSide::Sell));
276 }
277
278 #[rstest]
279 fn test_maker_order_without_side_is_accepted() {
280 let order: PolymarketMakerOrder = serde_json::from_str(sample_maker_order_json()).unwrap();
283 assert!(order.side.is_none());
284 }
285
286 #[rstest]
287 fn test_maker_order_with_side_is_parsed() {
288 let json = r#"{
289 "asset_id": "12345",
290 "fee_rate_bps": "0",
291 "maker_address": "0xaddr",
292 "matched_amount": "10.0",
293 "order_id": "order-1",
294 "outcome": "Yes",
295 "owner": "owner-1",
296 "price": "0.4",
297 "side": "BUY"
298 }"#;
299 let order: PolymarketMakerOrder = serde_json::from_str(json).unwrap();
300 assert_eq!(order.side, Some(PolymarketOrderSide::Buy));
301 }
302
303 #[rstest]
304 fn test_maker_order_with_empty_fee_rate_bps_is_accepted() {
305 let json = r#"{
309 "asset_id": "12345",
310 "fee_rate_bps": "",
311 "maker_address": "0xaddr",
312 "matched_amount": "10.0",
313 "order_id": "order-1",
314 "outcome": "Yes",
315 "owner": "owner-1",
316 "price": "0.4"
317 }"#;
318 let order: PolymarketMakerOrder = serde_json::from_str(json).unwrap();
319 assert_eq!(order.matched_amount, dec!(10.0));
320 }
321}