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 #[serde(default)]
689 pub redeemable: bool,
690}
691
692#[derive(Clone, Debug, Deserialize)]
696pub struct DataApiTrade {
697 pub proxy_wallet: Option<String>,
698 #[serde(rename = "token_id")]
699 pub asset: String,
700 pub condition_id: String,
701 pub side: PolymarketOrderSide,
702 #[serde(deserialize_with = "deserialize_decimal_from_json_number")]
703 pub price: Decimal,
704 #[serde(deserialize_with = "deserialize_decimal_from_json_number")]
705 pub size: Decimal,
706 pub timestamp: i64,
707 pub title: Option<String>,
708 pub slug: Option<String>,
709 pub icon: Option<String>,
710 pub event_slug: Option<String>,
711 pub outcome: Option<String>,
712 pub outcome_index: Option<i64>,
713 pub name: Option<String>,
714 pub pseudonym: Option<String>,
715 pub bio: Option<String>,
716 pub profile_image: Option<String>,
717 pub profile_image_optimized: Option<String>,
718 pub transaction_hash: String,
719}
720
721#[derive(Clone, Debug, Deserialize)]
723pub struct DataApiPagination {
724 pub has_more: bool,
726 pub next_cursor: Option<String>,
728}
729
730#[derive(Clone, Debug, Deserialize)]
734#[serde(bound = "T: Deserialize<'de>")]
735pub struct DataApiPage<T> {
736 #[serde(
737 default = "Vec::new",
738 deserialize_with = "deserialize_nullable_vec_as_empty"
739 )]
740 pub data: Vec<T>,
741 pub pagination: DataApiPagination,
742}
743
744fn deserialize_nullable_vec_as_empty<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
745where
746 D: serde::Deserializer<'de>,
747 T: Deserialize<'de>,
748{
749 Ok(Option::<Vec<T>>::deserialize(deserializer)?.unwrap_or_default())
750}
751
752#[cfg(test)]
753mod tests {
754 use rstest::rstest;
755 use rust_decimal_macros::dec;
756
757 use super::*;
758 use crate::common::enums::{
759 PolymarketOrderStatus, PolymarketSignatureType, PolymarketTradeStatus,
760 };
761
762 fn load<T: serde::de::DeserializeOwned>(filename: &str) -> T {
763 let path = format!("test_data/{filename}");
764 let content = std::fs::read_to_string(path).expect("Failed to read test data");
765 serde_json::from_str(&content).expect("Failed to parse test data")
766 }
767
768 #[rstest]
769 #[case::market(include_str!("../../test_data/decimal_precision_market.json"), false)]
770 #[case::event(include_str!("../../test_data/decimal_precision_event.json"), true)]
771 fn test_gamma_financial_fields_round_trip_exactly(#[case] raw: &str, #[case] event: bool) {
772 let encoded = if event {
773 serde_json::to_string(&serde_json::from_str::<GammaEvent>(raw).unwrap()).unwrap()
774 } else {
775 serde_json::to_string(&serde_json::from_str::<GammaMarket>(raw).unwrap()).unwrap()
776 };
777 let expected: std::collections::BTreeMap<String, Box<serde_json::value::RawValue>> =
778 serde_json::from_str(raw).unwrap();
779 let actual: std::collections::BTreeMap<String, Box<serde_json::value::RawValue>> =
780 serde_json::from_str(&encoded).unwrap();
781
782 for (field, value) in expected {
783 if value.get().starts_with(|c: char| c.is_ascii_digit()) {
784 assert_eq!(actual[&field].get(), value.get(), "{field}");
785 }
786 }
787 }
788
789 #[rstest]
790 fn test_data_api_position_preserves_decimal_precision() {
791 let raw = include_str!("../../test_data/decimal_precision_position.json");
792 let position: DataApiPosition = serde_json::from_str(raw).unwrap();
793 assert_eq!(position.asset, "precision-asset");
794 assert_eq!(position.condition_id, "0xprecision");
795 assert_eq!(position.size, dec!(12345678901.123456));
796 assert_eq!(
797 position.avg_price,
798 Some(dec!(0.1234567890123456789012345678))
799 );
800 let strings = raw
801 .replace("12345678901.123456", "\"12345678901.123456\"")
802 .replace(
803 "0.1234567890123456789012345678",
804 "\"0.1234567890123456789012345678\"",
805 );
806 let string_position: DataApiPosition = serde_json::from_str(&strings).unwrap();
807 assert_eq!(string_position.size, position.size);
808 assert_eq!(string_position.avg_price, position.avg_price);
809 }
810
811 #[rstest]
812 fn test_gamma_financial_fields_and_fee_info_preserve_decimal_precision() {
813 let market: GammaMarket = serde_json::from_str(include_str!(
814 "../../test_data/decimal_precision_market.json"
815 ))
816 .unwrap();
817 assert_eq!(market.best_bid, Some(dec!(0.1234567890123456789012345678)));
818 assert_eq!(market.best_ask, Some(dec!(0.2345678901234567890123456789)));
819 assert_eq!(market.liquidity_num, Some(dec!(12345678901.123456)));
820 assert_eq!(market.volume_num, Some(dec!(12345678901.123457)));
821 let fee = market.fee_schedule.unwrap();
822 let info = fee.to_info();
823 assert_eq!(info["exponent"], "1.234567890123456789012345678");
824 assert_eq!(info["rate"], "0.1234567890123456789012345678");
825 assert_eq!(info["rebateRate"], "0.0234567890123456789012345678");
826 assert_eq!(info["takerOnly"], true);
827 let restored: FeeSchedule = serde_json::from_value(info).unwrap();
828 assert_eq!(restored.exponent, fee.exponent);
829 assert_eq!(restored.rate, fee.rate);
830 assert_eq!(restored.rebate_rate, fee.rebate_rate);
831 assert_eq!(restored.taker_only, fee.taker_only);
832 }
833
834 #[rstest]
835 fn test_open_order_live_buy_gtc() {
836 let order: PolymarketOpenOrder = load("http_open_order.json");
837
838 assert_eq!(
839 order.id,
840 "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12"
841 );
842 assert_eq!(order.status, PolymarketOrderStatus::Live);
843 assert_eq!(order.side, PolymarketOrderSide::Buy);
844 assert_eq!(order.order_type, PolymarketOrderType::GTC);
845 assert_eq!(order.outcome, PolymarketOutcome::yes());
846 assert_eq!(order.original_size, dec!(100.0000));
847 assert_eq!(order.price, dec!(0.5000));
848 assert_eq!(order.size_matched, dec!(25.0000));
849 assert_eq!(order.created_at, 1703875200);
850 assert!(order.expiration.is_none());
851 assert_eq!(order.associate_trades, Some(vec!["0xabc001".to_string()]));
852 }
853
854 #[rstest]
855 fn test_open_order_matched_sell_fok() {
856 let order: PolymarketOpenOrder = load("http_open_order_sell_fok.json");
857
858 assert_eq!(order.status, PolymarketOrderStatus::Matched);
859 assert_eq!(order.side, PolymarketOrderSide::Sell);
860 assert_eq!(order.order_type, PolymarketOrderType::FOK);
861 assert_eq!(order.outcome, PolymarketOutcome::no());
862 assert_eq!(order.size_matched, dec!(50.0000));
863 assert_eq!(order.expiration, Some("1735689600".to_string()));
864 assert!(order.associate_trades.is_none());
865 }
866
867 #[rstest]
868 fn test_open_order_roundtrip() {
869 let order: PolymarketOpenOrder = load("http_open_order.json");
870 let json = serde_json::to_string(&order).unwrap();
871 let order2: PolymarketOpenOrder = serde_json::from_str(&json).unwrap();
872 assert_eq!(order, order2);
873 }
874
875 #[rstest]
876 fn test_trade_report_fields() {
877 let trade: PolymarketTradeReport = load("http_trade_report.json");
878
879 assert_eq!(trade.id, "trade-0xabcdef1234");
880 assert_eq!(
881 trade.taker_order_id,
882 "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12"
883 );
884 assert_eq!(trade.side, PolymarketOrderSide::Buy);
885 assert_eq!(trade.size, dec!(25.0000));
886 assert_eq!(trade.fee_rate_bps, dec!(0));
887 assert_eq!(trade.price, dec!(0.5000));
888 assert_eq!(trade.status, PolymarketTradeStatus::Confirmed);
889 assert_eq!(trade.outcome, PolymarketOutcome::yes());
890 assert_eq!(trade.bucket_index, 0);
891 assert_eq!(trade.trader_side, PolymarketLiquiditySide::Taker);
892 assert_eq!(trade.maker_orders.len(), 2);
893 }
894
895 #[rstest]
896 fn test_trade_report_maker_orders() {
897 let trade: PolymarketTradeReport = load("http_trade_report.json");
898
899 let first = &trade.maker_orders[0];
900 assert_eq!(first.matched_amount, dec!(25.0000));
901 assert_eq!(first.price, dec!(0.5000));
902 assert_eq!(first.outcome, PolymarketOutcome::yes());
903
904 let second = &trade.maker_orders[1];
905 assert_eq!(second.matched_amount, dec!(5.0000));
906 }
907
908 #[rstest]
909 fn test_trade_report_roundtrip() {
910 let trade: PolymarketTradeReport = load("http_trade_report.json");
911 let json = serde_json::to_string(&trade).unwrap();
912 let trade2: PolymarketTradeReport = serde_json::from_str(&json).unwrap();
913 assert_eq!(trade, trade2);
914 }
915
916 #[rstest]
917 fn test_signed_order_camel_case_fields() {
918 let order: PolymarketOrder = load("http_signed_order.json");
919 let debug = format!("{order:?}");
920
921 assert_eq!(order.salt, 123456789);
922 assert_eq!(order.maker, "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266");
923 assert_eq!(order.maker_amount, dec!(100000000));
924 assert_eq!(order.taker_amount, dec!(50000000));
925 assert_eq!(order.expiration, "0");
926 assert_eq!(order.timestamp, "1713398400000");
927 assert_eq!(
928 order.metadata,
929 "0x0000000000000000000000000000000000000000000000000000000000000000"
930 );
931 assert_eq!(
932 order.builder,
933 "0x0000000000000000000000000000000000000000000000000000000000000000"
934 );
935 assert_eq!(order.side, PolymarketOrderSide::Buy);
936 assert_eq!(order.signature_type, PolymarketSignatureType::Eoa);
937 assert!(debug.contains(REDACTED));
938 assert!(!debug.contains(order.signature.expose_secret()));
939 }
940
941 #[rstest]
942 fn test_signed_order_roundtrip() {
943 let order: PolymarketOrder = load("http_signed_order.json");
944 let json = serde_json::to_string(&order).unwrap();
945 let order2: PolymarketOrder = serde_json::from_str(&json).unwrap();
946 assert_eq!(order, order2);
947 }
948
949 #[rstest]
950 fn test_signed_order_serializes_camel_case() {
951 let order: PolymarketOrder = load("http_signed_order.json");
952 let json = serde_json::to_string(&order).unwrap();
953
954 assert!(json.contains("\"tokenId\""));
956 assert!(json.contains("\"makerAmount\""));
957 assert!(json.contains("\"takerAmount\""));
958 assert!(json.contains("\"signatureType\""));
959 assert!(json.contains("\"expiration\""));
960 assert!(json.contains("\"timestamp\""));
961 assert!(json.contains("\"metadata\""));
962 assert!(json.contains("\"builder\""));
963 }
964
965 #[rstest]
966 fn test_signed_order_omits_v1_fields() {
967 let order: PolymarketOrder = load("http_signed_order.json");
971 let json = serde_json::to_string(&order).unwrap();
972
973 assert!(
974 !json.contains("\"taker\""),
975 "wire body must not include `taker`: {json}"
976 );
977 assert!(
978 !json.contains("\"nonce\""),
979 "wire body must not include `nonce`: {json}"
980 );
981 assert!(
982 !json.contains("\"feeRateBps\""),
983 "wire body must not include `feeRateBps`: {json}"
984 );
985 }
986
987 #[rstest]
988 fn test_signed_order_v2_docs_example_roundtrips() {
989 let docs_example = r#"{
992 "salt": 12345,
993 "maker": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
994 "signer": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
995 "tokenId": "102936",
996 "makerAmount": "1000000",
997 "takerAmount": "2000000",
998 "side": "BUY",
999 "signatureType": 1,
1000 "expiration": "0",
1001 "timestamp": "1713398400000",
1002 "metadata": "0x0000000000000000000000000000000000000000000000000000000000000000",
1003 "builder": "0x0000000000000000000000000000000000000000000000000000000000000000",
1004 "signature": "0xdeadbeef"
1005 }"#;
1006
1007 let order: PolymarketOrder = serde_json::from_str(docs_example).unwrap();
1008 assert_eq!(order.salt, 12345);
1009 assert_eq!(order.token_id.as_str(), "102936");
1010 assert_eq!(order.maker_amount, dec!(1000000));
1011 assert_eq!(order.taker_amount, dec!(2000000));
1012 assert_eq!(order.side, PolymarketOrderSide::Buy);
1013 assert_eq!(order.signature_type, PolymarketSignatureType::PolyProxy);
1014 assert_eq!(order.expiration, "0");
1015 assert_eq!(order.timestamp, "1713398400000");
1016
1017 let json = serde_json::to_string(&order).unwrap();
1019 let order2: PolymarketOrder = serde_json::from_str(&json).unwrap();
1020 assert_eq!(order, order2);
1021 }
1022
1023 #[rstest]
1024 fn test_gamma_event_deserialization() {
1025 let events: Vec<GammaEvent> = load("gamma_event.json");
1026
1027 assert_eq!(events.len(), 1);
1028 let event = &events[0];
1029 assert_eq!(event.id, "30829");
1030 assert_eq!(
1031 event.slug.as_deref(),
1032 Some("democratic-presidential-nominee-2028")
1033 );
1034 assert_eq!(
1035 event.title.as_deref(),
1036 Some("Democratic Presidential Nominee 2028")
1037 );
1038 assert_eq!(event.active, Some(true));
1039 assert_eq!(event.closed, Some(false));
1040 assert_eq!(event.archived, Some(false));
1041 assert_eq!(event.markets.len(), 2);
1042 assert_eq!(
1043 event.markets[0].condition_id,
1044 "0xc8f1cf5d4f26e0fd9c8fe89f2a7b3263b902cf14fde7bfccef525753bb492e47"
1045 );
1046 assert_eq!(
1047 event.markets[1].condition_id,
1048 "0xe39adea057926dc197fe30a441f57a340b2a232d5a687010f78bba9b6e02620f"
1049 );
1050 }
1051
1052 #[rstest]
1053 fn test_gamma_event_empty_markets() {
1054 let json = r#"[{"id": "evt-002"}]"#;
1055 let events: Vec<GammaEvent> = serde_json::from_str(json).unwrap();
1056
1057 assert_eq!(events.len(), 1);
1058 assert_eq!(events[0].id, "evt-002");
1059 assert!(events[0].markets.is_empty());
1060 assert!(events[0].slug.is_none());
1061 }
1062
1063 #[rstest]
1064 fn test_sports_market_are_weird() {
1065 let money_line: GammaMarket = load("gamma_market_sports_market_money_line.json");
1066 let map_handicap: GammaMarket = load("gamma_market_sports_market_map_handicap.json");
1067
1068 assert_eq!(
1070 money_line.events.as_ref().unwrap()[0].game_id,
1071 map_handicap.events.as_ref().unwrap()[0].game_id
1072 );
1073
1074 assert!(map_handicap.game_id.is_none());
1076 assert_eq!(money_line.game_id.as_deref(), Some("1427074"));
1077 }
1078
1079 #[rstest]
1080 fn test_gamma_event_composite_sports_game_id() {
1081 let events: Vec<GammaEvent> = load("gamma_event_sports_composite_game_id.json");
1084
1085 assert_eq!(events.len(), 1);
1086
1087 let event = &events[0];
1088
1089 assert_eq!(event.id, "835109");
1090 assert_eq!(event.game_id.as_deref(), Some("287011684"));
1091 assert_eq!(event.markets.len(), 2);
1092 assert_eq!(event.markets[0].id, "3524358");
1093 assert_eq!(
1094 event.markets[0].game_id.as_deref(),
1095 Some("dd80aae9-52f9-4c7b-a1cf-7b4ab63cd281:STL:TEX")
1096 );
1097 assert_eq!(event.markets[1].id, "3554041");
1098 assert_eq!(event.markets[1].game_id, None);
1099
1100 let encoded = serde_json::to_value(event).unwrap();
1103
1104 assert_eq!(encoded["gameId"], serde_json::json!("287011684"));
1105 assert_eq!(
1106 encoded["markets"][0]["gameId"],
1107 serde_json::json!("dd80aae9-52f9-4c7b-a1cf-7b4ab63cd281:STL:TEX")
1108 );
1109 }
1110
1111 #[rstest]
1112 fn test_fee_schedule_decimal_fields() {
1113 let market: GammaMarket = load("gamma_market_sports_market_money_line.json");
1114 let schedule = market.fee_schedule.unwrap();
1115
1116 assert_eq!(schedule.exponent, Decimal::ONE);
1117 assert_eq!(schedule.rate, dec!(0.03));
1118 assert!(schedule.taker_only);
1119 assert_eq!(schedule.rebate_rate, dec!(0.25));
1120 }
1121
1122 #[rstest]
1123 fn test_gamma_market_crypto_market_config_fields() {
1124 let market: GammaMarket = load("gamma_market_crypto_twap.json");
1125 let config = market.crypto_market_config.as_ref().unwrap();
1126
1127 assert_eq!(config.id, "btc-5m-twap-60");
1128 assert_eq!(config.asset, "btc");
1129 assert_eq!(config.duration, "5m");
1130 assert!(config.twap_enabled);
1131 assert_eq!(config.twap_lookback_seconds, Some(60));
1132 assert_eq!(
1133 market.resolution_source.as_deref(),
1134 Some("https://data.chain.link/streams/btc-usd-twap-60s-streams")
1135 );
1136 assert_eq!(
1137 market.event_start_time.as_deref(),
1138 Some("2026-08-22T16:00:00Z")
1139 );
1140 }
1141
1142 #[rstest]
1143 fn test_crypto_market_config_absent_twap_lookback_serializes_omitted() {
1144 let crypto_market_config = serde_json::json!({
1145 "id": "btc-5m",
1146 "asset": "btc",
1147 "duration": "5m",
1148 "twapEnabled": false,
1149 });
1150
1151 let config: CryptoMarketConfig = serde_json::from_value(crypto_market_config).unwrap();
1152 let encoded = serde_json::to_value(config).unwrap();
1153
1154 assert!(encoded.get("twapLookbackSeconds").is_none());
1155 }
1156
1157 #[rstest]
1158 fn test_crypto_market_config_rejects_null_twap_lookback() {
1159 let crypto_market_config = serde_json::json!({
1160 "id": "btc-5m",
1161 "asset": "btc",
1162 "duration": "5m",
1163 "twapEnabled": false,
1164 "twapLookbackSeconds": null,
1165 });
1166
1167 let result = serde_json::from_value::<CryptoMarketConfig>(crypto_market_config);
1168
1169 assert!(result.is_err());
1170 }
1171
1172 #[rstest]
1173 #[case(serde_json::json!(-37))]
1174 #[case(serde_json::json!(i64::MIN))]
1175 #[case(serde_json::json!(i64::MAX))]
1176 fn test_crypto_market_config_signed_twap_lookback_roundtrip(
1177 #[case] twap_lookback_seconds: serde_json::Value,
1178 ) {
1179 let crypto_market_config = serde_json::json!({
1180 "id": "eth-15m",
1181 "asset": "eth",
1182 "duration": "15m",
1183 "twapEnabled": true,
1184 "twapLookbackSeconds": twap_lookback_seconds,
1185 });
1186
1187 let config: CryptoMarketConfig = serde_json::from_value(crypto_market_config).unwrap();
1188 let encoded = serde_json::to_value(config).unwrap();
1189
1190 assert_eq!(encoded["twapLookbackSeconds"], twap_lookback_seconds);
1191 }
1192
1193 #[rstest]
1194 fn test_crypto_market_config_rejects_twap_lookback_above_i64_max() {
1195 let crypto_market_config = serde_json::json!({
1196 "id": "eth-15m",
1197 "asset": "eth",
1198 "duration": "15m",
1199 "twapEnabled": true,
1200 "twapLookbackSeconds": 9_223_372_036_854_775_808u64,
1201 });
1202
1203 let result = serde_json::from_value::<CryptoMarketConfig>(crypto_market_config);
1204
1205 assert!(result.is_err());
1206 }
1207
1208 #[rstest]
1209 fn test_gamma_market_enriched_fields() {
1210 let market: GammaMarket = load("gamma_market.json");
1211
1212 assert_eq!(
1213 market.event_start_time.as_deref(),
1214 Some("2026-03-12T09:20:00Z")
1215 );
1216 assert_eq!(market.best_bid, Some(dec!(0.5)));
1217 assert_eq!(market.best_ask, Some(dec!(0.51)));
1218 assert_eq!(market.spread, Some(dec!(0.009)));
1219 assert_eq!(market.last_trade_price, Some(dec!(0.51)));
1220 assert!(market.one_day_price_change.is_none());
1221 assert!(market.one_week_price_change.is_none());
1222 assert_eq!(market.volume_1wk, Some(dec!(9.999997)));
1223 assert_eq!(market.volume_1mo, Some(dec!(9.999997)));
1224 assert_eq!(market.volume_1yr, Some(dec!(9.999997)));
1225 assert_eq!(market.rewards_min_size, Some(dec!(50.0)));
1226 assert_eq!(market.rewards_max_spread, Some(dec!(4.5)));
1227 assert_eq!(market.competitive, Some(0.9999750006249843));
1228 assert!(market.category.is_none());
1229 assert!(market.neg_risk_market_id.is_none());
1230 assert!(market.uma_resolution_status.is_none());
1231 assert_eq!(market.uma_resolution_statuses.as_deref(), Some("[]"));
1232 assert_eq!(
1233 market.outcome_prices.as_deref(),
1234 Some("[\"0.505\", \"0.495\"]")
1235 );
1236 }
1237
1238 #[rstest]
1239 fn test_gamma_market_uma_resolution_statuses() {
1240 let market: GammaMarket = load("gamma_market.json");
1241
1242 assert!(market.uma_resolution_status.is_none());
1243 assert_eq!(market.uma_resolution_statuses.as_deref(), Some("[]"));
1244 }
1245
1246 #[rstest]
1247 fn test_gamma_market_enriched_fields_default_to_none() {
1248 let json = r#"{"id": "m1", "conditionId": "0xcond", "clobTokenIds": "[]", "outcomes": "[]", "question": "Q?"}"#;
1250 let market: GammaMarket = serde_json::from_str(json).unwrap();
1251
1252 assert!(market.best_bid.is_none());
1253 assert!(market.spread.is_none());
1254 assert!(market.volume_1wk.is_none());
1255 assert!(market.rewards_min_size.is_none());
1256 assert!(market.competitive.is_none());
1257 assert!(market.category.is_none());
1258 assert!(market.neg_risk_market_id.is_none());
1259 assert!(market.crypto_market_config.is_none());
1260 assert!(market.event_start_time.is_none());
1261 }
1262
1263 #[rstest]
1264 fn test_gamma_event_enriched_fields() {
1265 let events: Vec<GammaEvent> = load("gamma_event.json");
1266 let event = &events[0];
1267
1268 assert_eq!(event.liquidity, Some(dec!(43042905.16152)));
1269 assert_eq!(event.volume, Some(dec!(799823812.487094)));
1270 assert_eq!(event.open_interest, Some(dec!(0.0)));
1271 assert_eq!(event.volume_24hr, Some(dec!(5669354.219446001)));
1272 assert!(event.category.is_none());
1273 assert_eq!(event.neg_risk, Some(true));
1274 assert_eq!(
1275 event.neg_risk_market_id.as_deref(),
1276 Some("0x2c3d7e0eee6f058be3006baabf0d54a07da254ba47fe6e3e095e7990c7814700")
1277 );
1278 assert_eq!(event.featured, Some(false));
1279 }
1280
1281 #[rstest]
1282 fn test_gamma_tag_deserialization() {
1283 let tags: Vec<GammaTag> = load("gamma_tags.json");
1284
1285 assert_eq!(tags.len(), 5);
1286 assert_eq!(tags[0].id, "101259");
1287 assert_eq!(tags[0].label.as_deref(), Some("Health and Human Services"));
1288 assert_eq!(tags[0].slug.as_deref(), Some("health-and-human-services"));
1289 assert_eq!(tags[2].slug.as_deref(), Some("attorney-general"));
1290 }
1291
1292 #[rstest]
1293 fn test_search_response_deserialization() {
1294 let response: SearchResponse = load("search_response.json");
1295
1296 assert!(response.markets.is_none());
1298
1299 let events = response.events.as_ref().unwrap();
1300 assert_eq!(events.len(), 1);
1301 assert_eq!(events[0].slug.as_deref(), Some("bitcoin-above-on-march-11"));
1302 assert_eq!(events[0].markets.len(), 1);
1303 }
1304
1305 #[rstest]
1306 fn test_search_response_empty_fields() {
1307 let json = "{}";
1308 let response: SearchResponse = serde_json::from_str(json).unwrap();
1309 assert!(response.markets.is_none());
1310 assert!(response.events.is_none());
1311 }
1312
1313 #[rstest]
1314 fn test_clob_book_response_deserialization() {
1315 let response: ClobBookResponse = load("clob_book_response.json");
1316
1317 assert_eq!(response.bids.len(), 3);
1318 assert_eq!(response.asks.len(), 3);
1319
1320 assert_eq!(response.bids[0].price, "0.48");
1321 assert_eq!(response.bids[0].size, "100.00");
1322 assert_eq!(response.bids[2].price, "0.50");
1323 assert_eq!(response.bids[2].size, "150.00");
1324
1325 assert_eq!(response.asks[0].price, "0.51");
1326 assert_eq!(response.asks[0].size, "120.00");
1327 assert_eq!(response.asks[2].price, "0.53");
1328 assert_eq!(response.asks[2].size, "90.00");
1329 }
1330
1331 #[rstest]
1332 fn test_clob_book_response_ignores_extra_fields() {
1333 let json = r#"{
1339 "market": "0xabc",
1340 "asset_id": "123",
1341 "hash": "0x1",
1342 "timestamp": "123",
1343 "bids": [],
1344 "asks": [],
1345 "tick_size": "0.01",
1346 "min_order_size": "5",
1347 "neg_risk": false,
1348 "last_trade_price": "0.55"
1349 }"#;
1350 let response: ClobBookResponse = serde_json::from_str(json).unwrap();
1351 assert!(response.bids.is_empty());
1352 assert!(response.asks.is_empty());
1353 }
1354
1355 #[rstest]
1356 fn test_clob_market_response_captured_fields() {
1357 let response: ClobMarketResponse = load("clob_market_response.json");
1358 let raw: serde_json::Value = load("clob_market_response.json");
1359
1360 assert_eq!(response.enable_order_book, Some(true));
1361 assert_eq!(response.active, Some(true));
1362 assert!(!response.closed);
1363 assert_eq!(response.archived, Some(false));
1364 assert_eq!(response.accepting_orders, Some(true));
1365 assert_eq!(
1366 response.accepting_order_timestamp.as_deref(),
1367 Some("2026-08-01T22:56:49Z")
1368 );
1369 assert_eq!(response.minimum_order_size, Some(dec!(5)));
1370 assert_eq!(response.minimum_tick_size, Some(dec!(0.01)));
1371 assert_eq!(
1372 response.condition_id,
1373 "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
1374 );
1375 assert_eq!(
1376 response.question_id.as_deref(),
1377 Some("0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd")
1378 );
1379 assert_eq!(
1380 response.question.as_deref(),
1381 Some("LoL: T1 vs Hanwha Life Esports (BO3) - LCK Round 3-4 Legend Group")
1382 );
1383 assert_eq!(response.description.as_deref(), raw["description"].as_str());
1384 assert_eq!(
1385 response.market_slug.as_deref(),
1386 Some("sanitized-clob-market")
1387 );
1388 assert_eq!(
1389 response.end_date_iso.as_deref(),
1390 Some("2026-08-08T00:00:00Z")
1391 );
1392 assert_eq!(
1393 response.game_start_time.as_deref(),
1394 Some("2026-08-08T08:00:00Z")
1395 );
1396 assert_eq!(response.seconds_delay, Some(1));
1397 assert_eq!(response.fpmm.as_deref(), Some(""));
1398 assert_eq!(response.maker_base_fee, Some(1000));
1399 assert_eq!(response.taker_base_fee, Some(1000));
1400 assert_eq!(response.notifications_enabled, Some(true));
1401 assert_eq!(response.neg_risk, Some(false));
1402 assert_eq!(response.neg_risk_market_id.as_deref(), Some(""));
1403 assert_eq!(response.neg_risk_request_id.as_deref(), Some(""));
1404 assert_eq!(
1405 response.icon.as_deref(),
1406 Some("https://example.com/sanitized-market.png")
1407 );
1408 assert_eq!(
1409 response.image.as_deref(),
1410 Some("https://example.com/sanitized-market.png")
1411 );
1412 let rewards = response.rewards.as_ref().expect("captured rewards");
1413 assert!(rewards.rates.is_none());
1414 assert_eq!(rewards.min_size, Some(dec!(50)));
1415 assert_eq!(rewards.max_spread, Some(dec!(4.5)));
1416 assert_eq!(response.is_50_50_outcome, Some(false));
1417 assert_eq!(response.tokens.len(), 2);
1418 assert_eq!(
1419 response.tokens[0].token_id,
1420 "10000000000000000000000000000000000000000000000000000000000000000000000000001"
1421 );
1422 assert_eq!(response.tokens[0].outcome, "T1");
1423 assert_eq!(response.tokens[0].price, Some(dec!(0.715)));
1424 assert!(!response.tokens[0].winner);
1425 assert_eq!(
1426 response.tokens[1].token_id,
1427 "10000000000000000000000000000000000000000000000000000000000000000000000000002"
1428 );
1429 assert_eq!(response.tokens[1].outcome, "Hanwha Life Esports");
1430 assert_eq!(response.tokens[1].price, Some(dec!(0.285)));
1431 assert!(!response.tokens[1].winner);
1432 assert_eq!(
1433 response.tags.as_deref(),
1434 Some(
1435 &[
1436 "Sports".to_string(),
1437 "Esports".to_string(),
1438 "league of legends".to_string(),
1439 "Games".to_string(),
1440 ][..]
1441 )
1442 );
1443 }
1444
1445 #[rstest]
1446 fn test_clob_market_rewards_documented_rate_fields() {
1447 let json = r#"{
1449 "rates":[{"asset_address":"0x1111111111111111111111111111111111111111","rewards_daily_rate":12.5}],
1450 "min_size":25,
1451 "max_spread":3.5
1452 }"#;
1453 let rewards: ClobMarketRewards = serde_json::from_str(json).unwrap();
1454
1455 let rates = rewards.rates.as_deref().expect("documented reward rate");
1456 assert_eq!(rates.len(), 1);
1457 assert_eq!(
1458 rates[0].asset_address,
1459 "0x1111111111111111111111111111111111111111"
1460 );
1461 assert_eq!(rates[0].rewards_daily_rate, dec!(12.5));
1462 assert_eq!(rewards.min_size, Some(dec!(25)));
1463 assert_eq!(rewards.max_spread, Some(dec!(3.5)));
1464 }
1465
1466 #[rstest]
1467 fn test_clob_market_decimal_fields_preserve_precision() {
1468 let json = r#"{
1469 "condition_id":"0xcondition",
1470 "closed":false,
1471 "minimum_order_size":123456789.1234567890123456789,
1472 "minimum_tick_size":0.1234567890123456789012345678,
1473 "rewards":{
1474 "rates":[{
1475 "asset_address":"0x1111111111111111111111111111111111111111",
1476 "rewards_daily_rate":0.1234567890123456789012345678
1477 }],
1478 "min_size":123456789.1234567890123456789,
1479 "max_spread":0.1234567890123456789012345678
1480 },
1481 "tokens":[{
1482 "token_id":"token-1",
1483 "outcome":"Yes",
1484 "price":0.1234567890123456789012345678,
1485 "winner":false
1486 }]
1487 }"#;
1488 let market: ClobMarketResponse = serde_json::from_str(json).unwrap();
1489 let precise = Decimal::from_str_exact("0.1234567890123456789012345678").unwrap();
1490 let large = Decimal::from_str_exact("123456789.1234567890123456789").unwrap();
1491
1492 assert_eq!(market.minimum_order_size, Some(large));
1493 assert_eq!(market.minimum_tick_size, Some(precise));
1494 let rewards = market.rewards.as_ref().unwrap();
1495 assert_eq!(
1496 rewards.rates.as_ref().unwrap()[0].rewards_daily_rate,
1497 precise
1498 );
1499 assert_eq!(rewards.min_size, Some(large));
1500 assert_eq!(rewards.max_spread, Some(precise));
1501 assert_eq!(market.tokens[0].price, Some(precise));
1502 let serialized = serde_json::to_string(&market).unwrap();
1503 assert!(serialized.contains("\"minimum_order_size\":123456789.1234567890123456789"));
1504 assert!(serialized.contains("\"minimum_tick_size\":0.1234567890123456789012345678"));
1505 assert!(serialized.contains("\"rewards_daily_rate\":0.1234567890123456789012345678"));
1506 assert!(serialized.contains("\"min_size\":123456789.1234567890123456789"));
1507 assert!(serialized.contains("\"max_spread\":0.1234567890123456789012345678"));
1508 assert!(serialized.contains("\"price\":0.1234567890123456789012345678"));
1509 }
1510
1511 #[rstest]
1512 fn test_clob_market_response_deserialization_accepting_false() {
1513 let response: ClobMarketResponse = load("clob_market_closed_binary_accepting_false.json");
1514 assert_eq!(
1515 response.condition_id,
1516 "0x8ccc3f4951ff02c1d34b87988752b4444ad17228732780a6cf22afefe8478bb6"
1517 );
1518 assert!(response.closed);
1519 assert_eq!(response.tokens.len(), 2);
1520 assert_eq!(response.tokens[0].outcome, "Yes");
1521 assert!(!response.tokens[0].winner);
1522 assert_eq!(response.tokens[1].outcome, "No");
1523 assert!(response.tokens[1].winner);
1524 }
1525
1526 #[rstest]
1527 fn test_clob_market_response_deserialization_accepting_true() {
1528 let response: ClobMarketResponse = load("clob_market_closed_binary_accepting_true.json");
1529 assert_eq!(
1530 response.condition_id,
1531 "0xd57eed0d44f5b8ca54925d8d6ff440b146b3e6e071da18136ee3ee572d34479e"
1532 );
1533 assert!(response.closed);
1534 assert_eq!(response.tokens.len(), 2);
1535 assert_eq!(response.tokens[0].outcome, "Yes");
1536 assert!(response.tokens[0].winner);
1537 assert_eq!(response.tokens[1].outcome, "No");
1538 assert!(!response.tokens[1].winner);
1539 }
1540
1541 #[rstest]
1542 fn test_tick_size_response_preserves_json_number() {
1543 let response: TickSizeResponse =
1544 serde_json::from_str(r#"{"minimum_tick_size":0.1234567890123456789012345678}"#)
1545 .unwrap();
1546 let precise =
1547 rust_decimal::Decimal::from_str_exact("0.1234567890123456789012345678").unwrap();
1548
1549 assert_eq!(response.minimum_tick_size, precise);
1550 }
1551
1552 #[rstest]
1553 fn test_fee_rate_response_zero() {
1554 let response: FeeRateResponse = load("clob_fee_rate_response_zero.json");
1555 assert_eq!(response.base_fee, dec!(0));
1556 }
1557
1558 #[rstest]
1559 fn test_fee_rate_response_nonzero() {
1560 let response: FeeRateResponse = load("clob_fee_rate_response_nonzero.json");
1561 assert_eq!(response.base_fee, dec!(150));
1562 }
1563
1564 #[rstest]
1565 fn test_data_api_position_deserialization() {
1566 let positions: Vec<DataApiPosition> =
1567 load::<DataApiPage<DataApiPosition>>("data_api_positions_response.json").data;
1568
1569 assert_eq!(positions.len(), 4);
1570 assert_eq!(
1571 positions[0].asset,
1572 "71321045863084981365469005770620412523470745398083994982746259498689308907982"
1573 );
1574 assert_eq!(
1575 positions[0].condition_id,
1576 "0xc8f1cf5d4f26e0fd9c8fe89f2a7b3263b902cf14fde7bfccef525753bb492e47"
1577 );
1578 assert_eq!(positions[0].size, dec!(150.5));
1579 assert_eq!(positions[0].avg_price, Some(dec!(0.55)));
1580 assert!(!positions[0].redeemable);
1581
1582 assert_eq!(positions[1].size, dec!(0));
1584 assert_eq!(positions[1].avg_price, Some(dec!(0.45)));
1585 assert!(!positions[1].redeemable);
1586
1587 assert_eq!(
1589 positions[2].condition_id,
1590 "0xabc123def456789012345678901234567890abcdef1234567890abcdef123456"
1591 );
1592 assert_eq!(positions[2].size, dec!(42));
1593 assert_eq!(positions[2].avg_price, Some(dec!(0.3)));
1594 assert!(positions[2].redeemable);
1595
1596 assert_eq!(positions[3].size, dec!(0.005));
1598 assert_eq!(positions[3].avg_price, Some(dec!(0.7)));
1599 assert!(!positions[3].redeemable);
1600 }
1601
1602 #[rstest]
1603 fn test_data_api_page_deserializes_null_and_missing_data_as_empty() {
1604 let null_data = r#"{
1606 "data": null,
1607 "pagination": {"limit": 500, "offset": 0, "has_more": false, "next_cursor": null}
1608 }"#;
1609 let page: DataApiPage<DataApiTrade> = serde_json::from_str(null_data).unwrap();
1610 assert!(page.data.is_empty());
1611 assert!(!page.pagination.has_more);
1612 assert!(page.pagination.next_cursor.is_none());
1613
1614 let missing_data = r#"{
1615 "pagination": {"limit": 500, "offset": 0, "has_more": false, "next_cursor": null}
1616 }"#;
1617 let page: DataApiPage<DataApiTrade> = serde_json::from_str(missing_data).unwrap();
1618 assert!(page.data.is_empty());
1619 }
1620
1621 #[rstest]
1622 fn test_data_api_trade_deserialization() {
1623 let trades: Vec<DataApiTrade> =
1624 load::<DataApiPage<DataApiTrade>>("data_api_trades_captured_response.json").data;
1625
1626 assert_eq!(trades.len(), 3);
1627 assert_eq!(
1628 trades[0].asset,
1629 "10000000000000000000000000000000000000000000000000000000000000000000000000001"
1630 );
1631 assert_eq!(
1632 trades[0].condition_id,
1633 "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
1634 );
1635 assert_eq!(trades[0].side, PolymarketOrderSide::Sell);
1636 assert_eq!(trades[0].price, dec!(0.7));
1637 assert_eq!(trades[0].size, dec!(92.59));
1638 assert_eq!(trades[0].timestamp, 1786179735);
1639 assert_eq!(
1640 trades[0].transaction_hash,
1641 "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
1642 );
1643
1644 assert_eq!(trades[1].asset, trades[0].asset);
1645 assert_eq!(trades[1].condition_id, trades[0].condition_id);
1646 assert_eq!(trades[1].side, PolymarketOrderSide::Buy);
1647 assert_eq!(trades[1].price, dec!(0.709999959));
1648 assert_eq!(trades[1].size, dec!(1.464786));
1649 assert_eq!(trades[1].timestamp, 1786179730);
1650 assert_eq!(
1651 trades[1].transaction_hash,
1652 "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
1653 );
1654 assert_eq!(
1655 trades[2].asset,
1656 "10000000000000000000000000000000000000000000000000000000000000000000000000002"
1657 );
1658 assert_eq!(trades[2].condition_id, trades[0].condition_id);
1659 assert_eq!(trades[2].side, PolymarketOrderSide::Buy);
1660 assert_eq!(trades[2].price, dec!(0.2972581967));
1661 assert_eq!(trades[2].size, dec!(244));
1662 assert_eq!(trades[2].timestamp, 1786179726);
1663 assert_eq!(
1664 trades[2].transaction_hash,
1665 "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
1666 );
1667
1668 for (trade, outcome, outcome_index) in [
1669 (&trades[0], "T1", 0),
1670 (&trades[1], "T1", 0),
1671 (&trades[2], "Hanwha Life Esports", 1),
1672 ] {
1673 assert_eq!(
1674 trade.proxy_wallet.as_deref(),
1675 Some("0x1111111111111111111111111111111111111111")
1676 );
1677 assert_eq!(
1678 trade.title.as_deref(),
1679 Some("LoL: T1 vs Hanwha Life Esports (BO3) - LCK Round 3-4 Legend Group")
1680 );
1681 assert_eq!(trade.slug.as_deref(), Some("sanitized-market"));
1682 assert_eq!(
1683 trade.icon.as_deref(),
1684 Some("https://example.com/sanitized-market.png")
1685 );
1686 assert_eq!(trade.event_slug.as_deref(), Some("sanitized-event"));
1687 assert_eq!(trade.outcome.as_deref(), Some(outcome));
1688 assert_eq!(trade.outcome_index, Some(outcome_index));
1689 assert_eq!(trade.name.as_deref(), Some("Sanitized trader"));
1690 assert_eq!(trade.pseudonym.as_deref(), Some("sanitized-trader"));
1691 assert_eq!(trade.bio.as_deref(), Some("Sanitized profile"));
1692 assert_eq!(
1693 trade.profile_image.as_deref(),
1694 Some("https://example.com/sanitized-profile.png")
1695 );
1696 assert_eq!(
1697 trade.profile_image_optimized.as_deref(),
1698 Some("https://example.com/sanitized-profile-optimized.png")
1699 );
1700 }
1701 }
1702
1703 #[rstest]
1704 fn test_data_api_trade_decimal_fields_preserve_precision() {
1705 let json = r#"{
1706 "token_id":"token-1",
1707 "condition_id":"0xcondition",
1708 "side":"BUY",
1709 "price":0.1234567890123456789012345678,
1710 "size":123456789.1234567890123456789,
1711 "timestamp":1786179735,
1712 "transaction_hash":"0xtransaction"
1713 }"#;
1714 let trade: DataApiTrade = serde_json::from_str(json).unwrap();
1715
1716 assert_eq!(
1717 trade.price,
1718 Decimal::from_str_exact("0.1234567890123456789012345678").unwrap()
1719 );
1720 assert_eq!(
1721 trade.size,
1722 Decimal::from_str_exact("123456789.1234567890123456789").unwrap()
1723 );
1724 }
1725
1726 #[rstest]
1727 fn test_gamma_market_fee_fields() {
1728 let market: GammaMarket = load("gamma_market.json");
1729
1730 assert_eq!(market.fees_enabled, Some(true));
1731 assert_eq!(market.fee_type.as_deref(), Some("crypto_fees"));
1732 assert!(market.tags.is_none());
1733 assert!(market.sports_market_type.is_none());
1734 }
1735
1736 #[rstest]
1737 fn test_gamma_market_sports_fee_fields() {
1738 let market: GammaMarket = load("gamma_market_sports_market_money_line.json");
1739
1740 assert_eq!(market.fees_enabled, Some(true));
1741 assert_eq!(market.fee_type.as_deref(), Some("sports_fees_v2"));
1742 }
1743
1744 #[rstest]
1745 fn test_gamma_event_tags() {
1746 let events: Vec<GammaEvent> = load("gamma_event_sports_composite_game_id.json");
1747 let tags = events[0].tags.as_ref().unwrap();
1748
1749 assert!(tags.iter().any(|tag| tag.slug.as_deref() == Some("sports")));
1750 }
1751
1752 #[rstest]
1753 fn test_fee_rate_response_to_rate() {
1754 let zero: FeeRateResponse = load("clob_fee_rate_response_zero.json");
1755 let nonzero: FeeRateResponse = load("clob_fee_rate_response_nonzero.json");
1756 let numeric: FeeRateResponse = serde_json::from_str(r#"{"base_fee":700}"#).unwrap();
1757
1758 assert_eq!(zero.to_rate(), Decimal::ZERO);
1759 assert_eq!(nonzero.to_rate(), dec!(0.015));
1760 assert_eq!(numeric.to_rate(), dec!(0.07));
1761 }
1762}