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