1use std::str::FromStr;
19
20pub use nautilus_core::serialization::{
21 serialize_decimal_as_str, serialize_optional_decimal_as_str,
22};
23use nautilus_model::identifiers::TradeId;
24use rust_decimal::Decimal;
25use serde::{
26 Deserialize, Deserializer, Serialize, Serializer,
27 de::{self, Error, MapAccess, SeqAccess, Unexpected, Visitor},
28};
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 fn deserialize_decimal_from_json<'de, D>(deserializer: D) -> Result<Decimal, D::Error>
60where
61 D: Deserializer<'de>,
62{
63 let raw = Box::<RawValue>::deserialize(deserializer)?;
64 decimal_from_json(&raw).map_err(D::Error::custom)
65}
66
67pub fn deserialize_optional_decimal_from_json<'de, D>(
73 deserializer: D,
74) -> Result<Option<Decimal>, D::Error>
75where
76 D: Deserializer<'de>,
77{
78 Option::<Box<RawValue>>::deserialize(deserializer)?
79 .map(|raw| decimal_from_json(&raw).map_err(D::Error::custom))
80 .transpose()
81}
82
83pub(crate) fn decimal_from_json(raw: &RawValue) -> anyhow::Result<Decimal> {
84 if raw.get().starts_with('"') {
85 let value: String = serde_json::from_str(raw.get())?;
86 Ok(parse_decimal_exact(&value)?)
87 } else {
88 Ok(parse_decimal_exact(raw.get())?)
89 }
90}
91
92pub(crate) fn parse_decimal_exact(value: &str) -> Result<Decimal, rust_decimal::Error> {
93 if let Some((base, exponent)) = value.split_once(['e', 'E']) {
94 let exponent: i64 = exponent
95 .parse()
96 .map_err(|_| rust_decimal::Error::from("invalid decimal exponent"))?;
97 let negative = base.starts_with('-');
98 let base = base
99 .strip_prefix(['-', '+'])
100 .unwrap_or(base)
101 .replace('_', "");
102 let mut parts = base.split('.');
103 let whole = parts.next().unwrap_or_default();
104
105 let fraction = parts.next().unwrap_or_default();
106 if parts.next().is_some() || whole.is_empty() && fraction.is_empty() {
107 return Err(rust_decimal::Error::from("invalid decimal significand"));
108 }
109
110 let mut digits = format!("{whole}{fraction}");
111 if !digits.bytes().all(|digit| digit.is_ascii_digit()) {
112 return Err(rust_decimal::Error::from("invalid decimal significand"));
113 }
114
115 let mut scale = i64::try_from(fraction.len())
116 .ok()
117 .and_then(|scale| scale.checked_sub(exponent))
118 .ok_or_else(|| rust_decimal::Error::from("decimal scale out of range"))?;
119
120 digits = digits.trim_start_matches('0').to_string();
121 if digits.is_empty() {
122 return Ok(Decimal::ZERO);
123 }
124
125 while scale > 0 && digits.ends_with('0') {
126 digits.pop();
127 scale -= 1;
128 }
129
130 if !(0..=28).contains(&scale) {
131 if (-28..0).contains(&scale) && digits.len() as i64 - scale <= 29 {
132 digits.extend(std::iter::repeat_n('0', (-scale) as usize));
133 scale = 0;
134 } else {
135 return Err(rust_decimal::Error::from("decimal scale out of range"));
136 }
137 }
138
139 let mantissa: i128 = digits
140 .parse()
141 .map_err(|_| rust_decimal::Error::from("decimal mantissa out of range"))?;
142 Decimal::try_from_i128_with_scale(if negative { -mantissa } else { mantissa }, scale as u32)
143 } else {
144 Decimal::from_str_exact(value)
145 }
146}
147
148pub fn deserialize_decimal_from_str<'de, D>(deserializer: D) -> Result<Decimal, D::Error>
154where
155 D: Deserializer<'de>,
156{
157 let value = std::borrow::Cow::<'de, str>::deserialize(deserializer)?;
158 parse_decimal_exact(&value).map_err(D::Error::custom)
159}
160
161pub fn deserialize_optional_decimal_from_str<'de, D>(
167 deserializer: D,
168) -> Result<Option<Decimal>, D::Error>
169where
170 D: Deserializer<'de>,
171{
172 Option::<String>::deserialize(deserializer)?
173 .filter(|value| !value.is_empty())
174 .map(|value| parse_decimal_exact(&value).map_err(D::Error::custom))
175 .transpose()
176}
177
178pub(crate) fn deserialize_crypto_twap_value<'de, D>(deserializer: D) -> Result<Decimal, D::Error>
183where
184 D: Deserializer<'de>,
185{
186 struct DecimalLikeVisitor;
187
188 impl<'de> Visitor<'de> for DecimalLikeVisitor {
189 type Value = Decimal;
190
191 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
192 formatter.write_str("`value` as a finite decimal JSON number or numeric string")
193 }
194
195 fn visit_i64<E: de::Error>(self, value: i64) -> Result<Self::Value, E> {
196 Ok(Decimal::from(value))
197 }
198
199 fn visit_u64<E: de::Error>(self, value: u64) -> Result<Self::Value, E> {
200 Ok(Decimal::from(value))
201 }
202
203 fn visit_i128<E: de::Error>(self, value: i128) -> Result<Self::Value, E> {
204 Decimal::try_from_i128_with_scale(value, 0)
205 .map_err(|e| E::custom(format!("invalid decimal-like `value`: {e}")))
206 }
207
208 fn visit_u128<E: de::Error>(self, value: u128) -> Result<Self::Value, E> {
209 Decimal::from_str(&value.to_string())
210 .map_err(|e| E::custom(format!("invalid decimal-like `value`: {e}")))
211 }
212
213 fn visit_f64<E: de::Error>(self, value: f64) -> Result<Self::Value, E> {
214 if !value.is_finite() {
215 return Err(E::invalid_value(Unexpected::Float(value), &self));
216 }
217
218 Decimal::try_from(value)
219 .map_err(|e| E::custom(format!("invalid decimal-like `value`: {e}")))
220 }
221
222 fn visit_str<E: de::Error>(self, value: &str) -> Result<Self::Value, E> {
223 let result = parse_decimal_exact(value);
224
225 result.map_err(|e| E::custom(format!("invalid decimal-like `value`: {e}")))
226 }
227
228 fn visit_string<E: de::Error>(self, value: String) -> Result<Self::Value, E> {
229 self.visit_str(&value)
230 }
231
232 fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
233 Err(E::invalid_type(Unexpected::Unit, &self))
234 }
235
236 fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
237 Err(E::invalid_type(Unexpected::Option, &self))
238 }
239
240 fn visit_bool<E: de::Error>(self, value: bool) -> Result<Self::Value, E> {
241 Err(E::invalid_type(Unexpected::Bool(value), &self))
242 }
243
244 fn visit_seq<A: SeqAccess<'de>>(self, _seq: A) -> Result<Self::Value, A::Error> {
245 Err(A::Error::invalid_type(Unexpected::Seq, &self))
246 }
247
248 fn visit_map<A: MapAccess<'de>>(self, _map: A) -> Result<Self::Value, A::Error> {
249 Err(A::Error::invalid_type(Unexpected::Map, &self))
250 }
251
252 fn visit_bytes<E: de::Error>(self, value: &[u8]) -> Result<Self::Value, E> {
253 Err(E::invalid_type(Unexpected::Bytes(value), &self))
254 }
255
256 fn visit_byte_buf<E: de::Error>(self, value: Vec<u8>) -> Result<Self::Value, E> {
257 Err(E::invalid_type(Unexpected::Bytes(&value), &self))
258 }
259 }
260
261 deserializer.deserialize_any(DecimalLikeVisitor)
262}
263
264pub fn serialize_decimal_as_json_number<S>(
266 value: &Decimal,
267 serializer: S,
268) -> Result<S::Ok, S::Error>
269where
270 S: Serializer,
271{
272 let raw = RawValue::from_string(value.to_string()).map_err(serde::ser::Error::custom)?;
273 raw.serialize(serializer)
274}
275
276pub fn serialize_optional_decimal_as_json_number<S>(
278 value: &Option<Decimal>,
279 serializer: S,
280) -> Result<S::Ok, S::Error>
281where
282 S: Serializer,
283{
284 match value {
285 Some(value) => serialize_decimal_as_json_number(value, serializer),
286 None => serializer.serialize_none(),
287 }
288}
289
290pub fn deserialize_optional_polymarket_game_id<'de, D>(
299 deserializer: D,
300) -> Result<Option<String>, D::Error>
301where
302 D: Deserializer<'de>,
303{
304 #[derive(Deserialize)]
305 #[serde(untagged)]
306 enum Raw {
307 Str(String),
308 Num(Number),
309 }
310
311 let game_id = match Option::<Raw>::deserialize(deserializer)? {
312 None => return Ok(None),
313 Some(Raw::Str(value)) => value,
314 Some(Raw::Num(value)) => value.to_string(),
315 };
316
317 if game_id.is_empty() || game_id == "-1" {
318 return Ok(None);
319 }
320
321 Ok(Some(game_id))
322}
323
324const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
326const FNV_PRIME: u64 = 0x0100_0000_01b3;
327
328#[must_use]
336pub fn determine_trade_id(
337 asset_id: &str,
338 side: PolymarketOrderSide,
339 price: &str,
340 size: &str,
341 timestamp: &str,
342) -> TradeId {
343 let side_byte: &[u8] = match side {
344 PolymarketOrderSide::Buy => b"B",
345 PolymarketOrderSide::Sell => b"S",
346 };
347 let mut h: u64 = FNV_OFFSET_BASIS;
348
349 for bytes in [
350 asset_id.as_bytes(),
351 b"\x1f",
352 side_byte,
353 b"\x1f",
354 price.as_bytes(),
355 b"\x1f",
356 size.as_bytes(),
357 b"\x1f",
358 timestamp.as_bytes(),
359 ] {
360 for &b in bytes {
361 h ^= u64::from(b);
362 h = h.wrapping_mul(FNV_PRIME);
363 }
364 }
365 TradeId::new(format!("{h:016x}"))
366}
367
368#[cfg(test)]
369mod tests {
370 use rstest::rstest;
371 use serde::{Deserialize, Serialize};
372
373 use super::*;
374
375 #[rstest]
376 #[case("1e-28", "0.0000000000000000000000000001")]
377 #[case("0.00000000000000000000000000001e1", "0.0000000000000000000000000001")]
378 #[case("792281625142643375935439503350e-1", "79228162514264337593543950335")]
379 #[case("-12345e-4", "-1.2345")]
380 #[case("1.234567890123456789012345678e-1", "0.1234567890123456789012345678")]
381 fn test_parse_decimal_exact_scientific(#[case] raw: &str, #[case] expected: &str) {
382 assert_eq!(
383 parse_decimal_exact(raw).unwrap(),
384 Decimal::from_str_exact(expected).unwrap()
385 );
386 }
387
388 #[rstest]
389 #[case("0.12345678901234567890123456789e0")]
390 #[case("0.12345678901234567890123456789")]
391 #[case("1e-29")]
392 #[case("79228162514264337593543950336")]
393 #[case("NaN")]
394 #[case("Infinity")]
395 #[case("--1e0")]
396 #[case("1e9223372036854775808")]
397 fn test_parse_decimal_exact_rejects_invalid_or_inexact(#[case] raw: &str) {
398 assert!(parse_decimal_exact(raw).is_err(), "accepted {raw}");
399 }
400
401 #[derive(Debug, Deserialize)]
402 struct GameIdHolder {
403 #[serde(default, deserialize_with = "deserialize_optional_polymarket_game_id")]
404 game_id: Option<String>,
405 }
406
407 #[derive(Debug, Deserialize, Serialize)]
408 struct JsonDecimalHolder {
409 #[serde(
410 deserialize_with = "deserialize_decimal_from_json_number",
411 serialize_with = "serialize_decimal_as_json_number"
412 )]
413 value: Decimal,
414 #[serde(
415 default,
416 deserialize_with = "deserialize_optional_decimal_from_json_number",
417 serialize_with = "serialize_optional_decimal_as_json_number"
418 )]
419 optional: Option<Decimal>,
420 }
421
422 #[rstest]
423 fn test_json_decimal_number_preserves_precision() {
424 let json =
425 r#"{"value":0.1234567890123456789012345678,"optional":123456789.1234567890123456789}"#;
426 let holder: JsonDecimalHolder = serde_json::from_str(json).unwrap();
427
428 assert_eq!(
429 holder.value,
430 Decimal::from_str_exact("0.1234567890123456789012345678").unwrap()
431 );
432 assert_eq!(
433 holder.optional,
434 Some(Decimal::from_str_exact("123456789.1234567890123456789").unwrap())
435 );
436 assert_eq!(serde_json::to_string(&holder).unwrap(), json);
437 }
438
439 #[rstest]
440 fn test_optional_json_decimal_number_accepts_null_and_missing() {
441 let null: JsonDecimalHolder =
442 serde_json::from_str(r#"{"value":1,"optional":null}"#).unwrap();
443 let missing: JsonDecimalHolder = serde_json::from_str(r#"{"value":1}"#).unwrap();
444
445 assert_eq!(null.value, Decimal::ONE);
446 assert!(null.optional.is_none());
447 assert_eq!(missing.value, Decimal::ONE);
448 assert!(missing.optional.is_none());
449 }
450
451 #[rstest]
452 #[case::null(r#"{"game_id": null}"#, None)]
453 #[case::missing("{}", None)]
454 #[case::empty_string(r#"{"game_id": ""}"#, None)]
455 #[case::int_neg_one(r#"{"game_id": -1}"#, None)]
456 #[case::str_neg_one(r#"{"game_id": "-1"}"#, None)]
457 #[case::int_zero(r#"{"game_id": 0}"#, Some("0"))]
458 #[case::str_zero(r#"{"game_id": "0"}"#, Some("0"))]
459 #[case::int_value(r#"{"game_id": 1427074}"#, Some("1427074"))]
460 #[case::str_value(r#"{"game_id": "1427074"}"#, Some("1427074"))]
461 #[case::composite(
463 r#"{"game_id": "dd80aae9-52f9-4c7b-a1cf-7b4ab63cd281:STL:TEX"}"#,
464 Some("dd80aae9-52f9-4c7b-a1cf-7b4ab63cd281:STL:TEX")
465 )]
466 #[case::composite_rematch(
467 r#"{"game_id": "dd80aae9-52f9-4c7b-a1cf-7b4ab63cd281:DAL:LA:m2"}"#,
468 Some("dd80aae9-52f9-4c7b-a1cf-7b4ab63cd281:DAL:LA:m2")
469 )]
470 #[case::int_neg_other(r#"{"game_id": -2}"#, Some("-2"))]
473 #[case::str_neg_other(r#"{"game_id": "-2"}"#, Some("-2"))]
474 #[case::int_beyond_i64(r#"{"game_id": 18446744073709551615}"#, Some("18446744073709551615"))]
476 fn test_deserialize_optional_polymarket_game_id(
477 #[case] payload: &str,
478 #[case] expected: Option<&str>,
479 ) {
480 let holder: GameIdHolder = serde_json::from_str(payload).unwrap();
481 assert_eq!(holder.game_id.as_deref(), expected);
482 }
483
484 #[rstest]
485 fn test_determine_trade_id_is_deterministic() {
486 let id1 = determine_trade_id("asset-1", PolymarketOrderSide::Buy, "0.5", "10", "1700000");
487 let id2 = determine_trade_id("asset-1", PolymarketOrderSide::Buy, "0.5", "10", "1700000");
488 assert_eq!(id1, id2);
489 }
490
491 #[rstest]
492 fn test_determine_trade_id_differentiates_sides() {
493 let buy = determine_trade_id("asset-1", PolymarketOrderSide::Buy, "0.5", "10", "1700000");
494 let sell = determine_trade_id("asset-1", PolymarketOrderSide::Sell, "0.5", "10", "1700000");
495 assert_ne!(buy, sell);
496 }
497
498 #[rstest]
499 fn test_determine_trade_id_field_delimiter_prevents_collision() {
500 let a = determine_trade_id("asset-1", PolymarketOrderSide::Buy, "0.12", "34", "1700000");
502 let b = determine_trade_id("asset-1", PolymarketOrderSide::Buy, "0.1", "234", "1700000");
503 assert_ne!(a, b);
504 }
505
506 #[rstest]
507 fn test_determine_trade_id_format() {
508 let id = determine_trade_id("asset-1", PolymarketOrderSide::Buy, "0.5", "10", "1700000");
509 let s = id.to_string();
510 assert_eq!(s.len(), 16);
511 assert!(
513 s.chars()
514 .all(|c| c.is_ascii_digit() || ('a'..='f').contains(&c))
515 );
516 }
517}