1use std::str::FromStr;
19
20pub use nautilus_core::serialization::{
21 deserialize_decimal_from_str, deserialize_optional_decimal_from_str, serialize_decimal_as_str,
22 serialize_optional_decimal_as_str,
23};
24use nautilus_model::identifiers::TradeId;
25use rust_decimal::Decimal;
26use serde::{
27 Deserialize, Deserializer, Serialize, Serializer,
28 de::{self, Error, MapAccess, SeqAccess, Unexpected, Visitor},
29};
30use serde_json::{Number, value::RawValue};
31
32use crate::common::enums::PolymarketOrderSide;
33
34pub fn deserialize_decimal_from_json_number<'de, D>(deserializer: D) -> Result<Decimal, D::Error>
36where
37 D: Deserializer<'de>,
38{
39 let raw = Box::<RawValue>::deserialize(deserializer)?;
40 Decimal::from_str_exact(raw.get()).map_err(D::Error::custom)
41}
42
43pub fn deserialize_optional_decimal_from_json_number<'de, D>(
45 deserializer: D,
46) -> Result<Option<Decimal>, D::Error>
47where
48 D: Deserializer<'de>,
49{
50 Option::<Box<RawValue>>::deserialize(deserializer)?
51 .map(|raw| Decimal::from_str_exact(raw.get()).map_err(D::Error::custom))
52 .transpose()
53}
54
55pub(crate) fn deserialize_crypto_twap_value<'de, D>(deserializer: D) -> Result<Decimal, D::Error>
60where
61 D: Deserializer<'de>,
62{
63 struct DecimalLikeVisitor;
64
65 impl<'de> Visitor<'de> for DecimalLikeVisitor {
66 type Value = Decimal;
67
68 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 formatter.write_str("`value` as a finite decimal JSON number or numeric string")
70 }
71
72 fn visit_i64<E: de::Error>(self, value: i64) -> Result<Self::Value, E> {
73 Ok(Decimal::from(value))
74 }
75
76 fn visit_u64<E: de::Error>(self, value: u64) -> Result<Self::Value, E> {
77 Ok(Decimal::from(value))
78 }
79
80 fn visit_i128<E: de::Error>(self, value: i128) -> Result<Self::Value, E> {
81 Decimal::try_from_i128_with_scale(value, 0)
82 .map_err(|e| E::custom(format!("invalid decimal-like `value`: {e}")))
83 }
84
85 fn visit_u128<E: de::Error>(self, value: u128) -> Result<Self::Value, E> {
86 Decimal::from_str(&value.to_string())
87 .map_err(|e| E::custom(format!("invalid decimal-like `value`: {e}")))
88 }
89
90 fn visit_f64<E: de::Error>(self, value: f64) -> Result<Self::Value, E> {
91 if !value.is_finite() {
92 return Err(E::invalid_value(Unexpected::Float(value), &self));
93 }
94
95 Decimal::try_from(value)
96 .map_err(|e| E::custom(format!("invalid decimal-like `value`: {e}")))
97 }
98
99 fn visit_str<E: de::Error>(self, value: &str) -> Result<Self::Value, E> {
100 let result = if value.contains('e') || value.contains('E') {
101 Decimal::from_scientific(value)
102 } else {
103 Decimal::from_str(value)
104 };
105
106 result.map_err(|e| E::custom(format!("invalid decimal-like `value`: {e}")))
107 }
108
109 fn visit_string<E: de::Error>(self, value: String) -> Result<Self::Value, E> {
110 self.visit_str(&value)
111 }
112
113 fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
114 Err(E::invalid_type(Unexpected::Unit, &self))
115 }
116
117 fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
118 Err(E::invalid_type(Unexpected::Option, &self))
119 }
120
121 fn visit_bool<E: de::Error>(self, value: bool) -> Result<Self::Value, E> {
122 Err(E::invalid_type(Unexpected::Bool(value), &self))
123 }
124
125 fn visit_seq<A: SeqAccess<'de>>(self, _seq: A) -> Result<Self::Value, A::Error> {
126 Err(A::Error::invalid_type(Unexpected::Seq, &self))
127 }
128
129 fn visit_map<A: MapAccess<'de>>(self, _map: A) -> Result<Self::Value, A::Error> {
130 Err(A::Error::invalid_type(Unexpected::Map, &self))
131 }
132
133 fn visit_bytes<E: de::Error>(self, value: &[u8]) -> Result<Self::Value, E> {
134 Err(E::invalid_type(Unexpected::Bytes(value), &self))
135 }
136
137 fn visit_byte_buf<E: de::Error>(self, value: Vec<u8>) -> Result<Self::Value, E> {
138 Err(E::invalid_type(Unexpected::Bytes(&value), &self))
139 }
140 }
141
142 deserializer.deserialize_any(DecimalLikeVisitor)
143}
144
145pub fn serialize_decimal_as_json_number<S>(
147 value: &Decimal,
148 serializer: S,
149) -> Result<S::Ok, S::Error>
150where
151 S: Serializer,
152{
153 let raw = RawValue::from_string(value.to_string()).map_err(serde::ser::Error::custom)?;
154 raw.serialize(serializer)
155}
156
157pub fn serialize_optional_decimal_as_json_number<S>(
159 value: &Option<Decimal>,
160 serializer: S,
161) -> Result<S::Ok, S::Error>
162where
163 S: Serializer,
164{
165 match value {
166 Some(value) => serialize_decimal_as_json_number(value, serializer),
167 None => serializer.serialize_none(),
168 }
169}
170
171pub fn deserialize_optional_polymarket_game_id<'de, D>(
180 deserializer: D,
181) -> Result<Option<String>, D::Error>
182where
183 D: Deserializer<'de>,
184{
185 #[derive(Deserialize)]
186 #[serde(untagged)]
187 enum Raw {
188 Str(String),
189 Num(Number),
190 }
191
192 let game_id = match Option::<Raw>::deserialize(deserializer)? {
193 None => return Ok(None),
194 Some(Raw::Str(value)) => value,
195 Some(Raw::Num(value)) => value.to_string(),
196 };
197
198 if game_id.is_empty() || game_id == "-1" {
199 return Ok(None);
200 }
201
202 Ok(Some(game_id))
203}
204
205const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
207const FNV_PRIME: u64 = 0x0100_0000_01b3;
208
209#[must_use]
217pub fn determine_trade_id(
218 asset_id: &str,
219 side: PolymarketOrderSide,
220 price: &str,
221 size: &str,
222 timestamp: &str,
223) -> TradeId {
224 let side_byte: &[u8] = match side {
225 PolymarketOrderSide::Buy => b"B",
226 PolymarketOrderSide::Sell => b"S",
227 };
228 let mut h: u64 = FNV_OFFSET_BASIS;
229
230 for bytes in [
231 asset_id.as_bytes(),
232 b"\x1f",
233 side_byte,
234 b"\x1f",
235 price.as_bytes(),
236 b"\x1f",
237 size.as_bytes(),
238 b"\x1f",
239 timestamp.as_bytes(),
240 ] {
241 for &b in bytes {
242 h ^= u64::from(b);
243 h = h.wrapping_mul(FNV_PRIME);
244 }
245 }
246 TradeId::new(format!("{h:016x}"))
247}
248
249#[cfg(test)]
250mod tests {
251 use rstest::rstest;
252 use serde::{Deserialize, Serialize};
253
254 use super::*;
255
256 #[derive(Debug, Deserialize)]
257 struct GameIdHolder {
258 #[serde(default, deserialize_with = "deserialize_optional_polymarket_game_id")]
259 game_id: Option<String>,
260 }
261
262 #[derive(Debug, Deserialize, Serialize)]
263 struct JsonDecimalHolder {
264 #[serde(
265 deserialize_with = "deserialize_decimal_from_json_number",
266 serialize_with = "serialize_decimal_as_json_number"
267 )]
268 value: Decimal,
269 #[serde(
270 default,
271 deserialize_with = "deserialize_optional_decimal_from_json_number",
272 serialize_with = "serialize_optional_decimal_as_json_number"
273 )]
274 optional: Option<Decimal>,
275 }
276
277 #[rstest]
278 fn test_json_decimal_number_preserves_precision() {
279 let json =
280 r#"{"value":0.1234567890123456789012345678,"optional":123456789.1234567890123456789}"#;
281 let holder: JsonDecimalHolder = serde_json::from_str(json).unwrap();
282
283 assert_eq!(
284 holder.value,
285 Decimal::from_str_exact("0.1234567890123456789012345678").unwrap()
286 );
287 assert_eq!(
288 holder.optional,
289 Some(Decimal::from_str_exact("123456789.1234567890123456789").unwrap())
290 );
291 assert_eq!(serde_json::to_string(&holder).unwrap(), json);
292 }
293
294 #[rstest]
295 fn test_optional_json_decimal_number_accepts_null_and_missing() {
296 let null: JsonDecimalHolder =
297 serde_json::from_str(r#"{"value":1,"optional":null}"#).unwrap();
298 let missing: JsonDecimalHolder = serde_json::from_str(r#"{"value":1}"#).unwrap();
299
300 assert_eq!(null.value, Decimal::ONE);
301 assert!(null.optional.is_none());
302 assert_eq!(missing.value, Decimal::ONE);
303 assert!(missing.optional.is_none());
304 }
305
306 #[rstest]
307 #[case::null(r#"{"game_id": null}"#, None)]
308 #[case::missing("{}", None)]
309 #[case::empty_string(r#"{"game_id": ""}"#, None)]
310 #[case::int_neg_one(r#"{"game_id": -1}"#, None)]
311 #[case::str_neg_one(r#"{"game_id": "-1"}"#, None)]
312 #[case::int_zero(r#"{"game_id": 0}"#, Some("0"))]
313 #[case::str_zero(r#"{"game_id": "0"}"#, Some("0"))]
314 #[case::int_value(r#"{"game_id": 1427074}"#, Some("1427074"))]
315 #[case::str_value(r#"{"game_id": "1427074"}"#, Some("1427074"))]
316 #[case::composite(
318 r#"{"game_id": "dd80aae9-52f9-4c7b-a1cf-7b4ab63cd281:STL:TEX"}"#,
319 Some("dd80aae9-52f9-4c7b-a1cf-7b4ab63cd281:STL:TEX")
320 )]
321 #[case::composite_rematch(
322 r#"{"game_id": "dd80aae9-52f9-4c7b-a1cf-7b4ab63cd281:DAL:LA:m2"}"#,
323 Some("dd80aae9-52f9-4c7b-a1cf-7b4ab63cd281:DAL:LA:m2")
324 )]
325 #[case::int_neg_other(r#"{"game_id": -2}"#, Some("-2"))]
328 #[case::str_neg_other(r#"{"game_id": "-2"}"#, Some("-2"))]
329 #[case::int_beyond_i64(r#"{"game_id": 18446744073709551615}"#, Some("18446744073709551615"))]
331 fn test_deserialize_optional_polymarket_game_id(
332 #[case] payload: &str,
333 #[case] expected: Option<&str>,
334 ) {
335 let holder: GameIdHolder = serde_json::from_str(payload).unwrap();
336 assert_eq!(holder.game_id.as_deref(), expected);
337 }
338
339 #[rstest]
340 fn test_determine_trade_id_is_deterministic() {
341 let id1 = determine_trade_id("asset-1", PolymarketOrderSide::Buy, "0.5", "10", "1700000");
342 let id2 = determine_trade_id("asset-1", PolymarketOrderSide::Buy, "0.5", "10", "1700000");
343 assert_eq!(id1, id2);
344 }
345
346 #[rstest]
347 fn test_determine_trade_id_differentiates_sides() {
348 let buy = determine_trade_id("asset-1", PolymarketOrderSide::Buy, "0.5", "10", "1700000");
349 let sell = determine_trade_id("asset-1", PolymarketOrderSide::Sell, "0.5", "10", "1700000");
350 assert_ne!(buy, sell);
351 }
352
353 #[rstest]
354 fn test_determine_trade_id_field_delimiter_prevents_collision() {
355 let a = determine_trade_id("asset-1", PolymarketOrderSide::Buy, "0.12", "34", "1700000");
357 let b = determine_trade_id("asset-1", PolymarketOrderSide::Buy, "0.1", "234", "1700000");
358 assert_ne!(a, b);
359 }
360
361 #[rstest]
362 fn test_determine_trade_id_format() {
363 let id = determine_trade_id("asset-1", PolymarketOrderSide::Buy, "0.5", "10", "1700000");
364 let s = id.to_string();
365 assert_eq!(s.len(), 16);
366 assert!(
368 s.chars()
369 .all(|c| c.is_ascii_digit() || ('a'..='f').contains(&c))
370 );
371 }
372}