1use std::sync::Arc;
19
20#[cfg(test)]
21use nautilus_core::string::secret::REDACTED;
22use nautilus_core::string::secret::SecretString;
23use rust_decimal::Decimal;
24use serde::{Deserialize, Serialize};
25use ustr::Ustr;
26
27use crate::common::{
28 enums::{
29 PolymarketLiquiditySide, PolymarketOrderSide, PolymarketOrderStatus, PolymarketOrderType,
30 PolymarketOutcome, PolymarketSignatureType, PolymarketTradeStatus,
31 },
32 models::PolymarketMakerOrder,
33 parse::{
34 deserialize_decimal_from_json, deserialize_decimal_from_json_number,
35 deserialize_decimal_from_str, deserialize_optional_decimal_from_json,
36 deserialize_optional_decimal_from_json_number, deserialize_optional_polymarket_game_id,
37 serialize_decimal_as_json_number, serialize_decimal_as_str,
38 serialize_optional_decimal_as_json_number,
39 },
40};
41
42macro_rules! impl_gamma_response_serde {
43 ($record:ty) => {
44 impl<'de> Deserialize<'de> for $record {
45 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
46 where
47 D: serde::Deserializer<'de>,
48 {
49 let raw = Box::<serde_json::value::RawValue>::deserialize(deserializer)?;
50
51 let mut value =
53 Self::deserialize(&mut serde_json::Deserializer::from_str(raw.get()))
54 .map_err(serde::de::Error::custom)?;
55 value.raw = raw.get().to_owned();
56 Ok(value)
57 }
58 }
59
60 impl Serialize for $record {
61 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
62 where
63 S: serde::Serializer,
64 {
65 Self::serialize(self, serializer)
66 }
67 }
68 };
69}
70
71#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
80#[serde(rename_all = "camelCase")]
81pub struct PolymarketOrder {
82 pub salt: u64,
83 pub maker: String,
84 pub signer: String,
85 pub token_id: Ustr,
86 #[serde(
87 serialize_with = "serialize_decimal_as_str",
88 deserialize_with = "deserialize_decimal_from_str"
89 )]
90 pub maker_amount: Decimal,
91 #[serde(
92 serialize_with = "serialize_decimal_as_str",
93 deserialize_with = "deserialize_decimal_from_str"
94 )]
95 pub taker_amount: Decimal,
96 pub side: PolymarketOrderSide,
97 pub signature_type: PolymarketSignatureType,
98 pub expiration: String,
101 pub timestamp: String,
104 pub metadata: String,
106 pub builder: String,
108 pub signature: SecretString,
109}
110
111#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
115pub struct PolymarketOpenOrder {
116 pub associate_trades: Option<Vec<String>>,
117 pub id: String,
118 pub status: PolymarketOrderStatus,
119 pub market: Ustr,
120 #[serde(
121 serialize_with = "serialize_decimal_as_str",
122 deserialize_with = "deserialize_decimal_from_str"
123 )]
124 pub original_size: Decimal,
125 pub outcome: PolymarketOutcome,
126 pub maker_address: String,
127 pub owner: String,
128 #[serde(
129 serialize_with = "serialize_decimal_as_str",
130 deserialize_with = "deserialize_decimal_from_str"
131 )]
132 pub price: Decimal,
133 pub side: PolymarketOrderSide,
134 #[serde(
135 serialize_with = "serialize_decimal_as_str",
136 deserialize_with = "deserialize_decimal_from_str"
137 )]
138 pub size_matched: Decimal,
139 pub asset_id: Ustr,
140 pub expiration: Option<String>,
141 pub order_type: PolymarketOrderType,
142 pub created_at: u64,
143}
144
145#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
149pub struct PolymarketTradeReport {
150 pub id: String,
151 pub taker_order_id: String,
152 pub market: Ustr,
153 pub asset_id: Ustr,
154 pub side: PolymarketOrderSide,
155 #[serde(
156 serialize_with = "serialize_decimal_as_str",
157 deserialize_with = "deserialize_decimal_from_str"
158 )]
159 pub size: Decimal,
160 #[serde(
161 serialize_with = "serialize_decimal_as_str",
162 deserialize_with = "deserialize_decimal_from_str"
163 )]
164 pub fee_rate_bps: Decimal,
165 #[serde(
166 serialize_with = "serialize_decimal_as_str",
167 deserialize_with = "deserialize_decimal_from_str"
168 )]
169 pub price: Decimal,
170 pub status: PolymarketTradeStatus,
171 pub match_time: String,
172 pub last_update: String,
173 pub outcome: PolymarketOutcome,
174 pub bucket_index: u64,
175 pub owner: String,
176 pub maker_address: String,
177 pub transaction_hash: String,
178 pub maker_orders: Vec<PolymarketMakerOrder>,
179 pub trader_side: PolymarketLiquiditySide,
180}
181
182#[derive(Clone, Debug, Deserialize, Serialize)]
186#[serde(remote = "Self", rename_all = "camelCase")]
187pub struct GammaMarket {
188 #[serde(skip)]
190 pub raw: String,
191 pub id: String,
193 pub condition_id: String,
195 #[serde(rename = "questionID")]
197 pub question_id: Option<String>,
198 #[serde(default)]
200 pub clob_token_ids: String,
201 #[serde(default)]
203 pub outcomes: String,
204 pub question: String,
206 pub description: Option<String>,
208 pub start_date: Option<String>,
210 pub event_start_time: Option<String>,
212 pub end_date: Option<String>,
214 pub active: Option<bool>,
216 pub closed: Option<bool>,
218 pub closed_time: Option<String>,
220 pub uma_resolution_status: Option<String>,
222 pub uma_resolution_statuses: Option<String>,
224 pub resolution_source: Option<String>,
226 pub crypto_market_config: Option<CryptoMarketConfig>,
228 pub accepting_orders: Option<bool>,
230 pub enable_order_book: Option<bool>,
232 #[serde(
234 default,
235 deserialize_with = "deserialize_optional_decimal_from_json_number",
236 serialize_with = "serialize_optional_decimal_as_json_number"
237 )]
238 pub order_price_min_tick_size: Option<Decimal>,
239 #[serde(
241 default,
242 deserialize_with = "deserialize_optional_decimal_from_json_number",
243 serialize_with = "serialize_optional_decimal_as_json_number"
244 )]
245 pub order_min_size: Option<Decimal>,
246 pub maker_base_fee: Option<i64>,
248 pub taker_base_fee: Option<i64>,
250 #[serde(rename = "slug")]
252 pub market_slug: Option<String>,
253 #[serde(rename = "negRisk")]
255 pub neg_risk: Option<bool>,
256 #[serde(
258 default,
259 deserialize_with = "deserialize_optional_decimal_from_json_number",
260 serialize_with = "serialize_optional_decimal_as_json_number"
261 )]
262 pub liquidity_num: Option<Decimal>,
263 #[serde(
265 default,
266 deserialize_with = "deserialize_optional_decimal_from_json_number",
267 serialize_with = "serialize_optional_decimal_as_json_number"
268 )]
269 pub volume_num: Option<Decimal>,
270 #[serde(rename = "volume24hr")]
272 #[serde(
273 default,
274 deserialize_with = "deserialize_optional_decimal_from_json_number",
275 serialize_with = "serialize_optional_decimal_as_json_number"
276 )]
277 pub volume_24hr: Option<Decimal>,
278 pub outcome_prices: Option<String>,
280 #[serde(
282 default,
283 deserialize_with = "deserialize_optional_decimal_from_json_number",
284 serialize_with = "serialize_optional_decimal_as_json_number"
285 )]
286 pub best_bid: Option<Decimal>,
287 #[serde(
289 default,
290 deserialize_with = "deserialize_optional_decimal_from_json_number",
291 serialize_with = "serialize_optional_decimal_as_json_number"
292 )]
293 pub best_ask: Option<Decimal>,
294 #[serde(
296 default,
297 deserialize_with = "deserialize_optional_decimal_from_json_number",
298 serialize_with = "serialize_optional_decimal_as_json_number"
299 )]
300 pub spread: Option<Decimal>,
301 #[serde(
303 default,
304 deserialize_with = "deserialize_optional_decimal_from_json_number",
305 serialize_with = "serialize_optional_decimal_as_json_number"
306 )]
307 pub last_trade_price: Option<Decimal>,
308 #[serde(
310 default,
311 deserialize_with = "deserialize_optional_decimal_from_json_number",
312 serialize_with = "serialize_optional_decimal_as_json_number"
313 )]
314 pub one_day_price_change: Option<Decimal>,
315 #[serde(
317 default,
318 deserialize_with = "deserialize_optional_decimal_from_json_number",
319 serialize_with = "serialize_optional_decimal_as_json_number"
320 )]
321 pub one_week_price_change: Option<Decimal>,
322 #[serde(rename = "volume1wk")]
324 #[serde(
325 default,
326 deserialize_with = "deserialize_optional_decimal_from_json_number",
327 serialize_with = "serialize_optional_decimal_as_json_number"
328 )]
329 pub volume_1wk: Option<Decimal>,
330 #[serde(rename = "volume1mo")]
332 #[serde(
333 default,
334 deserialize_with = "deserialize_optional_decimal_from_json_number",
335 serialize_with = "serialize_optional_decimal_as_json_number"
336 )]
337 pub volume_1mo: Option<Decimal>,
338 #[serde(rename = "volume1yr")]
340 #[serde(
341 default,
342 deserialize_with = "deserialize_optional_decimal_from_json_number",
343 serialize_with = "serialize_optional_decimal_as_json_number"
344 )]
345 pub volume_1yr: Option<Decimal>,
346 #[serde(
348 default,
349 deserialize_with = "deserialize_optional_decimal_from_json_number",
350 serialize_with = "serialize_optional_decimal_as_json_number"
351 )]
352 pub rewards_min_size: Option<Decimal>,
353 #[serde(
355 default,
356 deserialize_with = "deserialize_optional_decimal_from_json_number",
357 serialize_with = "serialize_optional_decimal_as_json_number"
358 )]
359 pub rewards_max_spread: Option<Decimal>,
360 pub competitive: Option<f64>,
362 pub category: Option<String>,
364 #[serde(rename = "negRiskMarketID")]
366 pub neg_risk_market_id: Option<String>,
367 pub fee_schedule: Option<FeeSchedule>,
369 pub fees_enabled: Option<bool>,
371 pub fee_type: Option<String>,
373 pub tags: Option<Vec<GammaTag>>,
375 pub sports_market_type: Option<String>,
377 #[serde(default, deserialize_with = "deserialize_optional_polymarket_game_id")]
382 pub game_id: Option<String>,
383 #[serde(skip)]
386 pub parent_event: Option<Arc<GammaEvent>>,
387 pub events: Option<Vec<GammaEvent>>,
389}
390
391impl_gamma_response_serde!(GammaMarket);
392
393#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
394#[serde(rename_all = "camelCase")]
395pub struct FeeSchedule {
396 #[serde(
397 serialize_with = "serialize_decimal_as_json_number",
398 deserialize_with = "deserialize_decimal_from_json"
399 )]
400 pub exponent: Decimal,
401 #[serde(
402 serialize_with = "serialize_decimal_as_json_number",
403 deserialize_with = "deserialize_decimal_from_json"
404 )]
405 pub rate: Decimal,
406 pub taker_only: bool,
407 #[serde(
408 serialize_with = "serialize_decimal_as_json_number",
409 deserialize_with = "deserialize_decimal_from_json"
410 )]
411 pub rebate_rate: Decimal,
412}
413
414impl FeeSchedule {
415 pub(crate) fn to_info(&self) -> serde_json::Value {
416 serde_json::json!({
417 "exponent": self.exponent.to_string(),
418 "rate": self.rate.to_string(),
419 "takerOnly": self.taker_only,
420 "rebateRate": self.rebate_rate.to_string(),
421 })
422 }
423}
424
425#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
427#[serde(rename_all = "camelCase")]
428pub struct CryptoMarketConfig {
429 pub id: String,
430 pub asset: String,
431 pub duration: String,
432 pub twap_enabled: bool,
433 #[serde(
434 default,
435 skip_serializing_if = "Option::is_none",
436 deserialize_with = "deserialize_optional_non_null_i64"
437 )]
438 pub twap_lookback_seconds: Option<i64>,
439}
440
441fn deserialize_optional_non_null_i64<'de, D>(deserializer: D) -> Result<Option<i64>, D::Error>
442where
443 D: serde::Deserializer<'de>,
444{
445 i64::deserialize(deserializer).map(Some)
446}
447
448#[derive(Clone, Debug, Deserialize, Serialize)]
454#[serde(remote = "Self", rename_all = "camelCase")]
455pub struct GammaEvent {
456 #[serde(skip)]
458 pub raw: String,
459 pub id: String,
460 pub slug: Option<String>,
461 pub title: Option<String>,
462 pub description: Option<String>,
463 pub start_date: Option<String>,
464 pub end_date: Option<String>,
465 pub active: Option<bool>,
466 pub closed: Option<bool>,
467 pub archived: Option<bool>,
468 #[serde(default)]
469 pub markets: Vec<GammaMarket>,
470 #[serde(
472 default,
473 deserialize_with = "deserialize_optional_decimal_from_json_number",
474 serialize_with = "serialize_optional_decimal_as_json_number"
475 )]
476 pub liquidity: Option<Decimal>,
477 #[serde(
479 default,
480 deserialize_with = "deserialize_optional_decimal_from_json_number",
481 serialize_with = "serialize_optional_decimal_as_json_number"
482 )]
483 pub volume: Option<Decimal>,
484 #[serde(
486 default,
487 deserialize_with = "deserialize_optional_decimal_from_json_number",
488 serialize_with = "serialize_optional_decimal_as_json_number"
489 )]
490 pub open_interest: Option<Decimal>,
491 #[serde(rename = "volume24hr")]
493 #[serde(
494 default,
495 deserialize_with = "deserialize_optional_decimal_from_json_number",
496 serialize_with = "serialize_optional_decimal_as_json_number"
497 )]
498 pub volume_24hr: Option<Decimal>,
499 pub category: Option<String>,
501 pub tags: Option<Vec<GammaTag>>,
503 pub neg_risk: Option<bool>,
505 #[serde(rename = "negRiskMarketID")]
507 pub neg_risk_market_id: Option<String>,
508 pub featured: Option<bool>,
510 #[serde(default, deserialize_with = "deserialize_optional_polymarket_game_id")]
515 pub game_id: Option<String>,
516}
517
518impl_gamma_response_serde!(GammaEvent);
519
520#[derive(Clone, Debug, Deserialize, Serialize)]
522pub struct GammaTag {
523 pub id: String,
525 pub label: Option<String>,
527 pub slug: Option<String>,
529}
530
531#[derive(Clone, Debug, Deserialize, Serialize)]
533pub struct SearchResponse {
534 #[serde(default)]
536 pub markets: Option<Vec<GammaMarket>>,
537 #[serde(default)]
539 pub events: Option<Vec<GammaEvent>>,
540}
541
542#[derive(Clone, Debug, Deserialize)]
546pub struct TickSizeResponse {
547 #[serde(deserialize_with = "deserialize_decimal_from_json_number")]
549 pub minimum_tick_size: Decimal,
550}
551
552#[derive(Clone, Debug, Deserialize)]
556pub struct FeeRateResponse {
557 #[serde(deserialize_with = "deserialize_decimal_from_json")]
559 pub base_fee: Decimal,
560}
561
562impl FeeRateResponse {
563 #[must_use]
565 pub fn to_rate(&self) -> Decimal {
566 self.base_fee / Decimal::from(10_000)
567 }
568}
569
570#[derive(Clone, Debug, Deserialize)]
572pub struct ClobBookLevel {
573 pub price: String,
574 pub size: String,
575}
576
577#[derive(Clone, Debug, Deserialize)]
581pub struct ClobBookResponse {
582 pub bids: Vec<ClobBookLevel>,
583 pub asks: Vec<ClobBookLevel>,
584}
585
586#[derive(Clone, Debug, Deserialize, Serialize)]
588pub struct ClobMarketToken {
589 pub token_id: String,
590 pub outcome: String,
591 #[serde(
592 default,
593 deserialize_with = "deserialize_optional_decimal_from_json_number",
594 serialize_with = "serialize_optional_decimal_as_json_number"
595 )]
596 pub price: Option<Decimal>,
597 pub winner: bool,
598}
599
600#[derive(Clone, Debug, Deserialize, Serialize)]
602pub struct ClobMarketRewardRate {
603 pub asset_address: String,
604 #[serde(
605 deserialize_with = "deserialize_decimal_from_json_number",
606 serialize_with = "serialize_decimal_as_json_number"
607 )]
608 pub rewards_daily_rate: Decimal,
609}
610
611#[derive(Clone, Debug, Deserialize, Serialize)]
613pub struct ClobMarketRewards {
614 pub rates: Option<Vec<ClobMarketRewardRate>>,
615 #[serde(
616 default,
617 deserialize_with = "deserialize_optional_decimal_from_json_number",
618 serialize_with = "serialize_optional_decimal_as_json_number"
619 )]
620 pub min_size: Option<Decimal>,
621 #[serde(
622 default,
623 deserialize_with = "deserialize_optional_decimal_from_json_number",
624 serialize_with = "serialize_optional_decimal_as_json_number"
625 )]
626 pub max_spread: Option<Decimal>,
627}
628
629#[derive(Clone, Debug, Deserialize, Serialize)]
631pub struct ClobMarketResponse {
632 pub enable_order_book: Option<bool>,
633 pub active: Option<bool>,
634 pub condition_id: String,
635 pub closed: bool,
636 pub archived: Option<bool>,
637 pub accepting_orders: Option<bool>,
638 pub accepting_order_timestamp: Option<String>,
639 #[serde(
640 default,
641 deserialize_with = "deserialize_optional_decimal_from_json_number",
642 serialize_with = "serialize_optional_decimal_as_json_number"
643 )]
644 pub minimum_order_size: Option<Decimal>,
645 #[serde(
646 default,
647 deserialize_with = "deserialize_optional_decimal_from_json_number",
648 serialize_with = "serialize_optional_decimal_as_json_number"
649 )]
650 pub minimum_tick_size: Option<Decimal>,
651 pub question_id: Option<String>,
652 pub question: Option<String>,
653 pub description: Option<String>,
654 pub market_slug: Option<String>,
655 pub end_date_iso: Option<String>,
656 pub game_start_time: Option<String>,
657 pub seconds_delay: Option<i64>,
658 pub fpmm: Option<String>,
659 pub maker_base_fee: Option<i64>,
660 pub taker_base_fee: Option<i64>,
661 pub notifications_enabled: Option<bool>,
662 pub neg_risk: Option<bool>,
663 pub neg_risk_market_id: Option<String>,
664 pub neg_risk_request_id: Option<String>,
665 pub icon: Option<String>,
666 pub image: Option<String>,
667 pub rewards: Option<ClobMarketRewards>,
668 pub is_50_50_outcome: Option<bool>,
669 pub tokens: Vec<ClobMarketToken>,
670 pub tags: Option<Vec<String>>,
671}
672
673#[derive(Clone, Debug, Deserialize)]
677pub struct DataApiPosition {
678 #[serde(rename = "token_id")]
679 pub asset: String,
680 pub condition_id: String,
681 #[serde(
682 rename = "current_size",
683 deserialize_with = "deserialize_decimal_from_json"
684 )]
685 pub size: Decimal,
686 #[serde(default, deserialize_with = "deserialize_optional_decimal_from_json")]
687 pub avg_price: Option<Decimal>,
688}
689
690#[derive(Clone, Debug, Deserialize)]
694pub struct DataApiTrade {
695 pub proxy_wallet: Option<String>,
696 #[serde(rename = "token_id")]
697 pub asset: String,
698 pub condition_id: String,
699 pub side: PolymarketOrderSide,
700 #[serde(deserialize_with = "deserialize_decimal_from_json_number")]
701 pub price: Decimal,
702 #[serde(deserialize_with = "deserialize_decimal_from_json_number")]
703 pub size: Decimal,
704 pub timestamp: i64,
705 pub title: Option<String>,
706 pub slug: Option<String>,
707 pub icon: Option<String>,
708 pub event_slug: Option<String>,
709 pub outcome: Option<String>,
710 pub outcome_index: Option<i64>,
711 pub name: Option<String>,
712 pub pseudonym: Option<String>,
713 pub bio: Option<String>,
714 pub profile_image: Option<String>,
715 pub profile_image_optimized: Option<String>,
716 pub transaction_hash: String,
717}
718
719#[derive(Clone, Debug, Deserialize)]
721pub struct DataApiPagination {
722 pub has_more: bool,
724 pub next_cursor: Option<String>,
726}
727
728#[derive(Clone, Debug, Deserialize)]
732#[serde(bound = "T: Deserialize<'de>")]
733pub struct DataApiPage<T> {
734 #[serde(
735 default = "Vec::new",
736 deserialize_with = "deserialize_nullable_vec_as_empty"
737 )]
738 pub data: Vec<T>,
739 pub pagination: DataApiPagination,
740}
741
742fn deserialize_nullable_vec_as_empty<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
743where
744 D: serde::Deserializer<'de>,
745 T: Deserialize<'de>,
746{
747 Ok(Option::<Vec<T>>::deserialize(deserializer)?.unwrap_or_default())
748}
749
750#[cfg(test)]
751mod tests {
752 use rstest::rstest;
753 use rust_decimal_macros::dec;
754
755 use super::*;
756 use crate::common::enums::{
757 PolymarketOrderStatus, PolymarketSignatureType, PolymarketTradeStatus,
758 };
759
760 fn load<T: serde::de::DeserializeOwned>(filename: &str) -> T {
761 let path = format!("test_data/{filename}");
762 let content = std::fs::read_to_string(path).expect("Failed to read test data");
763 serde_json::from_str(&content).expect("Failed to parse test data")
764 }
765
766 #[rstest]
767 #[case::market(include_str!("../../test_data/decimal_precision_market.json"), false)]
768 #[case::event(include_str!("../../test_data/decimal_precision_event.json"), true)]
769 fn test_gamma_financial_fields_round_trip_exactly(#[case] raw: &str, #[case] event: bool) {
770 let encoded = if event {
771 serde_json::to_string(&serde_json::from_str::<GammaEvent>(raw).unwrap()).unwrap()
772 } else {
773 serde_json::to_string(&serde_json::from_str::<GammaMarket>(raw).unwrap()).unwrap()
774 };
775 let expected: std::collections::BTreeMap<String, Box<serde_json::value::RawValue>> =
776 serde_json::from_str(raw).unwrap();
777 let actual: std::collections::BTreeMap<String, Box<serde_json::value::RawValue>> =
778 serde_json::from_str(&encoded).unwrap();
779
780 for (field, value) in expected {
781 if value.get().starts_with(|c: char| c.is_ascii_digit()) {
782 assert_eq!(actual[&field].get(), value.get(), "{field}");
783 }
784 }
785 }
786
787 #[rstest]
788 fn test_data_api_position_preserves_decimal_precision() {
789 let raw = include_str!("../../test_data/decimal_precision_position.json");
790 let position: DataApiPosition = serde_json::from_str(raw).unwrap();
791 assert_eq!(position.asset, "precision-asset");
792 assert_eq!(position.condition_id, "0xprecision");
793 assert_eq!(position.size, dec!(12345678901.123456));
794 assert_eq!(
795 position.avg_price,
796 Some(dec!(0.1234567890123456789012345678))
797 );
798 let strings = raw
799 .replace("12345678901.123456", "\"12345678901.123456\"")
800 .replace(
801 "0.1234567890123456789012345678",
802 "\"0.1234567890123456789012345678\"",
803 );
804 let string_position: DataApiPosition = serde_json::from_str(&strings).unwrap();
805 assert_eq!(string_position.size, position.size);
806 assert_eq!(string_position.avg_price, position.avg_price);
807 }
808
809 #[rstest]
810 fn test_gamma_financial_fields_and_fee_info_preserve_decimal_precision() {
811 let market: GammaMarket = serde_json::from_str(include_str!(
812 "../../test_data/decimal_precision_market.json"
813 ))
814 .unwrap();
815 assert_eq!(market.best_bid, Some(dec!(0.1234567890123456789012345678)));
816 assert_eq!(market.best_ask, Some(dec!(0.2345678901234567890123456789)));
817 assert_eq!(market.liquidity_num, Some(dec!(12345678901.123456)));
818 assert_eq!(market.volume_num, Some(dec!(12345678901.123457)));
819 let fee = market.fee_schedule.unwrap();
820 let info = fee.to_info();
821 assert_eq!(info["exponent"], "1.234567890123456789012345678");
822 assert_eq!(info["rate"], "0.1234567890123456789012345678");
823 assert_eq!(info["rebateRate"], "0.0234567890123456789012345678");
824 assert_eq!(info["takerOnly"], true);
825 let restored: FeeSchedule = serde_json::from_value(info).unwrap();
826 assert_eq!(restored.exponent, fee.exponent);
827 assert_eq!(restored.rate, fee.rate);
828 assert_eq!(restored.rebate_rate, fee.rebate_rate);
829 assert_eq!(restored.taker_only, fee.taker_only);
830 }
831
832 #[rstest]
833 fn test_open_order_live_buy_gtc() {
834 let order: PolymarketOpenOrder = load("http_open_order.json");
835
836 assert_eq!(
837 order.id,
838 "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12"
839 );
840 assert_eq!(order.status, PolymarketOrderStatus::Live);
841 assert_eq!(order.side, PolymarketOrderSide::Buy);
842 assert_eq!(order.order_type, PolymarketOrderType::GTC);
843 assert_eq!(order.outcome, PolymarketOutcome::yes());
844 assert_eq!(order.original_size, dec!(100.0000));
845 assert_eq!(order.price, dec!(0.5000));
846 assert_eq!(order.size_matched, dec!(25.0000));
847 assert_eq!(order.created_at, 1703875200);
848 assert!(order.expiration.is_none());
849 assert_eq!(order.associate_trades, Some(vec!["0xabc001".to_string()]));
850 }
851
852 #[rstest]
853 fn test_open_order_matched_sell_fok() {
854 let order: PolymarketOpenOrder = load("http_open_order_sell_fok.json");
855
856 assert_eq!(order.status, PolymarketOrderStatus::Matched);
857 assert_eq!(order.side, PolymarketOrderSide::Sell);
858 assert_eq!(order.order_type, PolymarketOrderType::FOK);
859 assert_eq!(order.outcome, PolymarketOutcome::no());
860 assert_eq!(order.size_matched, dec!(50.0000));
861 assert_eq!(order.expiration, Some("1735689600".to_string()));
862 assert!(order.associate_trades.is_none());
863 }
864
865 #[rstest]
866 fn test_open_order_roundtrip() {
867 let order: PolymarketOpenOrder = load("http_open_order.json");
868 let json = serde_json::to_string(&order).unwrap();
869 let order2: PolymarketOpenOrder = serde_json::from_str(&json).unwrap();
870 assert_eq!(order, order2);
871 }
872
873 #[rstest]
874 fn test_trade_report_fields() {
875 let trade: PolymarketTradeReport = load("http_trade_report.json");
876
877 assert_eq!(trade.id, "trade-0xabcdef1234");
878 assert_eq!(
879 trade.taker_order_id,
880 "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12"
881 );
882 assert_eq!(trade.side, PolymarketOrderSide::Buy);
883 assert_eq!(trade.size, dec!(25.0000));
884 assert_eq!(trade.fee_rate_bps, dec!(0));
885 assert_eq!(trade.price, dec!(0.5000));
886 assert_eq!(trade.status, PolymarketTradeStatus::Confirmed);
887 assert_eq!(trade.outcome, PolymarketOutcome::yes());
888 assert_eq!(trade.bucket_index, 0);
889 assert_eq!(trade.trader_side, PolymarketLiquiditySide::Taker);
890 assert_eq!(trade.maker_orders.len(), 2);
891 }
892
893 #[rstest]
894 fn test_trade_report_maker_orders() {
895 let trade: PolymarketTradeReport = load("http_trade_report.json");
896
897 let first = &trade.maker_orders[0];
898 assert_eq!(first.matched_amount, dec!(25.0000));
899 assert_eq!(first.price, dec!(0.5000));
900 assert_eq!(first.outcome, PolymarketOutcome::yes());
901
902 let second = &trade.maker_orders[1];
903 assert_eq!(second.matched_amount, dec!(5.0000));
904 }
905
906 #[rstest]
907 fn test_trade_report_roundtrip() {
908 let trade: PolymarketTradeReport = load("http_trade_report.json");
909 let json = serde_json::to_string(&trade).unwrap();
910 let trade2: PolymarketTradeReport = serde_json::from_str(&json).unwrap();
911 assert_eq!(trade, trade2);
912 }
913
914 #[rstest]
915 fn test_signed_order_camel_case_fields() {
916 let order: PolymarketOrder = load("http_signed_order.json");
917 let debug = format!("{order:?}");
918
919 assert_eq!(order.salt, 123456789);
920 assert_eq!(order.maker, "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266");
921 assert_eq!(order.maker_amount, dec!(100000000));
922 assert_eq!(order.taker_amount, dec!(50000000));
923 assert_eq!(order.expiration, "0");
924 assert_eq!(order.timestamp, "1713398400000");
925 assert_eq!(
926 order.metadata,
927 "0x0000000000000000000000000000000000000000000000000000000000000000"
928 );
929 assert_eq!(
930 order.builder,
931 "0x0000000000000000000000000000000000000000000000000000000000000000"
932 );
933 assert_eq!(order.side, PolymarketOrderSide::Buy);
934 assert_eq!(order.signature_type, PolymarketSignatureType::Eoa);
935 assert!(debug.contains(REDACTED));
936 assert!(!debug.contains(order.signature.expose_secret()));
937 }
938
939 #[rstest]
940 fn test_signed_order_roundtrip() {
941 let order: PolymarketOrder = load("http_signed_order.json");
942 let json = serde_json::to_string(&order).unwrap();
943 let order2: PolymarketOrder = serde_json::from_str(&json).unwrap();
944 assert_eq!(order, order2);
945 }
946
947 #[rstest]
948 fn test_signed_order_serializes_camel_case() {
949 let order: PolymarketOrder = load("http_signed_order.json");
950 let json = serde_json::to_string(&order).unwrap();
951
952 assert!(json.contains("\"tokenId\""));
954 assert!(json.contains("\"makerAmount\""));
955 assert!(json.contains("\"takerAmount\""));
956 assert!(json.contains("\"signatureType\""));
957 assert!(json.contains("\"expiration\""));
958 assert!(json.contains("\"timestamp\""));
959 assert!(json.contains("\"metadata\""));
960 assert!(json.contains("\"builder\""));
961 }
962
963 #[rstest]
964 fn test_signed_order_omits_v1_fields() {
965 let order: PolymarketOrder = load("http_signed_order.json");
969 let json = serde_json::to_string(&order).unwrap();
970
971 assert!(
972 !json.contains("\"taker\""),
973 "wire body must not include `taker`: {json}"
974 );
975 assert!(
976 !json.contains("\"nonce\""),
977 "wire body must not include `nonce`: {json}"
978 );
979 assert!(
980 !json.contains("\"feeRateBps\""),
981 "wire body must not include `feeRateBps`: {json}"
982 );
983 }
984
985 #[rstest]
986 fn test_signed_order_v2_docs_example_roundtrips() {
987 let docs_example = r#"{
990 "salt": 12345,
991 "maker": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
992 "signer": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
993 "tokenId": "102936",
994 "makerAmount": "1000000",
995 "takerAmount": "2000000",
996 "side": "BUY",
997 "signatureType": 1,
998 "expiration": "0",
999 "timestamp": "1713398400000",
1000 "metadata": "0x0000000000000000000000000000000000000000000000000000000000000000",
1001 "builder": "0x0000000000000000000000000000000000000000000000000000000000000000",
1002 "signature": "0xdeadbeef"
1003 }"#;
1004
1005 let order: PolymarketOrder = serde_json::from_str(docs_example).unwrap();
1006 assert_eq!(order.salt, 12345);
1007 assert_eq!(order.token_id.as_str(), "102936");
1008 assert_eq!(order.maker_amount, dec!(1000000));
1009 assert_eq!(order.taker_amount, dec!(2000000));
1010 assert_eq!(order.side, PolymarketOrderSide::Buy);
1011 assert_eq!(order.signature_type, PolymarketSignatureType::PolyProxy);
1012 assert_eq!(order.expiration, "0");
1013 assert_eq!(order.timestamp, "1713398400000");
1014
1015 let json = serde_json::to_string(&order).unwrap();
1017 let order2: PolymarketOrder = serde_json::from_str(&json).unwrap();
1018 assert_eq!(order, order2);
1019 }
1020
1021 #[rstest]
1022 fn test_gamma_event_deserialization() {
1023 let events: Vec<GammaEvent> = load("gamma_event.json");
1024
1025 assert_eq!(events.len(), 1);
1026 let event = &events[0];
1027 assert_eq!(event.id, "30829");
1028 assert_eq!(
1029 event.slug.as_deref(),
1030 Some("democratic-presidential-nominee-2028")
1031 );
1032 assert_eq!(
1033 event.title.as_deref(),
1034 Some("Democratic Presidential Nominee 2028")
1035 );
1036 assert_eq!(event.active, Some(true));
1037 assert_eq!(event.closed, Some(false));
1038 assert_eq!(event.archived, Some(false));
1039 assert_eq!(event.markets.len(), 2);
1040 assert_eq!(
1041 event.markets[0].condition_id,
1042 "0xc8f1cf5d4f26e0fd9c8fe89f2a7b3263b902cf14fde7bfccef525753bb492e47"
1043 );
1044 assert_eq!(
1045 event.markets[1].condition_id,
1046 "0xe39adea057926dc197fe30a441f57a340b2a232d5a687010f78bba9b6e02620f"
1047 );
1048 }
1049
1050 #[rstest]
1051 fn test_gamma_event_empty_markets() {
1052 let json = r#"[{"id": "evt-002"}]"#;
1053 let events: Vec<GammaEvent> = serde_json::from_str(json).unwrap();
1054
1055 assert_eq!(events.len(), 1);
1056 assert_eq!(events[0].id, "evt-002");
1057 assert!(events[0].markets.is_empty());
1058 assert!(events[0].slug.is_none());
1059 }
1060
1061 #[rstest]
1062 fn test_sports_market_are_weird() {
1063 let money_line: GammaMarket = load("gamma_market_sports_market_money_line.json");
1064 let map_handicap: GammaMarket = load("gamma_market_sports_market_map_handicap.json");
1065
1066 assert_eq!(
1068 money_line.events.as_ref().unwrap()[0].game_id,
1069 map_handicap.events.as_ref().unwrap()[0].game_id
1070 );
1071
1072 assert!(map_handicap.game_id.is_none());
1074 assert_eq!(money_line.game_id.as_deref(), Some("1427074"));
1075 }
1076
1077 #[rstest]
1078 fn test_gamma_event_composite_sports_game_id() {
1079 let events: Vec<GammaEvent> = load("gamma_event_sports_composite_game_id.json");
1082
1083 assert_eq!(events.len(), 1);
1084
1085 let event = &events[0];
1086
1087 assert_eq!(event.id, "835109");
1088 assert_eq!(event.game_id.as_deref(), Some("287011684"));
1089 assert_eq!(event.markets.len(), 2);
1090 assert_eq!(event.markets[0].id, "3524358");
1091 assert_eq!(
1092 event.markets[0].game_id.as_deref(),
1093 Some("dd80aae9-52f9-4c7b-a1cf-7b4ab63cd281:STL:TEX")
1094 );
1095 assert_eq!(event.markets[1].id, "3554041");
1096 assert_eq!(event.markets[1].game_id, None);
1097
1098 let encoded = serde_json::to_value(event).unwrap();
1101
1102 assert_eq!(encoded["gameId"], serde_json::json!("287011684"));
1103 assert_eq!(
1104 encoded["markets"][0]["gameId"],
1105 serde_json::json!("dd80aae9-52f9-4c7b-a1cf-7b4ab63cd281:STL:TEX")
1106 );
1107 }
1108
1109 #[rstest]
1110 fn test_fee_schedule_decimal_fields() {
1111 let market: GammaMarket = load("gamma_market_sports_market_money_line.json");
1112 let schedule = market.fee_schedule.unwrap();
1113
1114 assert_eq!(schedule.exponent, Decimal::ONE);
1115 assert_eq!(schedule.rate, dec!(0.03));
1116 assert!(schedule.taker_only);
1117 assert_eq!(schedule.rebate_rate, dec!(0.25));
1118 }
1119
1120 #[rstest]
1121 fn test_gamma_market_crypto_market_config_fields() {
1122 let market: GammaMarket = load("gamma_market_crypto_twap.json");
1123 let config = market.crypto_market_config.as_ref().unwrap();
1124
1125 assert_eq!(config.id, "btc-5m-twap-60");
1126 assert_eq!(config.asset, "btc");
1127 assert_eq!(config.duration, "5m");
1128 assert!(config.twap_enabled);
1129 assert_eq!(config.twap_lookback_seconds, Some(60));
1130 assert_eq!(
1131 market.resolution_source.as_deref(),
1132 Some("https://data.chain.link/streams/btc-usd-twap-60s-streams")
1133 );
1134 assert_eq!(
1135 market.event_start_time.as_deref(),
1136 Some("2026-08-22T16:00:00Z")
1137 );
1138 }
1139
1140 #[rstest]
1141 fn test_crypto_market_config_absent_twap_lookback_serializes_omitted() {
1142 let crypto_market_config = serde_json::json!({
1143 "id": "btc-5m",
1144 "asset": "btc",
1145 "duration": "5m",
1146 "twapEnabled": false,
1147 });
1148
1149 let config: CryptoMarketConfig = serde_json::from_value(crypto_market_config).unwrap();
1150 let encoded = serde_json::to_value(config).unwrap();
1151
1152 assert!(encoded.get("twapLookbackSeconds").is_none());
1153 }
1154
1155 #[rstest]
1156 fn test_crypto_market_config_rejects_null_twap_lookback() {
1157 let crypto_market_config = serde_json::json!({
1158 "id": "btc-5m",
1159 "asset": "btc",
1160 "duration": "5m",
1161 "twapEnabled": false,
1162 "twapLookbackSeconds": null,
1163 });
1164
1165 let result = serde_json::from_value::<CryptoMarketConfig>(crypto_market_config);
1166
1167 assert!(result.is_err());
1168 }
1169
1170 #[rstest]
1171 #[case(serde_json::json!(-37))]
1172 #[case(serde_json::json!(i64::MIN))]
1173 #[case(serde_json::json!(i64::MAX))]
1174 fn test_crypto_market_config_signed_twap_lookback_roundtrip(
1175 #[case] twap_lookback_seconds: serde_json::Value,
1176 ) {
1177 let crypto_market_config = serde_json::json!({
1178 "id": "eth-15m",
1179 "asset": "eth",
1180 "duration": "15m",
1181 "twapEnabled": true,
1182 "twapLookbackSeconds": twap_lookback_seconds,
1183 });
1184
1185 let config: CryptoMarketConfig = serde_json::from_value(crypto_market_config).unwrap();
1186 let encoded = serde_json::to_value(config).unwrap();
1187
1188 assert_eq!(encoded["twapLookbackSeconds"], twap_lookback_seconds);
1189 }
1190
1191 #[rstest]
1192 fn test_crypto_market_config_rejects_twap_lookback_above_i64_max() {
1193 let crypto_market_config = serde_json::json!({
1194 "id": "eth-15m",
1195 "asset": "eth",
1196 "duration": "15m",
1197 "twapEnabled": true,
1198 "twapLookbackSeconds": 9_223_372_036_854_775_808u64,
1199 });
1200
1201 let result = serde_json::from_value::<CryptoMarketConfig>(crypto_market_config);
1202
1203 assert!(result.is_err());
1204 }
1205
1206 #[rstest]
1207 fn test_gamma_market_enriched_fields() {
1208 let market: GammaMarket = load("gamma_market.json");
1209
1210 assert_eq!(
1211 market.event_start_time.as_deref(),
1212 Some("2026-03-12T09:20:00Z")
1213 );
1214 assert_eq!(market.best_bid, Some(dec!(0.5)));
1215 assert_eq!(market.best_ask, Some(dec!(0.51)));
1216 assert_eq!(market.spread, Some(dec!(0.009)));
1217 assert_eq!(market.last_trade_price, Some(dec!(0.51)));
1218 assert!(market.one_day_price_change.is_none());
1219 assert!(market.one_week_price_change.is_none());
1220 assert_eq!(market.volume_1wk, Some(dec!(9.999997)));
1221 assert_eq!(market.volume_1mo, Some(dec!(9.999997)));
1222 assert_eq!(market.volume_1yr, Some(dec!(9.999997)));
1223 assert_eq!(market.rewards_min_size, Some(dec!(50.0)));
1224 assert_eq!(market.rewards_max_spread, Some(dec!(4.5)));
1225 assert_eq!(market.competitive, Some(0.9999750006249843));
1226 assert!(market.category.is_none());
1227 assert!(market.neg_risk_market_id.is_none());
1228 assert!(market.uma_resolution_status.is_none());
1229 assert_eq!(market.uma_resolution_statuses.as_deref(), Some("[]"));
1230 assert_eq!(
1231 market.outcome_prices.as_deref(),
1232 Some("[\"0.505\", \"0.495\"]")
1233 );
1234 }
1235
1236 #[rstest]
1237 fn test_gamma_market_uma_resolution_statuses() {
1238 let market: GammaMarket = load("gamma_market.json");
1239
1240 assert!(market.uma_resolution_status.is_none());
1241 assert_eq!(market.uma_resolution_statuses.as_deref(), Some("[]"));
1242 }
1243
1244 #[rstest]
1245 fn test_gamma_market_enriched_fields_default_to_none() {
1246 let json = r#"{"id": "m1", "conditionId": "0xcond", "clobTokenIds": "[]", "outcomes": "[]", "question": "Q?"}"#;
1248 let market: GammaMarket = serde_json::from_str(json).unwrap();
1249
1250 assert!(market.best_bid.is_none());
1251 assert!(market.spread.is_none());
1252 assert!(market.volume_1wk.is_none());
1253 assert!(market.rewards_min_size.is_none());
1254 assert!(market.competitive.is_none());
1255 assert!(market.category.is_none());
1256 assert!(market.neg_risk_market_id.is_none());
1257 assert!(market.crypto_market_config.is_none());
1258 assert!(market.event_start_time.is_none());
1259 }
1260
1261 #[rstest]
1262 fn test_gamma_event_enriched_fields() {
1263 let events: Vec<GammaEvent> = load("gamma_event.json");
1264 let event = &events[0];
1265
1266 assert_eq!(event.liquidity, Some(dec!(43042905.16152)));
1267 assert_eq!(event.volume, Some(dec!(799823812.487094)));
1268 assert_eq!(event.open_interest, Some(dec!(0.0)));
1269 assert_eq!(event.volume_24hr, Some(dec!(5669354.219446001)));
1270 assert!(event.category.is_none());
1271 assert_eq!(event.neg_risk, Some(true));
1272 assert_eq!(
1273 event.neg_risk_market_id.as_deref(),
1274 Some("0x2c3d7e0eee6f058be3006baabf0d54a07da254ba47fe6e3e095e7990c7814700")
1275 );
1276 assert_eq!(event.featured, Some(false));
1277 }
1278
1279 #[rstest]
1280 fn test_gamma_tag_deserialization() {
1281 let tags: Vec<GammaTag> = load("gamma_tags.json");
1282
1283 assert_eq!(tags.len(), 5);
1284 assert_eq!(tags[0].id, "101259");
1285 assert_eq!(tags[0].label.as_deref(), Some("Health and Human Services"));
1286 assert_eq!(tags[0].slug.as_deref(), Some("health-and-human-services"));
1287 assert_eq!(tags[2].slug.as_deref(), Some("attorney-general"));
1288 }
1289
1290 #[rstest]
1291 fn test_search_response_deserialization() {
1292 let response: SearchResponse = load("search_response.json");
1293
1294 assert!(response.markets.is_none());
1296
1297 let events = response.events.as_ref().unwrap();
1298 assert_eq!(events.len(), 1);
1299 assert_eq!(events[0].slug.as_deref(), Some("bitcoin-above-on-march-11"));
1300 assert_eq!(events[0].markets.len(), 1);
1301 }
1302
1303 #[rstest]
1304 fn test_search_response_empty_fields() {
1305 let json = "{}";
1306 let response: SearchResponse = serde_json::from_str(json).unwrap();
1307 assert!(response.markets.is_none());
1308 assert!(response.events.is_none());
1309 }
1310
1311 #[rstest]
1312 fn test_clob_book_response_deserialization() {
1313 let response: ClobBookResponse = load("clob_book_response.json");
1314
1315 assert_eq!(response.bids.len(), 3);
1316 assert_eq!(response.asks.len(), 3);
1317
1318 assert_eq!(response.bids[0].price, "0.48");
1319 assert_eq!(response.bids[0].size, "100.00");
1320 assert_eq!(response.bids[2].price, "0.50");
1321 assert_eq!(response.bids[2].size, "150.00");
1322
1323 assert_eq!(response.asks[0].price, "0.51");
1324 assert_eq!(response.asks[0].size, "120.00");
1325 assert_eq!(response.asks[2].price, "0.53");
1326 assert_eq!(response.asks[2].size, "90.00");
1327 }
1328
1329 #[rstest]
1330 fn test_clob_book_response_ignores_extra_fields() {
1331 let json = r#"{
1337 "market": "0xabc",
1338 "asset_id": "123",
1339 "hash": "0x1",
1340 "timestamp": "123",
1341 "bids": [],
1342 "asks": [],
1343 "tick_size": "0.01",
1344 "min_order_size": "5",
1345 "neg_risk": false,
1346 "last_trade_price": "0.55"
1347 }"#;
1348 let response: ClobBookResponse = serde_json::from_str(json).unwrap();
1349 assert!(response.bids.is_empty());
1350 assert!(response.asks.is_empty());
1351 }
1352
1353 #[rstest]
1354 fn test_clob_market_response_captured_fields() {
1355 let response: ClobMarketResponse = load("clob_market_response.json");
1356 let raw: serde_json::Value = load("clob_market_response.json");
1357
1358 assert_eq!(response.enable_order_book, Some(true));
1359 assert_eq!(response.active, Some(true));
1360 assert!(!response.closed);
1361 assert_eq!(response.archived, Some(false));
1362 assert_eq!(response.accepting_orders, Some(true));
1363 assert_eq!(
1364 response.accepting_order_timestamp.as_deref(),
1365 Some("2026-08-01T22:56:49Z")
1366 );
1367 assert_eq!(response.minimum_order_size, Some(dec!(5)));
1368 assert_eq!(response.minimum_tick_size, Some(dec!(0.01)));
1369 assert_eq!(
1370 response.condition_id,
1371 "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
1372 );
1373 assert_eq!(
1374 response.question_id.as_deref(),
1375 Some("0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd")
1376 );
1377 assert_eq!(
1378 response.question.as_deref(),
1379 Some("LoL: T1 vs Hanwha Life Esports (BO3) - LCK Round 3-4 Legend Group")
1380 );
1381 assert_eq!(response.description.as_deref(), raw["description"].as_str());
1382 assert_eq!(
1383 response.market_slug.as_deref(),
1384 Some("sanitized-clob-market")
1385 );
1386 assert_eq!(
1387 response.end_date_iso.as_deref(),
1388 Some("2026-08-08T00:00:00Z")
1389 );
1390 assert_eq!(
1391 response.game_start_time.as_deref(),
1392 Some("2026-08-08T08:00:00Z")
1393 );
1394 assert_eq!(response.seconds_delay, Some(1));
1395 assert_eq!(response.fpmm.as_deref(), Some(""));
1396 assert_eq!(response.maker_base_fee, Some(1000));
1397 assert_eq!(response.taker_base_fee, Some(1000));
1398 assert_eq!(response.notifications_enabled, Some(true));
1399 assert_eq!(response.neg_risk, Some(false));
1400 assert_eq!(response.neg_risk_market_id.as_deref(), Some(""));
1401 assert_eq!(response.neg_risk_request_id.as_deref(), Some(""));
1402 assert_eq!(
1403 response.icon.as_deref(),
1404 Some("https://example.com/sanitized-market.png")
1405 );
1406 assert_eq!(
1407 response.image.as_deref(),
1408 Some("https://example.com/sanitized-market.png")
1409 );
1410 let rewards = response.rewards.as_ref().expect("captured rewards");
1411 assert!(rewards.rates.is_none());
1412 assert_eq!(rewards.min_size, Some(dec!(50)));
1413 assert_eq!(rewards.max_spread, Some(dec!(4.5)));
1414 assert_eq!(response.is_50_50_outcome, Some(false));
1415 assert_eq!(response.tokens.len(), 2);
1416 assert_eq!(
1417 response.tokens[0].token_id,
1418 "10000000000000000000000000000000000000000000000000000000000000000000000000001"
1419 );
1420 assert_eq!(response.tokens[0].outcome, "T1");
1421 assert_eq!(response.tokens[0].price, Some(dec!(0.715)));
1422 assert!(!response.tokens[0].winner);
1423 assert_eq!(
1424 response.tokens[1].token_id,
1425 "10000000000000000000000000000000000000000000000000000000000000000000000000002"
1426 );
1427 assert_eq!(response.tokens[1].outcome, "Hanwha Life Esports");
1428 assert_eq!(response.tokens[1].price, Some(dec!(0.285)));
1429 assert!(!response.tokens[1].winner);
1430 assert_eq!(
1431 response.tags.as_deref(),
1432 Some(
1433 &[
1434 "Sports".to_string(),
1435 "Esports".to_string(),
1436 "league of legends".to_string(),
1437 "Games".to_string(),
1438 ][..]
1439 )
1440 );
1441 }
1442
1443 #[rstest]
1444 fn test_clob_market_rewards_documented_rate_fields() {
1445 let json = r#"{
1447 "rates":[{"asset_address":"0x1111111111111111111111111111111111111111","rewards_daily_rate":12.5}],
1448 "min_size":25,
1449 "max_spread":3.5
1450 }"#;
1451 let rewards: ClobMarketRewards = serde_json::from_str(json).unwrap();
1452
1453 let rates = rewards.rates.as_deref().expect("documented reward rate");
1454 assert_eq!(rates.len(), 1);
1455 assert_eq!(
1456 rates[0].asset_address,
1457 "0x1111111111111111111111111111111111111111"
1458 );
1459 assert_eq!(rates[0].rewards_daily_rate, dec!(12.5));
1460 assert_eq!(rewards.min_size, Some(dec!(25)));
1461 assert_eq!(rewards.max_spread, Some(dec!(3.5)));
1462 }
1463
1464 #[rstest]
1465 fn test_clob_market_decimal_fields_preserve_precision() {
1466 let json = r#"{
1467 "condition_id":"0xcondition",
1468 "closed":false,
1469 "minimum_order_size":123456789.1234567890123456789,
1470 "minimum_tick_size":0.1234567890123456789012345678,
1471 "rewards":{
1472 "rates":[{
1473 "asset_address":"0x1111111111111111111111111111111111111111",
1474 "rewards_daily_rate":0.1234567890123456789012345678
1475 }],
1476 "min_size":123456789.1234567890123456789,
1477 "max_spread":0.1234567890123456789012345678
1478 },
1479 "tokens":[{
1480 "token_id":"token-1",
1481 "outcome":"Yes",
1482 "price":0.1234567890123456789012345678,
1483 "winner":false
1484 }]
1485 }"#;
1486 let market: ClobMarketResponse = serde_json::from_str(json).unwrap();
1487 let precise = Decimal::from_str_exact("0.1234567890123456789012345678").unwrap();
1488 let large = Decimal::from_str_exact("123456789.1234567890123456789").unwrap();
1489
1490 assert_eq!(market.minimum_order_size, Some(large));
1491 assert_eq!(market.minimum_tick_size, Some(precise));
1492 let rewards = market.rewards.as_ref().unwrap();
1493 assert_eq!(
1494 rewards.rates.as_ref().unwrap()[0].rewards_daily_rate,
1495 precise
1496 );
1497 assert_eq!(rewards.min_size, Some(large));
1498 assert_eq!(rewards.max_spread, Some(precise));
1499 assert_eq!(market.tokens[0].price, Some(precise));
1500 let serialized = serde_json::to_string(&market).unwrap();
1501 assert!(serialized.contains("\"minimum_order_size\":123456789.1234567890123456789"));
1502 assert!(serialized.contains("\"minimum_tick_size\":0.1234567890123456789012345678"));
1503 assert!(serialized.contains("\"rewards_daily_rate\":0.1234567890123456789012345678"));
1504 assert!(serialized.contains("\"min_size\":123456789.1234567890123456789"));
1505 assert!(serialized.contains("\"max_spread\":0.1234567890123456789012345678"));
1506 assert!(serialized.contains("\"price\":0.1234567890123456789012345678"));
1507 }
1508
1509 #[rstest]
1510 fn test_clob_market_response_deserialization_accepting_false() {
1511 let response: ClobMarketResponse = load("clob_market_closed_binary_accepting_false.json");
1512 assert_eq!(
1513 response.condition_id,
1514 "0x8ccc3f4951ff02c1d34b87988752b4444ad17228732780a6cf22afefe8478bb6"
1515 );
1516 assert!(response.closed);
1517 assert_eq!(response.tokens.len(), 2);
1518 assert_eq!(response.tokens[0].outcome, "Yes");
1519 assert!(!response.tokens[0].winner);
1520 assert_eq!(response.tokens[1].outcome, "No");
1521 assert!(response.tokens[1].winner);
1522 }
1523
1524 #[rstest]
1525 fn test_clob_market_response_deserialization_accepting_true() {
1526 let response: ClobMarketResponse = load("clob_market_closed_binary_accepting_true.json");
1527 assert_eq!(
1528 response.condition_id,
1529 "0xd57eed0d44f5b8ca54925d8d6ff440b146b3e6e071da18136ee3ee572d34479e"
1530 );
1531 assert!(response.closed);
1532 assert_eq!(response.tokens.len(), 2);
1533 assert_eq!(response.tokens[0].outcome, "Yes");
1534 assert!(response.tokens[0].winner);
1535 assert_eq!(response.tokens[1].outcome, "No");
1536 assert!(!response.tokens[1].winner);
1537 }
1538
1539 #[rstest]
1540 fn test_tick_size_response_preserves_json_number() {
1541 let response: TickSizeResponse =
1542 serde_json::from_str(r#"{"minimum_tick_size":0.1234567890123456789012345678}"#)
1543 .unwrap();
1544 let precise =
1545 rust_decimal::Decimal::from_str_exact("0.1234567890123456789012345678").unwrap();
1546
1547 assert_eq!(response.minimum_tick_size, precise);
1548 }
1549
1550 #[rstest]
1551 fn test_fee_rate_response_zero() {
1552 let response: FeeRateResponse = load("clob_fee_rate_response_zero.json");
1553 assert_eq!(response.base_fee, dec!(0));
1554 }
1555
1556 #[rstest]
1557 fn test_fee_rate_response_nonzero() {
1558 let response: FeeRateResponse = load("clob_fee_rate_response_nonzero.json");
1559 assert_eq!(response.base_fee, dec!(150));
1560 }
1561
1562 #[rstest]
1563 fn test_data_api_position_deserialization() {
1564 let positions: Vec<DataApiPosition> =
1565 load::<DataApiPage<DataApiPosition>>("data_api_positions_response.json").data;
1566
1567 assert_eq!(positions.len(), 4);
1568 assert_eq!(
1569 positions[0].asset,
1570 "71321045863084981365469005770620412523470745398083994982746259498689308907982"
1571 );
1572 assert_eq!(
1573 positions[0].condition_id,
1574 "0xc8f1cf5d4f26e0fd9c8fe89f2a7b3263b902cf14fde7bfccef525753bb492e47"
1575 );
1576 assert_eq!(positions[0].size, dec!(150.5));
1577 assert_eq!(positions[0].avg_price, Some(dec!(0.55)));
1578
1579 assert_eq!(positions[1].size, dec!(0));
1581 assert_eq!(positions[1].avg_price, Some(dec!(0.45)));
1582
1583 assert_eq!(
1585 positions[2].condition_id,
1586 "0xabc123def456789012345678901234567890abcdef1234567890abcdef123456"
1587 );
1588 assert_eq!(positions[2].size, dec!(42));
1589 assert_eq!(positions[2].avg_price, Some(dec!(0.3)));
1590
1591 assert_eq!(positions[3].size, dec!(0.005));
1593 assert_eq!(positions[3].avg_price, Some(dec!(0.7)));
1594 }
1595
1596 #[rstest]
1597 fn test_data_api_page_deserializes_null_and_missing_data_as_empty() {
1598 let null_data = r#"{
1600 "data": null,
1601 "pagination": {"limit": 500, "offset": 0, "has_more": false, "next_cursor": null}
1602 }"#;
1603 let page: DataApiPage<DataApiTrade> = serde_json::from_str(null_data).unwrap();
1604 assert!(page.data.is_empty());
1605 assert!(!page.pagination.has_more);
1606 assert!(page.pagination.next_cursor.is_none());
1607
1608 let missing_data = r#"{
1609 "pagination": {"limit": 500, "offset": 0, "has_more": false, "next_cursor": null}
1610 }"#;
1611 let page: DataApiPage<DataApiTrade> = serde_json::from_str(missing_data).unwrap();
1612 assert!(page.data.is_empty());
1613 }
1614
1615 #[rstest]
1616 fn test_data_api_trade_deserialization() {
1617 let trades: Vec<DataApiTrade> =
1618 load::<DataApiPage<DataApiTrade>>("data_api_trades_captured_response.json").data;
1619
1620 assert_eq!(trades.len(), 3);
1621 assert_eq!(
1622 trades[0].asset,
1623 "10000000000000000000000000000000000000000000000000000000000000000000000000001"
1624 );
1625 assert_eq!(
1626 trades[0].condition_id,
1627 "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
1628 );
1629 assert_eq!(trades[0].side, PolymarketOrderSide::Sell);
1630 assert_eq!(trades[0].price, dec!(0.7));
1631 assert_eq!(trades[0].size, dec!(92.59));
1632 assert_eq!(trades[0].timestamp, 1786179735);
1633 assert_eq!(
1634 trades[0].transaction_hash,
1635 "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
1636 );
1637
1638 assert_eq!(trades[1].asset, trades[0].asset);
1639 assert_eq!(trades[1].condition_id, trades[0].condition_id);
1640 assert_eq!(trades[1].side, PolymarketOrderSide::Buy);
1641 assert_eq!(trades[1].price, dec!(0.709999959));
1642 assert_eq!(trades[1].size, dec!(1.464786));
1643 assert_eq!(trades[1].timestamp, 1786179730);
1644 assert_eq!(
1645 trades[1].transaction_hash,
1646 "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
1647 );
1648 assert_eq!(
1649 trades[2].asset,
1650 "10000000000000000000000000000000000000000000000000000000000000000000000000002"
1651 );
1652 assert_eq!(trades[2].condition_id, trades[0].condition_id);
1653 assert_eq!(trades[2].side, PolymarketOrderSide::Buy);
1654 assert_eq!(trades[2].price, dec!(0.2972581967));
1655 assert_eq!(trades[2].size, dec!(244));
1656 assert_eq!(trades[2].timestamp, 1786179726);
1657 assert_eq!(
1658 trades[2].transaction_hash,
1659 "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
1660 );
1661
1662 for (trade, outcome, outcome_index) in [
1663 (&trades[0], "T1", 0),
1664 (&trades[1], "T1", 0),
1665 (&trades[2], "Hanwha Life Esports", 1),
1666 ] {
1667 assert_eq!(
1668 trade.proxy_wallet.as_deref(),
1669 Some("0x1111111111111111111111111111111111111111")
1670 );
1671 assert_eq!(
1672 trade.title.as_deref(),
1673 Some("LoL: T1 vs Hanwha Life Esports (BO3) - LCK Round 3-4 Legend Group")
1674 );
1675 assert_eq!(trade.slug.as_deref(), Some("sanitized-market"));
1676 assert_eq!(
1677 trade.icon.as_deref(),
1678 Some("https://example.com/sanitized-market.png")
1679 );
1680 assert_eq!(trade.event_slug.as_deref(), Some("sanitized-event"));
1681 assert_eq!(trade.outcome.as_deref(), Some(outcome));
1682 assert_eq!(trade.outcome_index, Some(outcome_index));
1683 assert_eq!(trade.name.as_deref(), Some("Sanitized trader"));
1684 assert_eq!(trade.pseudonym.as_deref(), Some("sanitized-trader"));
1685 assert_eq!(trade.bio.as_deref(), Some("Sanitized profile"));
1686 assert_eq!(
1687 trade.profile_image.as_deref(),
1688 Some("https://example.com/sanitized-profile.png")
1689 );
1690 assert_eq!(
1691 trade.profile_image_optimized.as_deref(),
1692 Some("https://example.com/sanitized-profile-optimized.png")
1693 );
1694 }
1695 }
1696
1697 #[rstest]
1698 fn test_data_api_trade_decimal_fields_preserve_precision() {
1699 let json = r#"{
1700 "token_id":"token-1",
1701 "condition_id":"0xcondition",
1702 "side":"BUY",
1703 "price":0.1234567890123456789012345678,
1704 "size":123456789.1234567890123456789,
1705 "timestamp":1786179735,
1706 "transaction_hash":"0xtransaction"
1707 }"#;
1708 let trade: DataApiTrade = serde_json::from_str(json).unwrap();
1709
1710 assert_eq!(
1711 trade.price,
1712 Decimal::from_str_exact("0.1234567890123456789012345678").unwrap()
1713 );
1714 assert_eq!(
1715 trade.size,
1716 Decimal::from_str_exact("123456789.1234567890123456789").unwrap()
1717 );
1718 }
1719
1720 #[rstest]
1721 fn test_gamma_market_fee_fields() {
1722 let market: GammaMarket = load("gamma_market.json");
1723
1724 assert_eq!(market.fees_enabled, Some(true));
1725 assert_eq!(market.fee_type.as_deref(), Some("crypto_fees"));
1726 assert!(market.tags.is_none());
1727 assert!(market.sports_market_type.is_none());
1728 }
1729
1730 #[rstest]
1731 fn test_gamma_market_sports_fee_fields() {
1732 let market: GammaMarket = load("gamma_market_sports_market_money_line.json");
1733
1734 assert_eq!(market.fees_enabled, Some(true));
1735 assert_eq!(market.fee_type.as_deref(), Some("sports_fees_v2"));
1736 }
1737
1738 #[rstest]
1739 fn test_gamma_event_tags() {
1740 let events: Vec<GammaEvent> = load("gamma_event_sports_composite_game_id.json");
1741 let tags = events[0].tags.as_ref().unwrap();
1742
1743 assert!(tags.iter().any(|tag| tag.slug.as_deref() == Some("sports")));
1744 }
1745
1746 #[rstest]
1747 fn test_fee_rate_response_to_rate() {
1748 let zero: FeeRateResponse = load("clob_fee_rate_response_zero.json");
1749 let nonzero: FeeRateResponse = load("clob_fee_rate_response_nonzero.json");
1750 let numeric: FeeRateResponse = serde_json::from_str(r#"{"base_fee":700}"#).unwrap();
1751
1752 assert_eq!(zero.to_rate(), Decimal::ZERO);
1753 assert_eq!(nonzero.to_rate(), dec!(0.015));
1754 assert_eq!(numeric.to_rate(), dec!(0.07));
1755 }
1756}