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