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