1use nautilus_core::serialization::deserialize_empty_string_as_none;
19use rust_decimal::Decimal;
20use serde::{
21 Deserialize, Deserializer, Serialize, Serializer,
22 de::{
23 DeserializeSeed, MapAccess, Visitor,
24 value::{BorrowedStrDeserializer, MapAccessDeserializer},
25 },
26};
27use serde_json::value::RawValue;
28use ustr::Ustr;
29
30use crate::common::{
31 enums::{
32 PolymarketEventType, PolymarketLiquiditySide, PolymarketOrderSide, PolymarketOrderStatus,
33 PolymarketOrderType, PolymarketOutcome, PolymarketTradeStatus,
34 },
35 models::PolymarketMakerOrder,
36 parse::{
37 deserialize_decimal_from_str, deserialize_optional_decimal_from_str,
38 serialize_decimal_as_str, serialize_optional_decimal_as_str,
39 },
40};
41
42#[derive(Clone, Debug, PartialEq, Eq)]
44pub struct PolymarketUserOrderStatus {
45 pub status: PolymarketOrderStatus,
46 pub reason: Option<String>,
47}
48
49impl PolymarketUserOrderStatus {
50 pub(crate) fn new(status: PolymarketOrderStatus, reason: Option<&str>) -> Self {
51 Self {
52 status,
53 reason: reason
54 .filter(|reason| !reason.trim().is_empty())
55 .map(str::to_string),
56 }
57 }
58}
59
60impl From<PolymarketOrderStatus> for PolymarketUserOrderStatus {
61 fn from(status: PolymarketOrderStatus) -> Self {
62 Self::new(status, None)
63 }
64}
65
66impl<'de> Deserialize<'de> for PolymarketUserOrderStatus {
67 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
68 where
69 D: Deserializer<'de>,
70 {
71 let raw = String::deserialize(deserializer)?;
72
73 PolymarketOrderStatus::parse_wire(&raw)
74 .map(|(status, reason)| Self::new(status, reason))
75 .ok_or_else(|| {
76 serde::de::Error::custom(format!("Unknown PolymarketOrderStatus: {raw}"))
77 })
78 }
79}
80
81impl Serialize for PolymarketUserOrderStatus {
82 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
83 where
84 S: Serializer,
85 {
86 match self.reason.as_deref() {
87 Some(reason) => serializer.serialize_str(&format!("{}_{reason}", self.status)),
88 None => self.status.serialize(serializer),
89 }
90 }
91}
92
93#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
97pub struct PolymarketUserOrder {
98 pub asset_id: Ustr,
99 pub associate_trades: Option<Vec<String>>,
100 pub created_at: Option<String>,
101 pub expiration: Option<String>,
102 pub id: String,
103 pub maker_address: Option<Ustr>,
104 pub market: Ustr,
105 pub order_owner: Option<Ustr>,
106 pub order_type: Option<PolymarketOrderType>,
107 pub original_size: String,
108 pub outcome: Option<PolymarketOutcome>,
109 pub owner: Ustr,
110 pub price: String,
111 pub side: PolymarketOrderSide,
112 pub size_matched: String,
113 pub status: Option<PolymarketUserOrderStatus>,
114 pub timestamp: String,
115 #[serde(rename = "type")]
116 pub event_type: PolymarketEventType,
117}
118
119#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
123pub struct PolymarketUserTrade {
124 pub asset_id: Ustr,
125 pub bucket_index: u64,
126 pub fee_rate_bps: String,
127 pub id: String,
128 pub last_update: String,
129 pub maker_address: Ustr,
130 pub maker_orders: Vec<PolymarketMakerOrder>,
131 pub market: Ustr,
132 pub match_time: String,
133 pub outcome: PolymarketOutcome,
134 pub owner: Ustr,
135 pub price: String,
136 pub side: PolymarketOrderSide,
137 pub size: String,
138 pub status: PolymarketTradeStatus,
139 pub taker_order_id: String,
140 pub timestamp: String,
141 pub trade_owner: Ustr,
142 #[serde(
143 default,
144 deserialize_with = "deserialize_empty_string_as_none",
145 skip_serializing_if = "Option::is_none"
146 )]
147 pub transaction_hash: Option<String>,
148 pub trader_side: PolymarketLiquiditySide,
149 #[serde(rename = "type")]
150 pub event_type: PolymarketEventType,
151}
152
153#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
155pub struct PolymarketBookLevel {
156 pub price: String,
157 pub size: String,
158}
159
160#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
162pub struct PolymarketBookSnapshot {
163 pub market: Ustr,
164 pub asset_id: Ustr,
165 pub bids: Vec<PolymarketBookLevel>,
166 pub asks: Vec<PolymarketBookLevel>,
167 pub timestamp: String,
168 #[serde(default)]
169 pub hash: Option<String>,
170 #[serde(default)]
171 pub min_order_size: Option<String>,
172 #[serde(default)]
173 pub tick_size: Option<String>,
174 #[serde(default)]
175 pub neg_risk: Option<bool>,
176 #[serde(default)]
177 pub last_trade_price: Option<String>,
178}
179
180#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
182pub struct PolymarketQuote {
183 pub asset_id: Ustr,
184 pub price: String,
185 pub side: PolymarketOrderSide,
186 pub size: String,
187 pub hash: String,
188 #[serde(default)]
189 pub best_bid: Option<String>,
190 #[serde(default)]
191 pub best_ask: Option<String>,
192}
193
194#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
196pub struct PolymarketQuotes {
197 pub market: Ustr,
198 pub price_changes: Vec<PolymarketQuote>,
199 pub timestamp: String,
200}
201
202#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
204pub struct PolymarketTrade {
205 pub market: Ustr,
206 pub asset_id: Ustr,
207 pub fee_rate_bps: String,
208 pub price: String,
209 pub side: PolymarketOrderSide,
210 pub size: String,
211 pub timestamp: String,
212 #[serde(default)]
213 pub transaction_hash: Option<String>,
214}
215
216#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
218pub struct PolymarketTickSizeChange {
219 pub market: Ustr,
220 pub asset_id: Ustr,
221 pub new_tick_size: String,
222 pub old_tick_size: String,
223 pub timestamp: String,
224}
225
226#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
228pub struct PolymarketNewMarketEvent {
229 pub id: String,
230 pub ticker: String,
231 pub slug: String,
232 pub title: String,
233 pub description: String,
234}
235
236#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
238pub struct PolymarketNewMarketFeeSchedule {
239 #[serde(
240 serialize_with = "serialize_decimal_as_str",
241 deserialize_with = "deserialize_decimal_from_str"
242 )]
243 pub exponent: Decimal,
244 #[serde(
245 serialize_with = "serialize_decimal_as_str",
246 deserialize_with = "deserialize_decimal_from_str"
247 )]
248 pub rate: Decimal,
249 pub taker_only: bool,
250 #[serde(
251 serialize_with = "serialize_decimal_as_str",
252 deserialize_with = "deserialize_decimal_from_str"
253 )]
254 pub rebate_rate: Decimal,
255}
256
257#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
261pub struct PolymarketNewMarket {
262 pub id: String,
263 pub question: String,
264 pub market: Ustr,
265 pub slug: String,
266 pub description: String,
267 pub assets_ids: Vec<String>,
268 pub outcomes: Vec<String>,
269 pub timestamp: String,
270 pub tags: Vec<String>,
271 pub condition_id: String,
272 pub active: bool,
273 pub clob_token_ids: Vec<String>,
274 #[serde(default)]
275 pub order_price_min_tick_size: Option<String>,
276 #[serde(default)]
277 pub group_item_title: Option<String>,
278 #[serde(default)]
279 pub event_message: Option<PolymarketNewMarketEvent>,
280 #[serde(default)]
281 pub sports_market_type: Option<String>,
282 #[serde(default)]
283 pub line: Option<String>,
284 #[serde(default)]
285 pub game_start_time: Option<String>,
286 #[serde(
287 default,
288 skip_serializing_if = "Option::is_none",
289 serialize_with = "serialize_optional_decimal_as_str",
290 deserialize_with = "deserialize_optional_decimal_from_str"
291 )]
292 pub taker_base_fee: Option<Decimal>,
293 #[serde(default)]
294 pub fees_enabled: Option<bool>,
295 #[serde(default)]
296 pub fee_schedule: Option<PolymarketNewMarketFeeSchedule>,
297}
298
299#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
303pub struct PolymarketMarketResolved {
304 pub id: String,
305 pub market: Ustr,
306 pub assets_ids: Vec<String>,
307 pub winning_asset_id: String,
308 pub winning_outcome: String,
309 pub timestamp: String,
310 pub tags: Vec<String>,
311}
312
313#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
318pub struct PolymarketBestBidAsk {
319 pub market: Ustr,
320 pub asset_id: Ustr,
321 pub best_bid: String,
322 pub best_ask: String,
323 pub spread: String,
324 pub timestamp: String,
325}
326
327#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
329#[serde(tag = "event_type")]
330pub enum MarketWsMessage {
331 #[serde(rename = "book")]
332 Book(PolymarketBookSnapshot),
333 #[serde(rename = "price_change")]
334 PriceChange(PolymarketQuotes),
335 #[serde(rename = "last_trade_price")]
336 LastTradePrice(PolymarketTrade),
337 #[serde(rename = "tick_size_change")]
338 TickSizeChange(PolymarketTickSizeChange),
339 #[serde(rename = "new_market")]
340 NewMarket(Box<PolymarketNewMarket>),
341 #[serde(rename = "market_resolved")]
342 MarketResolved(PolymarketMarketResolved),
343 #[serde(rename = "best_bid_ask")]
344 BestBidAsk(PolymarketBestBidAsk),
345}
346
347struct PayloadMapAccess<A> {
348 inner: A,
349}
350
351impl<A> PayloadMapAccess<A> {
352 const fn new(inner: A) -> Self {
353 Self { inner }
354 }
355}
356
357impl<'de, A> MapAccess<'de> for PayloadMapAccess<A>
358where
359 A: MapAccess<'de>,
360{
361 type Error = A::Error;
362
363 fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Self::Error>
364 where
365 K: DeserializeSeed<'de>,
366 {
367 let Some(key) = self.inner.next_key::<&'de str>()? else {
368 return Ok(None);
369 };
370
371 if key == "event_type" {
372 return Err(serde::de::Error::duplicate_field("event_type"));
373 }
374
375 seed.deserialize(BorrowedStrDeserializer::new(key))
376 .map(Some)
377 }
378
379 fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value, Self::Error>
380 where
381 V: DeserializeSeed<'de>,
382 {
383 self.inner.next_value_seed(seed)
384 }
385
386 fn size_hint(&self) -> Option<usize> {
387 self.inner.size_hint()
388 }
389}
390
391struct MarketWsMessageVisitor;
392
393impl<'de> Visitor<'de> for MarketWsMessageVisitor {
394 type Value = MarketWsMessage;
395
396 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
397 formatter.write_str("a Polymarket market-channel message with event_type first")
398 }
399
400 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
401 where
402 A: MapAccess<'de>,
403 {
404 let Some(key) = map.next_key::<&str>()? else {
405 return Err(serde::de::Error::custom("expected event_type field"));
406 };
407
408 if key != "event_type" {
409 return Err(serde::de::Error::custom(
410 "event_type was not the first field",
411 ));
412 }
413
414 let event_type = map.next_value::<&str>()?;
415 let remaining = MapAccessDeserializer::new(PayloadMapAccess::new(map));
416 match event_type {
417 "book" => PolymarketBookSnapshot::deserialize(remaining).map(Self::Value::Book),
418 "price_change" => {
419 PolymarketQuotes::deserialize(remaining).map(Self::Value::PriceChange)
420 }
421 "last_trade_price" => {
422 PolymarketTrade::deserialize(remaining).map(Self::Value::LastTradePrice)
423 }
424 "tick_size_change" => {
425 PolymarketTickSizeChange::deserialize(remaining).map(Self::Value::TickSizeChange)
426 }
427 "new_market" => PolymarketNewMarket::deserialize(remaining)
428 .map(Box::new)
429 .map(Self::Value::NewMarket),
430 "market_resolved" => {
431 PolymarketMarketResolved::deserialize(remaining).map(Self::Value::MarketResolved)
432 }
433 "best_bid_ask" => {
434 PolymarketBestBidAsk::deserialize(remaining).map(Self::Value::BestBidAsk)
435 }
436 other => Err(serde::de::Error::unknown_variant(
437 other,
438 &[
439 "book",
440 "price_change",
441 "last_trade_price",
442 "tick_size_change",
443 "new_market",
444 "market_resolved",
445 "best_bid_ask",
446 ],
447 )),
448 }
449 }
450}
451
452impl MarketWsMessage {
453 pub fn parse(text: &str) -> serde_json::Result<Self> {
459 let mut deserializer = serde_json::Deserializer::from_str(text);
460 serde::Deserializer::deserialize_map(&mut deserializer, MarketWsMessageVisitor)
461 .and_then(|message| {
462 deserializer.end()?;
463 Ok(message)
464 })
465 .or_else(|_| Self::parse_reordered(text))
466 .or_else(|_| serde_json::from_str(text))
467 }
468
469 fn parse_reordered(text: &str) -> serde_json::Result<Self> {
470 let tag = serde_json::from_str::<MarketWsTag>(text)?;
471 match tag.event_type {
472 MarketWsEventTag::Book => serde_json::from_str(text).map(Self::Book),
473 MarketWsEventTag::PriceChange => serde_json::from_str(text).map(Self::PriceChange),
474 MarketWsEventTag::LastTradePrice => {
475 serde_json::from_str(text).map(Self::LastTradePrice)
476 }
477 MarketWsEventTag::TickSizeChange => {
478 serde_json::from_str(text).map(Self::TickSizeChange)
479 }
480 MarketWsEventTag::NewMarket => serde_json::from_str(text)
481 .map(Box::new)
482 .map(Self::NewMarket),
483 MarketWsEventTag::MarketResolved => {
484 serde_json::from_str(text).map(Self::MarketResolved)
485 }
486 MarketWsEventTag::BestBidAsk => serde_json::from_str(text).map(Self::BestBidAsk),
487 }
488 }
489}
490
491#[derive(Deserialize)]
492#[serde(rename_all = "snake_case")]
493enum MarketWsEventTag {
494 Book,
495 PriceChange,
496 LastTradePrice,
497 TickSizeChange,
498 NewMarket,
499 MarketResolved,
500 BestBidAsk,
501}
502
503#[derive(Deserialize)]
504struct MarketWsTag {
505 event_type: MarketWsEventTag,
506}
507
508#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
510#[serde(tag = "event_type")]
511pub enum UserWsMessage {
512 #[serde(rename = "order")]
513 Order(PolymarketUserOrder),
514 #[serde(rename = "trade")]
515 Trade(PolymarketUserTrade),
516}
517
518struct UserWsMessageVisitor;
519
520impl<'de> Visitor<'de> for UserWsMessageVisitor {
521 type Value = UserWsMessage;
522
523 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
524 formatter.write_str("a Polymarket user-channel message with event_type first")
525 }
526
527 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
528 where
529 A: MapAccess<'de>,
530 {
531 let Some(key) = map.next_key::<&str>()? else {
532 return Err(serde::de::Error::custom("expected event_type field"));
533 };
534
535 if key != "event_type" {
536 return Err(serde::de::Error::custom(
537 "event_type was not the first field",
538 ));
539 }
540
541 let event_type = map.next_value::<&str>()?;
542 let remaining = MapAccessDeserializer::new(PayloadMapAccess::new(map));
543 match event_type {
544 "order" => PolymarketUserOrder::deserialize(remaining).map(Self::Value::Order),
545 "trade" => PolymarketUserTrade::deserialize(remaining).map(Self::Value::Trade),
546 other => Err(serde::de::Error::unknown_variant(
547 other,
548 &["order", "trade"],
549 )),
550 }
551 }
552}
553
554impl UserWsMessage {
555 pub fn parse(text: &str) -> serde_json::Result<Self> {
561 let mut deserializer = serde_json::Deserializer::from_str(text);
562 serde::Deserializer::deserialize_map(&mut deserializer, UserWsMessageVisitor)
563 .and_then(|message| {
564 deserializer.end()?;
565 Ok(message)
566 })
567 .or_else(|_| Self::parse_reordered(text))
568 .or_else(|_| serde_json::from_str(text))
569 }
570
571 pub fn parse_batch(text: &str) -> serde_json::Result<Vec<Self>> {
580 #[derive(Deserialize)]
582 struct EventTypeTag {
583 event_type: Option<String>,
584 }
585
586 let elements: Vec<&RawValue> = serde_json::from_str(text)?;
589 let mut messages = Vec::with_capacity(elements.len());
590 let mut skipped = 0usize;
591
592 for element in elements {
593 let tag: EventTypeTag = serde_json::from_str(element.get())?;
594 match tag.event_type.as_deref() {
595 Some(event_type) if !matches!(event_type, "order" | "trade") => skipped += 1,
596 _ => messages.push(serde_json::from_str(element.get())?),
597 }
598 }
599
600 if skipped > 0 {
601 log::debug!("Skipped {skipped} user WS message(s) with an unrecognized event_type");
602 }
603
604 Ok(messages)
605 }
606
607 fn parse_reordered(text: &str) -> serde_json::Result<Self> {
608 let tag = serde_json::from_str::<UserWsTag>(text)?;
609 match tag.event_type {
610 UserWsEventTag::Order => serde_json::from_str(text).map(Self::Order),
611 UserWsEventTag::Trade => serde_json::from_str(text).map(Self::Trade),
612 }
613 }
614}
615
616#[derive(Deserialize)]
617#[serde(rename_all = "snake_case")]
618enum UserWsEventTag {
619 Order,
620 Trade,
621}
622
623#[derive(Deserialize)]
624struct UserWsTag {
625 event_type: UserWsEventTag,
626}
627
628#[derive(Debug)]
630pub enum PolymarketWsMessage {
631 Market(MarketWsMessage),
632 User(UserWsMessage),
633 Reconnected,
635}
636
637#[derive(Debug, Serialize)]
639pub struct PolymarketWsAuth {
640 #[serde(rename = "apiKey")]
641 pub api_key: String,
642 pub secret: String,
643 pub passphrase: String,
644}
645
646#[derive(Debug, Serialize)]
652pub struct MarketInitialSubscribeRequest {
653 pub assets_ids: Vec<String>,
654 #[serde(rename = "type")]
655 pub msg_type: &'static str,
656 pub initial_dump: bool,
657 #[serde(skip_serializing_if = "std::ops::Not::not")]
658 pub custom_feature_enabled: bool,
659}
660
661#[derive(Debug, Serialize)]
667pub struct MarketSubscribeRequest {
668 pub assets_ids: Vec<String>,
669 pub operation: &'static str,
670 pub initial_dump: bool,
671 #[serde(skip_serializing_if = "std::ops::Not::not")]
672 pub custom_feature_enabled: bool,
673}
674
675#[derive(Debug, Serialize)]
679pub struct MarketUnsubscribeRequest {
680 pub assets_ids: Vec<String>,
681 pub operation: &'static str,
682}
683
684#[derive(Debug, Serialize)]
688pub struct UserSubscribeRequest {
689 pub auth: PolymarketWsAuth,
690 #[serde(rename = "type")]
691 pub msg_type: &'static str,
692}
693
694#[cfg(test)]
695mod tests {
696 use rstest::rstest;
697
698 use super::*;
699 use crate::common::enums::{
700 PolymarketEventType, PolymarketLiquiditySide, PolymarketOrderSide, PolymarketOrderStatus,
701 PolymarketOrderType, PolymarketOutcome, PolymarketTradeStatus,
702 };
703
704 #[rstest]
705 fn user_subscribe_request_matches_all_markets_wire_format() {
706 let request = UserSubscribeRequest {
707 auth: PolymarketWsAuth {
708 api_key: "fixture-key".to_string(),
709 secret: "fixture-secret".to_string(),
710 passphrase: "fixture-passphrase".to_string(),
711 },
712 msg_type: "user",
713 };
714
715 assert_eq!(
716 serde_json::to_value(request).unwrap(),
717 serde_json::json!({
718 "auth": {
719 "apiKey": "fixture-key",
720 "secret": "fixture-secret",
721 "passphrase": "fixture-passphrase",
722 },
723 "type": "user",
724 }),
725 );
726 }
727
728 fn load<T: serde::de::DeserializeOwned>(filename: &str) -> T {
729 let path = format!("test_data/{filename}");
730 let content = std::fs::read_to_string(path).expect("Failed to read test data");
731 serde_json::from_str(&content).expect("Failed to parse test data")
732 }
733
734 fn load_text(filename: &str) -> String {
735 let path = format!("test_data/{filename}");
736 std::fs::read_to_string(path).expect("Failed to read test data")
737 }
738
739 fn auto_redeem_element() -> serde_json::Value {
742 serde_json::json!({
743 "event_type": "auto_redeem",
744 "proxy_wallet": "0x0000000000000000000000000000000000000000",
745 "txn_hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
746 "amount": "70",
747 "condition_id": "0xb88862256916cb0c82e72667bd43f3e6cfe94cd7d5cda0bf763ec82394021e07",
748 "question": "Bitcoin Up or Down - August 17, 9:35AM-9:40AM ET",
749 "slug": "btc-updown-5m-1786973700",
750 "neg_risk": false,
751 "timestamp": "1786974414920",
752 "position_id": "",
753 "outcome_index": 0,
754 "legs": 0,
755 "owner": "00000000-0000-0000-0000-000000000000",
756 })
757 }
758
759 #[rstest]
760 fn test_book_snapshot() {
761 let snap: PolymarketBookSnapshot = load("ws_book_snapshot.json");
762
763 assert_eq!(
764 snap.asset_id.as_str(),
765 "71321045679252212594626385532706912750332728571942532289631379312455583992563"
766 );
767 assert_eq!(snap.bids.len(), 3);
768 assert_eq!(snap.asks.len(), 3);
769 assert_eq!(snap.bids[0].price, "0.48");
770 assert_eq!(snap.bids[0].size, "500.0");
771 assert_eq!(snap.asks[0].price, "0.53");
772 assert_eq!(snap.timestamp, "1703875200000");
773 assert!(snap.hash.is_none());
774 assert!(snap.min_order_size.is_none());
775 assert!(snap.tick_size.is_none());
776 assert!(snap.neg_risk.is_none());
777 assert!(snap.last_trade_price.is_none());
778 }
779
780 #[rstest]
781 fn test_book_snapshot_roundtrip() {
782 let snap: PolymarketBookSnapshot = load("ws_book_snapshot.json");
783 let json = serde_json::to_string(&snap).unwrap();
784 let snap2: PolymarketBookSnapshot = serde_json::from_str(&json).unwrap();
785 assert_eq!(snap, snap2);
786 }
787
788 #[rstest]
789 fn test_quotes() {
790 let quotes: PolymarketQuotes = load("ws_quotes.json");
791
792 assert_eq!(quotes.price_changes.len(), 2);
793 assert_eq!(quotes.price_changes[0].side, PolymarketOrderSide::Buy);
794 assert_eq!(quotes.price_changes[0].price, "0.51");
795 assert_eq!(quotes.price_changes[0].best_bid.as_deref(), Some("0.51"));
796 assert_eq!(quotes.price_changes[0].best_ask.as_deref(), Some("0.52"));
797 assert_eq!(quotes.price_changes[1].side, PolymarketOrderSide::Sell);
798 assert_eq!(quotes.timestamp, "1703875201000");
799 }
800
801 #[rstest]
802 fn test_last_trade() {
803 let trade: PolymarketTrade = load("ws_last_trade.json");
804
805 assert_eq!(trade.price, "0.51");
806 assert_eq!(trade.size, "25.0");
807 assert_eq!(trade.side, PolymarketOrderSide::Buy);
808 assert_eq!(trade.fee_rate_bps, "0");
809 assert_eq!(trade.timestamp, "1703875202000");
810 assert_eq!(
811 trade.transaction_hash.as_deref(),
812 Some("0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab")
813 );
814 }
815
816 #[rstest]
817 fn test_optional_market_hash_fields_default() {
818 let snap: PolymarketBookSnapshot = load("ws_book_snapshot_missing_hash.json");
819 let trade: PolymarketTrade = load("ws_last_trade_missing_transaction_hash.json");
820
821 assert!(snap.hash.is_none());
822 assert!(snap.min_order_size.is_none());
823 assert!(snap.tick_size.is_none());
824 assert!(snap.neg_risk.is_none());
825 assert!(snap.last_trade_price.is_none());
826 assert!(trade.transaction_hash.is_none());
827 }
828
829 #[rstest]
830 fn test_tick_size_change() {
831 let msg: PolymarketTickSizeChange = load("ws_tick_size_change.json");
832
833 assert_eq!(msg.new_tick_size, "0.01");
834 assert_eq!(msg.old_tick_size, "0.1");
835 assert_eq!(msg.timestamp, "1703875210000");
836 }
837
838 #[rstest]
839 fn test_user_order_placement() {
840 let order: PolymarketUserOrder = load("ws_user_order_placement.json");
841
842 assert_eq!(order.event_type, PolymarketEventType::Placement);
843 assert_eq!(
844 order.status.as_ref().map(|status| status.status),
845 Some(PolymarketOrderStatus::Live)
846 );
847 assert_eq!(order.side, PolymarketOrderSide::Buy);
848 assert_eq!(order.order_type, Some(PolymarketOrderType::GTC));
849 assert_eq!(order.outcome, Some(PolymarketOutcome::yes()));
850 assert_eq!(order.original_size, "100.0");
851 assert_eq!(order.size_matched, "0.0");
852 assert!(order.associate_trades.is_none());
853 assert!(order.expiration.is_none());
854 }
855
856 #[rstest]
857 fn test_user_order_update() {
858 let order: PolymarketUserOrder = load("ws_user_order_update.json");
859
860 assert_eq!(order.event_type, PolymarketEventType::Update);
861 assert_eq!(order.size_matched, "25.0");
862 assert_eq!(
863 order.associate_trades.as_deref(),
864 Some(&["trade-0xabcdef1234".to_string()][..])
865 );
866 }
867
868 #[rstest]
869 fn test_user_order_cancellation() {
870 let order: PolymarketUserOrder = load("ws_user_order_cancellation.json");
871
872 assert_eq!(order.event_type, PolymarketEventType::Cancellation);
873 assert_eq!(
874 order.status.as_ref().map(|status| status.status),
875 Some(PolymarketOrderStatus::Canceled)
876 );
877 assert_eq!(order.size_matched, "0.0");
878 }
879
880 #[rstest]
881 fn test_user_order_status_preserves_rejection_reason() {
882 let raw = "UNMATCHED_invalid post-only order: order crosses book";
883 let status: PolymarketUserOrderStatus =
884 serde_json::from_str(&format!("\"{raw}\"")).unwrap();
885
886 assert_eq!(status.status, PolymarketOrderStatus::Unmatched);
887 assert_eq!(
888 status.reason.as_deref(),
889 Some("invalid post-only order: order crosses book")
890 );
891 assert_eq!(
892 serde_json::to_string(&status).unwrap(),
893 format!("\"{raw}\"")
894 );
895 }
896
897 #[rstest]
901 fn test_user_order_fok_killed() {
902 let msg: UserWsMessage = load("ws_user_order_fok_killed.json");
903
904 let UserWsMessage::Order(order) = msg else {
905 panic!("Expected UserWsMessage::Order");
906 };
907 assert_eq!(order.event_type, PolymarketEventType::Cancellation);
908 assert_eq!(
909 order.status.as_ref().map(|status| status.status),
910 Some(PolymarketOrderStatus::Canceled)
911 );
912 assert_eq!(
913 order
914 .status
915 .as_ref()
916 .and_then(|status| status.reason.as_deref()),
917 Some("order couldn't be fully filled. FOK orders are fully filled or killed.")
918 );
919 assert_eq!(order.order_type, Some(PolymarketOrderType::FOK));
920 assert_eq!(order.size_matched, "");
921 assert_eq!(order.created_at.as_deref(), Some(""));
922 assert_eq!(
923 order.outcome.as_ref().map(PolymarketOutcome::as_str),
924 Some("")
925 );
926 }
927
928 #[rstest]
929 fn test_user_trade() {
930 let trade: PolymarketUserTrade = load("ws_user_trade.json");
931
932 assert_eq!(trade.event_type, PolymarketEventType::Trade);
933 assert_eq!(trade.status, PolymarketTradeStatus::Confirmed);
934 assert_eq!(trade.side, PolymarketOrderSide::Buy);
935 assert_eq!(trade.trader_side, PolymarketLiquiditySide::Taker);
936 assert_eq!(trade.price, "0.5");
937 assert_eq!(trade.size, "25.0");
938 assert_eq!(trade.fee_rate_bps, "0");
939 assert_eq!(trade.bucket_index, 1);
940 assert_eq!(trade.maker_orders.len(), 1);
941 assert_eq!(
942 trade.transaction_hash.as_deref(),
943 Some("0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab")
944 );
945 assert_eq!(
946 trade.taker_order_id,
947 "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12"
948 );
949 }
950
951 #[rstest]
952 fn test_user_trade_missing_transaction_hash() {
953 let mut value: serde_json::Value = load("ws_user_trade.json");
954 value
955 .as_object_mut()
956 .expect("trade fixture should be an object")
957 .remove("transaction_hash");
958
959 let trade: PolymarketUserTrade =
960 serde_json::from_value(value).expect("trade fixture should deserialize");
961
962 assert!(trade.transaction_hash.is_none());
963 }
964
965 #[rstest]
966 fn test_market_ws_message_book() {
967 let msg: MarketWsMessage = load("ws_market_book_msg.json");
968
969 assert!(matches!(msg, MarketWsMessage::Book(_)));
970 if let MarketWsMessage::Book(snap) = msg {
971 assert_eq!(snap.bids.len(), 2);
972 assert_eq!(snap.asks.len(), 2);
973 assert_eq!(snap.timestamp, "1703875200000");
974 }
975 }
976
977 #[rstest]
978 #[case("ws_market_book_msg.json")]
979 #[case("ws_market_price_change_msg.json")]
980 #[case("ws_market_last_trade_msg.json")]
981 #[case("ws_market_tick_size_msg.json")]
982 #[case("ws_market_new_market_msg.json")]
983 #[case("ws_market_resolved_msg.json")]
984 #[case("ws_market_best_bid_ask_msg.json")]
985 fn test_market_ws_message_parse(#[case] filename: &str) {
986 let text = load_text(filename);
987 let expected: MarketWsMessage =
988 serde_json::from_str(&text).expect("market fixture should deserialize");
989
990 let actual = MarketWsMessage::parse(&text).expect("market fixture should parse");
991
992 assert_eq!(actual, expected);
993 }
994
995 #[rstest]
996 fn test_market_ws_message_parse_with_reordered_event_type() {
997 let expected: MarketWsMessage = load("ws_market_book_msg.json");
998 let mut value: serde_json::Value = load("ws_market_book_msg.json");
999 let object = value
1000 .as_object_mut()
1001 .expect("market fixture should be an object");
1002 let event_type = object
1003 .remove("event_type")
1004 .expect("market fixture should contain event_type");
1005 object.insert("event_type".to_string(), event_type);
1006 let text = serde_json::to_string(&value).expect("market fixture should serialize");
1007
1008 assert!(!text.starts_with(r#"{"event_type":"#));
1009 assert_eq!(
1010 MarketWsMessage::parse(&text).expect("reordered market fixture should parse"),
1011 expected
1012 );
1013 }
1014
1015 #[rstest]
1016 fn test_market_ws_message_parse_rejects_duplicate_event_type() {
1017 let text = load_text("ws_market_book_msg.json").replacen(
1018 r#""event_type": "book","#,
1019 r#""event_type": "book", "event_type": "book","#,
1020 1,
1021 );
1022 let expected = serde_json::from_str::<MarketWsMessage>(&text)
1023 .expect_err("derived parser should reject a duplicate event_type");
1024 let actual = MarketWsMessage::parse(&text)
1025 .expect_err("optimized parser should reject a duplicate event_type");
1026
1027 assert_eq!(actual.to_string(), expected.to_string());
1028 }
1029
1030 #[rstest]
1031 fn test_market_ws_message_price_change() {
1032 let msg: MarketWsMessage = load("ws_market_price_change_msg.json");
1033
1034 assert!(matches!(msg, MarketWsMessage::PriceChange(_)));
1035 if let MarketWsMessage::PriceChange(quotes) = msg {
1036 assert_eq!(quotes.price_changes.len(), 1);
1037 }
1038 }
1039
1040 #[rstest]
1041 fn test_market_ws_message_last_trade_price() {
1042 let msg: MarketWsMessage = load("ws_market_last_trade_msg.json");
1043
1044 assert!(matches!(msg, MarketWsMessage::LastTradePrice(_)));
1045 if let MarketWsMessage::LastTradePrice(trade) = msg {
1046 assert_eq!(trade.price, "0.51");
1047 }
1048 }
1049
1050 #[rstest]
1051 fn test_market_ws_message_tick_size_change() {
1052 let msg: MarketWsMessage = load("ws_market_tick_size_msg.json");
1053
1054 assert!(matches!(msg, MarketWsMessage::TickSizeChange(_)));
1055 if let MarketWsMessage::TickSizeChange(change) = msg {
1056 assert_eq!(change.new_tick_size, "0.01");
1057 assert_eq!(change.old_tick_size, "0.1");
1058 }
1059 }
1060
1061 #[rstest]
1062 fn test_user_ws_message_order() {
1063 let msg: UserWsMessage = load("ws_user_order_msg.json");
1064
1065 let UserWsMessage::Order(order) = msg else {
1066 panic!("expected order message");
1067 };
1068 assert_eq!(
1069 order.asset_id.as_str(),
1070 "10000000000000000000000000000000000000000000000000000000000000000000000000001"
1071 );
1072 assert_eq!(order.associate_trades, Some(Vec::new()));
1073 assert_eq!(order.created_at.as_deref(), Some(""));
1074 assert_eq!(order.expiration.as_deref(), Some("0"));
1075 assert_eq!(
1076 order.id,
1077 "0x1111111111111111111111111111111111111111111111111111111111111111"
1078 );
1079 assert_eq!(
1080 order.maker_address.as_ref().map(Ustr::as_str),
1081 Some("0x1111111111111111111111111111111111111111")
1082 );
1083 assert_eq!(
1084 order.market.as_str(),
1085 "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
1086 );
1087 assert_eq!(
1088 order.order_owner.as_ref().map(Ustr::as_str),
1089 Some("11111111-2222-3333-4444-555555555555")
1090 );
1091 assert_eq!(order.order_type, Some(PolymarketOrderType::FOK));
1092 assert_eq!(order.original_size, "1");
1093 assert_eq!(
1094 order.outcome.as_ref().map(PolymarketOutcome::as_str),
1095 Some("")
1096 );
1097 assert_eq!(order.owner.as_str(), "11111111-2222-3333-4444-555555555555");
1098 assert_eq!(order.price, "0.01");
1099 assert_eq!(order.side, PolymarketOrderSide::Buy);
1100 assert_eq!(order.size_matched, "");
1101 assert_eq!(
1102 order.status.as_ref().map(|status| status.status),
1103 Some(PolymarketOrderStatus::Canceled)
1104 );
1105 assert_eq!(order.timestamp, "1786179547007");
1106 assert_eq!(order.event_type, PolymarketEventType::Cancellation);
1107 }
1108
1109 #[rstest]
1110 fn test_user_ws_message_order_optional_fields_absent() {
1111 let json = r#"{
1113 "event_type":"order",
1114 "id":"order-1",
1115 "owner":"owner-1",
1116 "market":"market-1",
1117 "asset_id":"asset-1",
1118 "side":"SELL",
1119 "original_size":"2",
1120 "size_matched":"0",
1121 "price":"0.5",
1122 "type":"PLACEMENT",
1123 "timestamp":"1786179547008"
1124 }"#;
1125 let UserWsMessage::Order(order) = serde_json::from_str(json).unwrap() else {
1126 panic!("expected order message");
1127 };
1128
1129 assert_eq!(order.asset_id.as_str(), "asset-1");
1130 assert!(order.associate_trades.is_none());
1131 assert!(order.created_at.is_none());
1132 assert!(order.expiration.is_none());
1133 assert_eq!(order.id, "order-1");
1134 assert!(order.maker_address.is_none());
1135 assert_eq!(order.market.as_str(), "market-1");
1136 assert!(order.order_owner.is_none());
1137 assert!(order.order_type.is_none());
1138 assert_eq!(order.original_size, "2");
1139 assert!(order.outcome.is_none());
1140 assert_eq!(order.owner.as_str(), "owner-1");
1141 assert_eq!(order.price, "0.5");
1142 assert_eq!(order.side, PolymarketOrderSide::Sell);
1143 assert_eq!(order.size_matched, "0");
1144 assert!(order.status.is_none());
1145 assert_eq!(order.timestamp, "1786179547008");
1146 assert_eq!(order.event_type, PolymarketEventType::Placement);
1147 }
1148
1149 #[rstest]
1150 fn test_user_ws_message_trade() {
1151 let msg: UserWsMessage = load("ws_user_trade_msg.json");
1152
1153 assert!(matches!(msg, UserWsMessage::Trade(_)));
1154 if let UserWsMessage::Trade(trade) = msg {
1155 assert_eq!(trade.event_type, PolymarketEventType::Trade);
1156 assert_eq!(trade.status, PolymarketTradeStatus::Confirmed);
1157 assert!(trade.transaction_hash.is_none());
1158 }
1159 }
1160
1161 #[rstest]
1162 #[case("ws_user_order_msg.json")]
1163 #[case("ws_user_order_fok_killed.json")]
1164 #[case("ws_user_trade_msg.json")]
1165 fn test_user_ws_message_parse(#[case] filename: &str) {
1166 let text = load_text(filename);
1167 let expected: UserWsMessage =
1168 serde_json::from_str(&text).expect("user fixture should deserialize");
1169
1170 let actual = UserWsMessage::parse(&text).expect("user fixture should parse");
1171
1172 assert_eq!(actual, expected);
1173 }
1174
1175 #[rstest]
1176 fn test_user_ws_message_parse_with_reordered_event_type() {
1177 let expected: UserWsMessage = load("ws_user_trade_msg.json");
1178 let mut value: serde_json::Value = load("ws_user_trade_msg.json");
1179 let object = value
1180 .as_object_mut()
1181 .expect("user fixture should be an object");
1182 let event_type = object
1183 .remove("event_type")
1184 .expect("user fixture should contain event_type");
1185 object.insert("event_type".to_string(), event_type);
1186 let text = serde_json::to_string(&value).expect("user fixture should serialize");
1187
1188 assert!(!text.starts_with(r#"{"event_type":"#));
1189 assert_eq!(
1190 UserWsMessage::parse(&text).expect("reordered user fixture should parse"),
1191 expected
1192 );
1193 }
1194
1195 #[rstest]
1196 fn test_user_ws_message_parse_rejects_duplicate_event_type() {
1197 let text = load_text("ws_user_order_msg.json").replacen(
1198 r#""event_type": "order""#,
1199 r#""event_type": "order", "event_type": "order""#,
1200 1,
1201 );
1202 let expected = serde_json::from_str::<UserWsMessage>(&text)
1203 .expect_err("derived parser should reject a duplicate event_type");
1204 let actual = UserWsMessage::parse(&text)
1205 .expect_err("optimized parser should reject a duplicate event_type");
1206
1207 assert_eq!(actual.to_string(), expected.to_string());
1208 }
1209
1210 #[rstest]
1211 fn test_user_ws_message_parse_batch() {
1212 let text = load_text("ws_user_batch_msg.json");
1213 let expected: Vec<UserWsMessage> =
1214 serde_json::from_str(&text).expect("user batch fixture should deserialize");
1215
1216 let actual = UserWsMessage::parse_batch(&text).expect("user batch fixture should parse");
1217
1218 assert_eq!(actual, expected);
1219 }
1220
1221 #[rstest]
1222 fn test_user_ws_message_parse_batch_with_reordered_event_type() {
1223 let expected: Vec<UserWsMessage> = load("ws_user_batch_msg.json");
1224 let mut value: serde_json::Value = load("ws_user_batch_msg.json");
1225 let first = value
1226 .as_array_mut()
1227 .expect("user batch fixture should be an array")[0]
1228 .as_object_mut()
1229 .expect("user batch element should be an object");
1230 let event_type = first
1231 .remove("event_type")
1232 .expect("user batch element should contain event_type");
1233 first.insert("event_type".to_string(), event_type);
1234 let text = serde_json::to_string(&value).expect("user batch fixture should serialize");
1235
1236 assert_eq!(
1237 UserWsMessage::parse_batch(&text).expect("reordered user batch should parse"),
1238 expected
1239 );
1240 }
1241
1242 #[rstest]
1243 fn test_user_ws_message_parse_batch_rejects_invalid_element() {
1244 let mut value: serde_json::Value = load("ws_user_batch_msg.json");
1245 value
1246 .as_array_mut()
1247 .expect("user batch fixture should be an array")[1]
1248 .as_object_mut()
1249 .expect("user batch element should be an object")
1250 .remove("event_type");
1251 let text = serde_json::to_string(&value).expect("user batch fixture should serialize");
1252
1253 assert!(UserWsMessage::parse_batch(&text).is_err());
1254 }
1255
1256 #[rstest]
1259 fn test_user_ws_message_parse_batch_rejects_duplicate_event_type() {
1260 let element = load_text("ws_user_order_msg.json").replacen(
1261 r#""event_type": "order""#,
1262 r#""event_type": "order", "event_type": "order""#,
1263 1,
1264 );
1265 let text = format!("[{element}]");
1266
1267 assert!(UserWsMessage::parse_batch(&text).is_err());
1268 }
1269
1270 #[rstest]
1273 fn test_user_ws_message_parse_batch_skips_unknown_event_type() {
1274 let expected: Vec<UserWsMessage> = load("ws_user_batch_msg.json");
1275 let mut value: serde_json::Value = load("ws_user_batch_msg.json");
1276 let elements = value
1277 .as_array_mut()
1278 .expect("user batch fixture should be an array");
1279 elements.insert(1, auto_redeem_element());
1280 let text = serde_json::to_string(&value).expect("user batch fixture should serialize");
1281
1282 let actual =
1283 UserWsMessage::parse_batch(&text).expect("batch with unknown event_type should parse");
1284
1285 assert_eq!(actual, expected);
1286 }
1287
1288 #[rstest]
1289 fn test_user_ws_message_parse_batch_all_unknown_event_types() {
1290 let text = serde_json::to_string(&serde_json::Value::Array(vec![
1291 auto_redeem_element(),
1292 auto_redeem_element(),
1293 ]))
1294 .expect("unknown batch should serialize");
1295
1296 let actual =
1297 UserWsMessage::parse_batch(&text).expect("batch of unknown event types should parse");
1298
1299 assert!(actual.is_empty());
1300 }
1301
1302 #[rstest]
1303 fn test_market_ws_message_new_market() {
1304 let msg: MarketWsMessage = load("ws_market_new_market_msg.json");
1305 let raw: serde_json::Value = load("ws_market_new_market_msg.json");
1306
1307 let MarketWsMessage::NewMarket(nm) = msg else {
1308 panic!("expected new market message");
1309 };
1310 assert_eq!(nm.id, "market-001");
1311 assert_eq!(
1312 nm.question,
1313 "Map 1 Rounds Handicap: Sangal (-6.5) vs zeste (+6.5)"
1314 );
1315 assert_eq!(
1316 nm.market.as_str(),
1317 "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
1318 );
1319 assert_eq!(nm.slug, "sanitized-new-market");
1320 assert_eq!(nm.description, raw["description"].as_str().unwrap());
1321 assert_eq!(
1322 nm.assets_ids,
1323 vec![
1324 "10000000000000000000000000000000000000000000000000000000000000000000000000001",
1325 "10000000000000000000000000000000000000000000000000000000000000000000000000002",
1326 ]
1327 );
1328 assert_eq!(nm.outcomes, vec!["Sangal", "zeste"]);
1329 assert_eq!(nm.timestamp, "1786179115414");
1330 assert!(nm.tags.is_empty());
1331 assert_eq!(
1332 nm.condition_id,
1333 "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
1334 );
1335 assert!(!nm.active);
1336 assert_eq!(nm.clob_token_ids, nm.assets_ids);
1337 assert_eq!(nm.order_price_min_tick_size.as_deref(), Some("0.01"));
1338 assert_eq!(
1339 nm.group_item_title.as_deref(),
1340 Some("Map 1 Rounds Handicap: Sangal (-6.5) vs zeste (+6.5)")
1341 );
1342 assert_eq!(
1343 nm.sports_market_type.as_deref(),
1344 Some("round_handicap_game_1")
1345 );
1346 assert_eq!(nm.line.as_deref(), Some("-6.5"));
1347 assert_eq!(
1348 nm.game_start_time.as_deref(),
1349 Some("2026-08-08 09:00:00+00")
1350 );
1351 assert_eq!(nm.taker_base_fee, Some(Decimal::from(1000)));
1352 assert_eq!(nm.fees_enabled, Some(true));
1353 let schedule = nm.fee_schedule.as_ref().expect("captured fee schedule");
1354 assert_eq!(schedule.exponent, Decimal::ONE);
1355 assert_eq!(schedule.rate, Decimal::new(5, 2));
1356 assert!(schedule.taker_only);
1357 assert_eq!(schedule.rebate_rate, Decimal::new(15, 2));
1358 let event = nm.event_message.as_ref().expect("captured event metadata");
1359 assert_eq!(event.id, "event-001");
1360 assert_eq!(event.ticker, "sanitized-event");
1361 assert_eq!(event.slug, "sanitized-event");
1362 assert_eq!(
1363 event.title,
1364 "Counter-Strike: Sangal vs zeste (BO3) - Esports World Cup Open Qualifier Group 16"
1365 );
1366 assert_eq!(
1367 event.description,
1368 raw["event_message"]["description"].as_str().unwrap()
1369 );
1370 }
1371
1372 #[rstest]
1373 fn test_market_ws_message_resolved() {
1374 let msg: MarketWsMessage = load("ws_market_resolved_msg.json");
1375
1376 assert!(matches!(msg, MarketWsMessage::MarketResolved(_)));
1377 if let MarketWsMessage::MarketResolved(mr) = msg {
1378 assert_eq!(mr.id, "1031769");
1379 assert_eq!(mr.winning_outcome, "Yes");
1380 assert_eq!(mr.assets_ids.len(), 2);
1381 assert_eq!(
1382 mr.winning_asset_id,
1383 "76043073756653678226373981964075571318267289248134717369284518995922789326425"
1384 );
1385 }
1386 }
1387
1388 #[rstest]
1389 fn test_market_ws_message_best_bid_ask() {
1390 let msg: MarketWsMessage = load("ws_market_best_bid_ask_msg.json");
1391
1392 assert!(matches!(msg, MarketWsMessage::BestBidAsk(_)));
1393 if let MarketWsMessage::BestBidAsk(bba) = msg {
1394 assert_eq!(bba.best_bid, "0.73");
1395 assert_eq!(bba.best_ask, "0.77");
1396 assert_eq!(bba.spread, "0.04");
1397 }
1398 }
1399}