pub use nautilus_core::serialization::{
deserialize_decimal_from_str, deserialize_optional_decimal_from_str, serialize_decimal_as_str,
serialize_optional_decimal_as_str,
};
use nautilus_model::identifiers::TradeId;
use serde::{Deserialize, Deserializer, de::Error};
use crate::common::enums::PolymarketOrderSide;
pub fn deserialize_optional_polymarket_game_id<'de, D>(
deserializer: D,
) -> Result<Option<u64>, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum Raw {
Str(String),
Int(i64),
}
let raw: Option<Raw> = Option::deserialize(deserializer)?;
match raw {
None => Ok(None),
Some(Raw::Str(s)) if s.is_empty() || s == "-1" => Ok(None),
Some(Raw::Str(s)) => s.parse::<u64>().map(Some).map_err(D::Error::custom),
Some(Raw::Int(-1)) => Ok(None),
Some(Raw::Int(i)) if i < 0 => Err(D::Error::custom(format!(
"negative game_id {i}: only -1 is recognized as the no-game sentinel"
))),
Some(Raw::Int(i)) => Ok(Some(i as u64)),
}
}
const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
const FNV_PRIME: u64 = 0x0100_0000_01b3;
#[must_use]
pub fn determine_trade_id(
asset_id: &str,
side: PolymarketOrderSide,
price: &str,
size: &str,
timestamp: &str,
) -> TradeId {
let side_byte: &[u8] = match side {
PolymarketOrderSide::Buy => b"B",
PolymarketOrderSide::Sell => b"S",
};
let mut h: u64 = FNV_OFFSET_BASIS;
for bytes in [
asset_id.as_bytes(),
b"\x1f",
side_byte,
b"\x1f",
price.as_bytes(),
b"\x1f",
size.as_bytes(),
b"\x1f",
timestamp.as_bytes(),
] {
for &b in bytes {
h ^= u64::from(b);
h = h.wrapping_mul(FNV_PRIME);
}
}
TradeId::new(format!("{h:016x}"))
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use serde::Deserialize;
use super::*;
#[derive(Debug, Deserialize)]
struct GameIdHolder {
#[serde(default, deserialize_with = "deserialize_optional_polymarket_game_id")]
game_id: Option<u64>,
}
#[rstest]
#[case::null(r#"{"game_id": null}"#, None)]
#[case::missing("{}", None)]
#[case::empty_string(r#"{"game_id": ""}"#, None)]
#[case::int_neg_one(r#"{"game_id": -1}"#, None)]
#[case::str_neg_one(r#"{"game_id": "-1"}"#, None)]
#[case::int_zero(r#"{"game_id": 0}"#, Some(0))]
#[case::str_zero(r#"{"game_id": "0"}"#, Some(0))]
#[case::int_value(r#"{"game_id": 1427074}"#, Some(1_427_074))]
#[case::str_value(r#"{"game_id": "1427074"}"#, Some(1_427_074))]
fn test_deserialize_optional_polymarket_game_id(
#[case] payload: &str,
#[case] expected: Option<u64>,
) {
let holder: GameIdHolder = serde_json::from_str(payload).unwrap();
assert_eq!(holder.game_id, expected);
}
#[rstest]
fn test_deserialize_optional_polymarket_game_id_rejects_garbage_string() {
let err = serde_json::from_str::<GameIdHolder>(r#"{"game_id": "not-a-number"}"#);
assert!(err.is_err());
}
#[rstest]
fn test_deserialize_optional_polymarket_game_id_rejects_negative_other_than_minus_one() {
let err = serde_json::from_str::<GameIdHolder>(r#"{"game_id": -2}"#).unwrap_err();
assert!(err.to_string().contains("only -1"));
}
#[rstest]
fn test_deserialize_optional_polymarket_game_id_rejects_negative_string_other_than_minus_one() {
let err = serde_json::from_str::<GameIdHolder>(r#"{"game_id": "-2"}"#);
assert!(err.is_err());
}
#[rstest]
fn test_determine_trade_id_is_deterministic() {
let id1 = determine_trade_id("asset-1", PolymarketOrderSide::Buy, "0.5", "10", "1700000");
let id2 = determine_trade_id("asset-1", PolymarketOrderSide::Buy, "0.5", "10", "1700000");
assert_eq!(id1, id2);
}
#[rstest]
fn test_determine_trade_id_differentiates_sides() {
let buy = determine_trade_id("asset-1", PolymarketOrderSide::Buy, "0.5", "10", "1700000");
let sell = determine_trade_id("asset-1", PolymarketOrderSide::Sell, "0.5", "10", "1700000");
assert_ne!(buy, sell);
}
#[rstest]
fn test_determine_trade_id_field_delimiter_prevents_collision() {
let a = determine_trade_id("asset-1", PolymarketOrderSide::Buy, "0.12", "34", "1700000");
let b = determine_trade_id("asset-1", PolymarketOrderSide::Buy, "0.1", "234", "1700000");
assert_ne!(a, b);
}
#[rstest]
fn test_determine_trade_id_format() {
let id = determine_trade_id("asset-1", PolymarketOrderSide::Buy, "0.5", "10", "1700000");
let s = id.to_string();
assert_eq!(s.len(), 16);
assert!(
s.chars()
.all(|c| c.is_ascii_digit() || ('a'..='f').contains(&c))
);
}
}