nautilus_polymarket/common/
parse.rs1pub use nautilus_core::serialization::{
19 deserialize_decimal_from_str, deserialize_optional_decimal_from_str, serialize_decimal_as_str,
20 serialize_optional_decimal_as_str,
21};
22use nautilus_model::identifiers::TradeId;
23use serde::{Deserialize, Deserializer, de::Error};
24
25use crate::common::enums::PolymarketOrderSide;
26
27pub fn deserialize_optional_polymarket_game_id<'de, D>(
33 deserializer: D,
34) -> Result<Option<u64>, D::Error>
35where
36 D: Deserializer<'de>,
37{
38 #[derive(Deserialize)]
39 #[serde(untagged)]
40 enum Raw {
41 Str(String),
42 Int(i64),
43 }
44
45 let raw: Option<Raw> = Option::deserialize(deserializer)?;
46 match raw {
47 None => Ok(None),
48 Some(Raw::Str(s)) if s.is_empty() || s == "-1" => Ok(None),
49 Some(Raw::Str(s)) => s.parse::<u64>().map(Some).map_err(D::Error::custom),
50 Some(Raw::Int(-1)) => Ok(None),
51 Some(Raw::Int(i)) if i < 0 => Err(D::Error::custom(format!(
52 "negative game_id {i}: only -1 is recognized as the no-game sentinel"
53 ))),
54 Some(Raw::Int(i)) => Ok(Some(i as u64)),
55 }
56}
57
58const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
60const FNV_PRIME: u64 = 0x0100_0000_01b3;
61
62#[must_use]
70pub fn determine_trade_id(
71 asset_id: &str,
72 side: PolymarketOrderSide,
73 price: &str,
74 size: &str,
75 timestamp: &str,
76) -> TradeId {
77 let side_byte: &[u8] = match side {
78 PolymarketOrderSide::Buy => b"B",
79 PolymarketOrderSide::Sell => b"S",
80 };
81 let mut h: u64 = FNV_OFFSET_BASIS;
82
83 for bytes in [
84 asset_id.as_bytes(),
85 b"\x1f",
86 side_byte,
87 b"\x1f",
88 price.as_bytes(),
89 b"\x1f",
90 size.as_bytes(),
91 b"\x1f",
92 timestamp.as_bytes(),
93 ] {
94 for &b in bytes {
95 h ^= u64::from(b);
96 h = h.wrapping_mul(FNV_PRIME);
97 }
98 }
99 TradeId::new(format!("{h:016x}"))
100}
101
102#[cfg(test)]
103mod tests {
104 use rstest::rstest;
105 use serde::Deserialize;
106
107 use super::*;
108
109 #[derive(Debug, Deserialize)]
110 struct GameIdHolder {
111 #[serde(default, deserialize_with = "deserialize_optional_polymarket_game_id")]
112 game_id: Option<u64>,
113 }
114
115 #[rstest]
116 #[case::null(r#"{"game_id": null}"#, None)]
117 #[case::missing("{}", None)]
118 #[case::empty_string(r#"{"game_id": ""}"#, None)]
119 #[case::int_neg_one(r#"{"game_id": -1}"#, None)]
120 #[case::str_neg_one(r#"{"game_id": "-1"}"#, None)]
121 #[case::int_zero(r#"{"game_id": 0}"#, Some(0))]
122 #[case::str_zero(r#"{"game_id": "0"}"#, Some(0))]
123 #[case::int_value(r#"{"game_id": 1427074}"#, Some(1_427_074))]
124 #[case::str_value(r#"{"game_id": "1427074"}"#, Some(1_427_074))]
125 fn test_deserialize_optional_polymarket_game_id(
126 #[case] payload: &str,
127 #[case] expected: Option<u64>,
128 ) {
129 let holder: GameIdHolder = serde_json::from_str(payload).unwrap();
130 assert_eq!(holder.game_id, expected);
131 }
132
133 #[rstest]
134 fn test_deserialize_optional_polymarket_game_id_rejects_garbage_string() {
135 let err = serde_json::from_str::<GameIdHolder>(r#"{"game_id": "not-a-number"}"#);
136 assert!(err.is_err());
137 }
138
139 #[rstest]
140 fn test_deserialize_optional_polymarket_game_id_rejects_negative_other_than_minus_one() {
141 let err = serde_json::from_str::<GameIdHolder>(r#"{"game_id": -2}"#).unwrap_err();
145 assert!(err.to_string().contains("only -1"));
146 }
147
148 #[rstest]
149 fn test_deserialize_optional_polymarket_game_id_rejects_negative_string_other_than_minus_one() {
150 let err = serde_json::from_str::<GameIdHolder>(r#"{"game_id": "-2"}"#);
153 assert!(err.is_err());
154 }
155
156 #[rstest]
157 fn test_determine_trade_id_is_deterministic() {
158 let id1 = determine_trade_id("asset-1", PolymarketOrderSide::Buy, "0.5", "10", "1700000");
159 let id2 = determine_trade_id("asset-1", PolymarketOrderSide::Buy, "0.5", "10", "1700000");
160 assert_eq!(id1, id2);
161 }
162
163 #[rstest]
164 fn test_determine_trade_id_differentiates_sides() {
165 let buy = determine_trade_id("asset-1", PolymarketOrderSide::Buy, "0.5", "10", "1700000");
166 let sell = determine_trade_id("asset-1", PolymarketOrderSide::Sell, "0.5", "10", "1700000");
167 assert_ne!(buy, sell);
168 }
169
170 #[rstest]
171 fn test_determine_trade_id_field_delimiter_prevents_collision() {
172 let a = determine_trade_id("asset-1", PolymarketOrderSide::Buy, "0.12", "34", "1700000");
174 let b = determine_trade_id("asset-1", PolymarketOrderSide::Buy, "0.1", "234", "1700000");
175 assert_ne!(a, b);
176 }
177
178 #[rstest]
179 fn test_determine_trade_id_format() {
180 let id = determine_trade_id("asset-1", PolymarketOrderSide::Buy, "0.5", "10", "1700000");
181 let s = id.to_string();
182 assert_eq!(s.len(), 16);
183 assert!(
185 s.chars()
186 .all(|c| c.is_ascii_digit() || ('a'..='f').contains(&c))
187 );
188 }
189}