nautilus_polymarket/common/
parse.rs1pub(crate) use nautilus_core::serialization::decimal::parse as parse_decimal_exact;
19pub use nautilus_core::serialization::{
20 decimal::{
21 deserialize_json as deserialize_decimal_from_json,
22 deserialize_optional_json as deserialize_optional_decimal_from_json,
23 },
24 serialize_decimal_as_str, serialize_optional_decimal_as_str,
25};
26use nautilus_model::identifiers::TradeId;
27use rust_decimal::Decimal;
28use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error};
29use serde_json::{Number, value::RawValue};
30
31use crate::common::enums::PolymarketOrderSide;
32
33pub fn deserialize_decimal_from_json_number<'de, D>(deserializer: D) -> Result<Decimal, D::Error>
35where
36 D: Deserializer<'de>,
37{
38 let raw = Box::<RawValue>::deserialize(deserializer)?;
39 parse_decimal_exact(raw.get()).map_err(D::Error::custom)
40}
41
42pub fn deserialize_optional_decimal_from_json_number<'de, D>(
44 deserializer: D,
45) -> Result<Option<Decimal>, D::Error>
46where
47 D: Deserializer<'de>,
48{
49 Option::<Box<RawValue>>::deserialize(deserializer)?
50 .map(|raw| parse_decimal_exact(raw.get()).map_err(D::Error::custom))
51 .transpose()
52}
53
54pub(crate) fn decimal_from_json(raw: &RawValue) -> anyhow::Result<Decimal> {
55 Ok(deserialize_decimal_from_json(raw)?)
56}
57
58pub fn deserialize_decimal_from_str<'de, D>(deserializer: D) -> Result<Decimal, D::Error>
64where
65 D: Deserializer<'de>,
66{
67 let value = std::borrow::Cow::<'de, str>::deserialize(deserializer)?;
68 parse_decimal_exact(&value).map_err(D::Error::custom)
69}
70
71pub fn deserialize_optional_decimal_from_str<'de, D>(
77 deserializer: D,
78) -> Result<Option<Decimal>, D::Error>
79where
80 D: Deserializer<'de>,
81{
82 Option::<String>::deserialize(deserializer)?
83 .filter(|value| !value.is_empty())
84 .map(|value| parse_decimal_exact(&value).map_err(D::Error::custom))
85 .transpose()
86}
87
88pub(crate) fn deserialize_crypto_twap_value<'de, D>(deserializer: D) -> Result<Decimal, D::Error>
93where
94 D: Deserializer<'de>,
95{
96 nautilus_core::serialization::decimal::deserialize_json(deserializer)
97 .map_err(|e| D::Error::custom(format!("invalid decimal-like `value`: {e}")))
98}
99
100pub fn serialize_decimal_as_json_number<S>(
102 value: &Decimal,
103 serializer: S,
104) -> Result<S::Ok, S::Error>
105where
106 S: Serializer,
107{
108 let raw = RawValue::from_string(value.to_string()).map_err(serde::ser::Error::custom)?;
109 raw.serialize(serializer)
110}
111
112pub fn serialize_optional_decimal_as_json_number<S>(
114 value: &Option<Decimal>,
115 serializer: S,
116) -> Result<S::Ok, S::Error>
117where
118 S: Serializer,
119{
120 match value {
121 Some(value) => serialize_decimal_as_json_number(value, serializer),
122 None => serializer.serialize_none(),
123 }
124}
125
126pub fn deserialize_optional_polymarket_game_id<'de, D>(
135 deserializer: D,
136) -> Result<Option<String>, D::Error>
137where
138 D: Deserializer<'de>,
139{
140 #[derive(Deserialize)]
141 #[serde(untagged)]
142 enum Raw {
143 Str(String),
144 Num(Number),
145 }
146
147 let game_id = match Option::<Raw>::deserialize(deserializer)? {
148 None => return Ok(None),
149 Some(Raw::Str(value)) => value,
150 Some(Raw::Num(value)) => value.to_string(),
151 };
152
153 if game_id.is_empty() || game_id == "-1" {
154 return Ok(None);
155 }
156
157 Ok(Some(game_id))
158}
159
160const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
162const FNV_PRIME: u64 = 0x0100_0000_01b3;
163
164#[must_use]
172pub fn determine_trade_id(
173 asset_id: &str,
174 side: PolymarketOrderSide,
175 price: &str,
176 size: &str,
177 timestamp: &str,
178) -> TradeId {
179 let side_byte: &[u8] = match side {
180 PolymarketOrderSide::Buy => b"B",
181 PolymarketOrderSide::Sell => b"S",
182 };
183 let mut h: u64 = FNV_OFFSET_BASIS;
184
185 for bytes in [
186 asset_id.as_bytes(),
187 b"\x1f",
188 side_byte,
189 b"\x1f",
190 price.as_bytes(),
191 b"\x1f",
192 size.as_bytes(),
193 b"\x1f",
194 timestamp.as_bytes(),
195 ] {
196 for &b in bytes {
197 h ^= u64::from(b);
198 h = h.wrapping_mul(FNV_PRIME);
199 }
200 }
201 TradeId::new(format!("{h:016x}"))
202}
203
204#[cfg(test)]
205mod tests {
206 use rstest::rstest;
207 use serde::{Deserialize, Serialize};
208
209 use super::*;
210
211 #[rstest]
212 fn test_raw_decimal_shapes_and_twap_value() {
213 let tokens: Vec<Box<RawValue>> =
214 serde_json::from_str(include_str!("../../test_data/decimal_tokens.json")).unwrap();
215 let expected = [
216 Decimal::from_str_exact("0.1234567890123456789012345678").unwrap(),
217 Decimal::from_str_exact("123456789.1234567890123456789").unwrap(),
218 ];
219
220 for (token, expected) in tokens.iter().zip(expected) {
221 assert_eq!(
222 deserialize_decimal_from_json(token.as_ref()).unwrap(),
223 expected
224 );
225 assert_eq!(
226 deserialize_crypto_twap_value(token.as_ref()).unwrap(),
227 expected
228 );
229 assert_eq!(
230 deserialize_optional_decimal_from_json(token.as_ref()).unwrap(),
231 Some(expected)
232 );
233 }
234
235 assert_eq!(
236 deserialize_decimal_from_json_number(tokens[0].as_ref()).unwrap(),
237 expected[0]
238 );
239 assert!(deserialize_decimal_from_json_number(tokens[1].as_ref()).is_err());
240 assert_eq!(
241 deserialize_optional_decimal_from_json(tokens[3].as_ref()).unwrap(),
242 None
243 );
244
245 for token in &tokens[2..] {
246 assert!(deserialize_decimal_from_json(token.as_ref()).is_err());
247 assert!(
248 deserialize_crypto_twap_value(token.as_ref())
249 .unwrap_err()
250 .to_string()
251 .contains("`value`")
252 );
253 }
254 }
255
256 #[rstest]
257 #[case("1e-28", "0.0000000000000000000000000001")]
258 #[case("0.00000000000000000000000000001e1", "0.0000000000000000000000000001")]
259 #[case("792281625142643375935439503350e-1", "79228162514264337593543950335")]
260 #[case("-12345e-4", "-1.2345")]
261 #[case("1.234567890123456789012345678e-1", "0.1234567890123456789012345678")]
262 fn test_parse_decimal_exact_scientific(#[case] raw: &str, #[case] expected: &str) {
263 assert_eq!(
264 parse_decimal_exact(raw).unwrap(),
265 Decimal::from_str_exact(expected).unwrap()
266 );
267 }
268
269 #[rstest]
270 #[case("0.12345678901234567890123456789e0")]
271 #[case("0.12345678901234567890123456789")]
272 #[case("1e-29")]
273 #[case("79228162514264337593543950336")]
274 #[case("NaN")]
275 #[case("Infinity")]
276 #[case("--1e0")]
277 #[case("1e9223372036854775808")]
278 fn test_parse_decimal_exact_rejects_invalid_or_inexact(#[case] raw: &str) {
279 assert!(parse_decimal_exact(raw).is_err(), "accepted {raw}");
280 }
281
282 #[derive(Debug, Deserialize)]
283 struct GameIdHolder {
284 #[serde(default, deserialize_with = "deserialize_optional_polymarket_game_id")]
285 game_id: Option<String>,
286 }
287
288 #[derive(Debug, Deserialize, Serialize)]
289 struct JsonDecimalHolder {
290 #[serde(
291 deserialize_with = "deserialize_decimal_from_json_number",
292 serialize_with = "serialize_decimal_as_json_number"
293 )]
294 value: Decimal,
295 #[serde(
296 default,
297 deserialize_with = "deserialize_optional_decimal_from_json_number",
298 serialize_with = "serialize_optional_decimal_as_json_number"
299 )]
300 optional: Option<Decimal>,
301 }
302
303 #[rstest]
304 fn test_json_decimal_number_preserves_precision() {
305 let json =
306 r#"{"value":0.1234567890123456789012345678,"optional":123456789.1234567890123456789}"#;
307 let holder: JsonDecimalHolder = serde_json::from_str(json).unwrap();
308
309 assert_eq!(
310 holder.value,
311 Decimal::from_str_exact("0.1234567890123456789012345678").unwrap()
312 );
313 assert_eq!(
314 holder.optional,
315 Some(Decimal::from_str_exact("123456789.1234567890123456789").unwrap())
316 );
317 assert_eq!(serde_json::to_string(&holder).unwrap(), json);
318 }
319
320 #[rstest]
321 fn test_optional_json_decimal_number_accepts_null_and_missing() {
322 let null: JsonDecimalHolder =
323 serde_json::from_str(r#"{"value":1,"optional":null}"#).unwrap();
324 let missing: JsonDecimalHolder = serde_json::from_str(r#"{"value":1}"#).unwrap();
325
326 assert_eq!(null.value, Decimal::ONE);
327 assert!(null.optional.is_none());
328 assert_eq!(missing.value, Decimal::ONE);
329 assert!(missing.optional.is_none());
330 }
331
332 #[rstest]
333 #[case::null(r#"{"game_id": null}"#, None)]
334 #[case::missing("{}", None)]
335 #[case::empty_string(r#"{"game_id": ""}"#, None)]
336 #[case::int_neg_one(r#"{"game_id": -1}"#, None)]
337 #[case::str_neg_one(r#"{"game_id": "-1"}"#, None)]
338 #[case::int_zero(r#"{"game_id": 0}"#, Some("0"))]
339 #[case::str_zero(r#"{"game_id": "0"}"#, Some("0"))]
340 #[case::int_value(r#"{"game_id": 1427074}"#, Some("1427074"))]
341 #[case::str_value(r#"{"game_id": "1427074"}"#, Some("1427074"))]
342 #[case::composite(
344 r#"{"game_id": "dd80aae9-52f9-4c7b-a1cf-7b4ab63cd281:STL:TEX"}"#,
345 Some("dd80aae9-52f9-4c7b-a1cf-7b4ab63cd281:STL:TEX")
346 )]
347 #[case::composite_rematch(
348 r#"{"game_id": "dd80aae9-52f9-4c7b-a1cf-7b4ab63cd281:DAL:LA:m2"}"#,
349 Some("dd80aae9-52f9-4c7b-a1cf-7b4ab63cd281:DAL:LA:m2")
350 )]
351 #[case::int_neg_other(r#"{"game_id": -2}"#, Some("-2"))]
354 #[case::str_neg_other(r#"{"game_id": "-2"}"#, Some("-2"))]
355 #[case::int_beyond_i64(r#"{"game_id": 18446744073709551615}"#, Some("18446744073709551615"))]
357 fn test_deserialize_optional_polymarket_game_id(
358 #[case] payload: &str,
359 #[case] expected: Option<&str>,
360 ) {
361 let holder: GameIdHolder = serde_json::from_str(payload).unwrap();
362 assert_eq!(holder.game_id.as_deref(), expected);
363 }
364
365 #[rstest]
366 fn test_determine_trade_id_is_deterministic() {
367 let id1 = determine_trade_id("asset-1", PolymarketOrderSide::Buy, "0.5", "10", "1700000");
368 let id2 = determine_trade_id("asset-1", PolymarketOrderSide::Buy, "0.5", "10", "1700000");
369 assert_eq!(id1, id2);
370 }
371
372 #[rstest]
373 fn test_determine_trade_id_differentiates_sides() {
374 let buy = determine_trade_id("asset-1", PolymarketOrderSide::Buy, "0.5", "10", "1700000");
375 let sell = determine_trade_id("asset-1", PolymarketOrderSide::Sell, "0.5", "10", "1700000");
376 assert_ne!(buy, sell);
377 }
378
379 #[rstest]
380 fn test_determine_trade_id_field_delimiter_prevents_collision() {
381 let a = determine_trade_id("asset-1", PolymarketOrderSide::Buy, "0.12", "34", "1700000");
383 let b = determine_trade_id("asset-1", PolymarketOrderSide::Buy, "0.1", "234", "1700000");
384 assert_ne!(a, b);
385 }
386
387 #[rstest]
388 fn test_determine_trade_id_format() {
389 let id = determine_trade_id("asset-1", PolymarketOrderSide::Buy, "0.5", "10", "1700000");
390 let s = id.to_string();
391 assert_eq!(s.len(), 16);
392 assert!(
394 s.chars()
395 .all(|c| c.is_ascii_digit() || ('a'..='f').contains(&c))
396 );
397 }
398}