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 #[derive(Deserialize)]
585 struct EventTypeTag {
586 event_type: Option<String>,
587 }
588
589 let elements: Vec<&RawValue> = serde_json::from_str(text)?;
592 let mut messages = Vec::with_capacity(elements.len());
593 let mut skipped = 0usize;
594
595 for element in elements {
596 let tag: EventTypeTag = serde_json::from_str(element.get())?;
597 match tag.event_type.as_deref() {
598 Some(event_type) if !matches!(event_type, "order" | "trade") => skipped += 1,
599 _ => messages.push(serde_json::from_str(element.get())?),
600 }
601 }
602
603 if skipped > 0 {
604 log::debug!("Skipped {skipped} user WS message(s) with an unrecognized event_type");
605 }
606
607 Ok(messages)
608 }
609
610 fn parse_reordered(text: &str) -> serde_json::Result<Self> {
611 let tag = serde_json::from_str::<UserWsTag>(text)?;
612 match tag.event_type {
613 UserWsEventTag::Order => serde_json::from_str(text).map(Self::Order),
614 UserWsEventTag::Trade => serde_json::from_str(text).map(Self::Trade),
615 }
616 }
617}
618
619#[derive(Deserialize)]
620#[serde(rename_all = "snake_case")]
621enum UserWsEventTag {
622 Order,
623 Trade,
624}
625
626#[derive(Deserialize)]
627struct UserWsTag {
628 event_type: UserWsEventTag,
629}
630
631#[derive(Debug)]
633pub enum PolymarketWsMessage {
634 Market(MarketWsMessage),
635 User(UserWsMessage),
636 Reconnected {
641 shard_id: Option<usize>,
642 },
643}
644
645#[derive(Debug, Serialize, Zeroize)]
647pub struct PolymarketWsAuth {
648 #[serde(rename = "apiKey")]
649 pub api_key: SecretString,
650 pub secret: SecretString,
651 pub passphrase: SecretString,
652}
653
654#[derive(Debug, Serialize)]
660pub struct MarketInitialSubscribeRequest {
661 pub assets_ids: Vec<String>,
662 #[serde(rename = "type")]
663 pub msg_type: &'static str,
664 pub initial_dump: bool,
665 #[serde(skip_serializing_if = "std::ops::Not::not")]
666 pub custom_feature_enabled: bool,
667}
668
669#[derive(Debug, Serialize)]
675pub struct MarketSubscribeRequest {
676 pub assets_ids: Vec<String>,
677 pub operation: &'static str,
678 pub initial_dump: bool,
679 #[serde(skip_serializing_if = "std::ops::Not::not")]
680 pub custom_feature_enabled: bool,
681}
682
683#[derive(Debug, Serialize)]
687pub struct MarketUnsubscribeRequest {
688 pub assets_ids: Vec<String>,
689 pub operation: &'static str,
690}
691
692#[derive(Debug, Serialize, Zeroize)]
696pub struct UserSubscribeRequest {
697 pub auth: PolymarketWsAuth,
698 #[serde(rename = "type")]
699 #[zeroize(skip)]
700 pub msg_type: &'static str,
701}
702
703#[cfg(test)]
704mod tests {
705 use rstest::rstest;
706
707 use super::*;
708 use crate::common::enums::{
709 PolymarketEventType, PolymarketLiquiditySide, PolymarketOrderSide, PolymarketOrderStatus,
710 PolymarketOrderType, PolymarketOutcome, PolymarketTradeStatus,
711 };
712
713 #[rstest]
714 fn user_subscribe_request_matches_all_markets_wire_format() {
715 let request = UserSubscribeRequest {
716 auth: PolymarketWsAuth {
717 api_key: SecretString::from("fixture-key"),
718 secret: SecretString::from("fixture-secret"),
719 passphrase: SecretString::from("fixture-passphrase"),
720 },
721 msg_type: "user",
722 };
723
724 assert_eq!(
725 serde_json::to_value(request).unwrap(),
726 serde_json::json!({
727 "auth": {
728 "apiKey": "fixture-key",
729 "secret": "fixture-secret",
730 "passphrase": "fixture-passphrase",
731 },
732 "type": "user",
733 }),
734 );
735 }
736
737 fn load<T: serde::de::DeserializeOwned>(filename: &str) -> T {
738 let path = format!("test_data/{filename}");
739 let content = std::fs::read_to_string(path).expect("Failed to read test data");
740 serde_json::from_str(&content).expect("Failed to parse test data")
741 }
742
743 fn load_text(filename: &str) -> String {
744 let path = format!("test_data/{filename}");
745 std::fs::read_to_string(path).expect("Failed to read test data")
746 }
747
748 fn auto_redeem_element() -> serde_json::Value {
751 serde_json::json!({
752 "event_type": "auto_redeem",
753 "proxy_wallet": "0x0000000000000000000000000000000000000000",
754 "txn_hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
755 "amount": "70",
756 "condition_id": "0xb88862256916cb0c82e72667bd43f3e6cfe94cd7d5cda0bf763ec82394021e07",
757 "question": "Bitcoin Up or Down - August 17, 9:35AM-9:40AM ET",
758 "slug": "btc-updown-5m-1786973700",
759 "neg_risk": false,
760 "timestamp": "1786974414920",
761 "position_id": "",
762 "outcome_index": 0,
763 "legs": 0,
764 "owner": "00000000-0000-0000-0000-000000000000",
765 })
766 }
767
768 #[rstest]
769 fn test_book_snapshot() {
770 let snap: PolymarketBookSnapshot = load("ws_book_snapshot.json");
771
772 assert_eq!(
773 snap.asset_id,
774 "71321045679252212594626385532706912750332728571942532289631379312455583992563"
775 );
776 assert_eq!(snap.bids.len(), 3);
777 assert_eq!(snap.asks.len(), 3);
778 assert_eq!(snap.bids[0].price, "0.48");
779 assert_eq!(snap.bids[0].size, "500.0");
780 assert_eq!(snap.asks[0].price, "0.53");
781 assert_eq!(snap.timestamp, "1703875200000");
782 assert!(snap.hash.is_none());
783 assert!(snap.min_order_size.is_none());
784 assert!(snap.tick_size.is_none());
785 assert!(snap.neg_risk.is_none());
786 assert!(snap.last_trade_price.is_none());
787 }
788
789 #[rstest]
790 fn test_book_snapshot_roundtrip() {
791 let snap: PolymarketBookSnapshot = load("ws_book_snapshot.json");
792 let json = serde_json::to_string(&snap).unwrap();
793 let snap2: PolymarketBookSnapshot = serde_json::from_str(&json).unwrap();
794 assert_eq!(snap, snap2);
795 }
796
797 #[rstest]
798 fn test_quotes() {
799 let quotes: PolymarketQuotes = load("ws_quotes.json");
800
801 assert_eq!(quotes.price_changes.len(), 2);
802 assert_eq!(quotes.price_changes[0].side, PolymarketOrderSide::Buy);
803 assert_eq!(quotes.price_changes[0].price, "0.51");
804 assert_eq!(quotes.price_changes[0].best_bid.as_deref(), Some("0.51"));
805 assert_eq!(quotes.price_changes[0].best_ask.as_deref(), Some("0.52"));
806 assert_eq!(quotes.price_changes[1].side, PolymarketOrderSide::Sell);
807 assert_eq!(quotes.timestamp, "1703875201000");
808 }
809
810 #[rstest]
811 fn test_last_trade() {
812 let trade: PolymarketTrade = load("ws_last_trade.json");
813
814 assert_eq!(trade.price, "0.51");
815 assert_eq!(trade.size, "25.0");
816 assert_eq!(trade.side, PolymarketOrderSide::Buy);
817 assert_eq!(trade.fee_rate_bps, "0");
818 assert_eq!(trade.timestamp, "1703875202000");
819 assert_eq!(
820 trade.transaction_hash.as_deref(),
821 Some("0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab")
822 );
823 }
824
825 #[rstest]
826 fn test_optional_market_hash_fields_default() {
827 let snap: PolymarketBookSnapshot = load("ws_book_snapshot_missing_hash.json");
828 let trade: PolymarketTrade = load("ws_last_trade_missing_transaction_hash.json");
829
830 assert!(snap.hash.is_none());
831 assert!(snap.min_order_size.is_none());
832 assert!(snap.tick_size.is_none());
833 assert!(snap.neg_risk.is_none());
834 assert!(snap.last_trade_price.is_none());
835 assert!(trade.transaction_hash.is_none());
836 }
837
838 #[rstest]
839 fn test_tick_size_change() {
840 let msg: PolymarketTickSizeChange = load("ws_tick_size_change.json");
841
842 assert_eq!(msg.new_tick_size, "0.01");
843 assert_eq!(msg.old_tick_size, "0.1");
844 assert_eq!(msg.timestamp, "1703875210000");
845 }
846
847 #[rstest]
848 fn test_user_order_placement() {
849 let order: PolymarketUserOrder = load("ws_user_order_placement.json");
850
851 assert_eq!(order.event_type, PolymarketEventType::Placement);
852 assert_eq!(
853 order.status.as_ref().map(|status| status.status),
854 Some(PolymarketOrderStatus::Live)
855 );
856 assert_eq!(order.side, PolymarketOrderSide::Buy);
857 assert_eq!(order.order_type, Some(PolymarketOrderType::GTC));
858 assert_eq!(order.outcome, Some(PolymarketOutcome::yes()));
859 assert_eq!(order.original_size, "100.0");
860 assert_eq!(order.size_matched, "0.0");
861 assert!(order.associate_trades.is_none());
862 assert!(order.expiration.is_none());
863 }
864
865 #[rstest]
866 fn test_user_order_update() {
867 let order: PolymarketUserOrder = load("ws_user_order_update.json");
868
869 assert_eq!(order.event_type, PolymarketEventType::Update);
870 assert_eq!(order.size_matched, "25.0");
871 assert_eq!(
872 order.associate_trades.as_deref(),
873 Some(&["trade-0xabcdef1234".to_string()][..])
874 );
875 }
876
877 #[rstest]
878 fn test_user_order_cancellation() {
879 let order: PolymarketUserOrder = load("ws_user_order_cancellation.json");
880
881 assert_eq!(order.event_type, PolymarketEventType::Cancellation);
882 assert_eq!(
883 order.status.as_ref().map(|status| status.status),
884 Some(PolymarketOrderStatus::Canceled)
885 );
886 assert_eq!(order.size_matched, "0.0");
887 }
888
889 #[rstest]
890 fn test_user_order_status_preserves_rejection_reason() {
891 let raw = "UNMATCHED_invalid post-only order: order crosses book";
892 let status: PolymarketUserOrderStatus =
893 serde_json::from_str(&format!("\"{raw}\"")).unwrap();
894
895 assert_eq!(status.status, PolymarketOrderStatus::Unmatched);
896 assert_eq!(
897 status.reason.as_deref(),
898 Some("invalid post-only order: order crosses book")
899 );
900 assert_eq!(
901 serde_json::to_string(&status).unwrap(),
902 format!("\"{raw}\"")
903 );
904 }
905
906 #[rstest]
910 fn test_user_order_fok_killed() {
911 let msg: UserWsMessage = load("ws_user_order_fok_killed.json");
912
913 let UserWsMessage::Order(order) = msg else {
914 panic!("Expected UserWsMessage::Order");
915 };
916 assert_eq!(order.event_type, PolymarketEventType::Cancellation);
917 assert_eq!(
918 order.status.as_ref().map(|status| status.status),
919 Some(PolymarketOrderStatus::Canceled)
920 );
921 assert_eq!(
922 order
923 .status
924 .as_ref()
925 .and_then(|status| status.reason.as_deref()),
926 Some("order couldn't be fully filled. FOK orders are fully filled or killed.")
927 );
928 assert_eq!(order.order_type, Some(PolymarketOrderType::FOK));
929 assert_eq!(order.size_matched, "");
930 assert_eq!(order.created_at.as_deref(), Some(""));
931 assert_eq!(
932 order.outcome.as_ref().map(PolymarketOutcome::as_str),
933 Some("")
934 );
935 }
936
937 #[rstest]
938 fn test_user_trade() {
939 let trade: PolymarketUserTrade = load("ws_user_trade.json");
940
941 assert_eq!(trade.event_type, PolymarketEventType::Trade);
942 assert_eq!(trade.status, PolymarketTradeStatus::Confirmed);
943 assert_eq!(trade.side, PolymarketOrderSide::Buy);
944 assert_eq!(trade.trader_side, PolymarketLiquiditySide::Taker);
945 assert_eq!(trade.price, "0.5");
946 assert_eq!(trade.size, "25.0");
947 assert_eq!(trade.fee_rate_bps, "0");
948 assert_eq!(trade.bucket_index, 1);
949 assert_eq!(trade.maker_orders.len(), 1);
950 assert_eq!(
951 trade.transaction_hash.as_deref(),
952 Some("0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab")
953 );
954 assert_eq!(
955 trade.taker_order_id,
956 "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12"
957 );
958 }
959
960 #[rstest]
961 fn test_user_trade_missing_transaction_hash() {
962 let mut value: serde_json::Value = load("ws_user_trade.json");
963 value
964 .as_object_mut()
965 .expect("trade fixture should be an object")
966 .remove("transaction_hash");
967
968 let trade: PolymarketUserTrade =
969 serde_json::from_value(value).expect("trade fixture should deserialize");
970
971 assert!(trade.transaction_hash.is_none());
972 }
973
974 #[rstest]
975 fn test_market_ws_message_book() {
976 let msg: MarketWsMessage = load("ws_market_book_msg.json");
977
978 assert!(matches!(msg, MarketWsMessage::Book(_)));
979 if let MarketWsMessage::Book(snap) = msg {
980 assert_eq!(snap.bids.len(), 2);
981 assert_eq!(snap.asks.len(), 2);
982 assert_eq!(snap.timestamp, "1703875200000");
983 }
984 }
985
986 #[rstest]
987 #[case("ws_market_book_msg.json")]
988 #[case("ws_market_price_change_msg.json")]
989 #[case("ws_market_last_trade_msg.json")]
990 #[case("ws_market_tick_size_msg.json")]
991 #[case("ws_market_new_market_msg.json")]
992 #[case("ws_market_resolved_msg.json")]
993 #[case("ws_market_best_bid_ask_msg.json")]
994 fn test_market_ws_message_parse(#[case] filename: &str) {
995 let text = load_text(filename);
996 let expected: MarketWsMessage =
997 serde_json::from_str(&text).expect("market fixture should deserialize");
998
999 let actual = MarketWsMessage::parse(&text).expect("market fixture should parse");
1000
1001 assert_eq!(actual, expected);
1002 }
1003
1004 #[rstest]
1005 fn test_market_ws_message_parse_with_reordered_event_type() {
1006 let expected: MarketWsMessage = load("ws_market_book_msg.json");
1007 let mut value: serde_json::Value = load("ws_market_book_msg.json");
1008 let object = value
1009 .as_object_mut()
1010 .expect("market fixture should be an object");
1011 let event_type = object
1012 .remove("event_type")
1013 .expect("market fixture should contain event_type");
1014 object.insert("event_type".to_string(), event_type);
1015 let text = serde_json::to_string(&value).expect("market fixture should serialize");
1016
1017 assert!(!text.starts_with(r#"{"event_type":"#));
1018 assert_eq!(
1019 MarketWsMessage::parse(&text).expect("reordered market fixture should parse"),
1020 expected
1021 );
1022 }
1023
1024 #[rstest]
1025 fn test_market_ws_message_parse_rejects_duplicate_event_type() {
1026 let text = load_text("ws_market_book_msg.json").replacen(
1027 r#""event_type": "book","#,
1028 r#""event_type": "book", "event_type": "book","#,
1029 1,
1030 );
1031 let expected = serde_json::from_str::<MarketWsMessage>(&text)
1032 .expect_err("derived parser should reject a duplicate event_type");
1033 let actual = MarketWsMessage::parse(&text)
1034 .expect_err("optimized parser should reject a duplicate event_type");
1035
1036 assert_eq!(actual.to_string(), expected.to_string());
1037 }
1038
1039 #[rstest]
1040 fn test_market_ws_message_price_change() {
1041 let msg: MarketWsMessage = load("ws_market_price_change_msg.json");
1042
1043 assert!(matches!(msg, MarketWsMessage::PriceChange(_)));
1044 if let MarketWsMessage::PriceChange(quotes) = msg {
1045 assert_eq!(quotes.price_changes.len(), 1);
1046 }
1047 }
1048
1049 #[rstest]
1050 fn test_market_ws_message_last_trade_price() {
1051 let msg: MarketWsMessage = load("ws_market_last_trade_msg.json");
1052
1053 assert!(matches!(msg, MarketWsMessage::LastTradePrice(_)));
1054 if let MarketWsMessage::LastTradePrice(trade) = msg {
1055 assert_eq!(trade.price, "0.51");
1056 }
1057 }
1058
1059 #[rstest]
1060 fn test_market_ws_message_tick_size_change() {
1061 let msg: MarketWsMessage = load("ws_market_tick_size_msg.json");
1062
1063 assert!(matches!(msg, MarketWsMessage::TickSizeChange(_)));
1064 if let MarketWsMessage::TickSizeChange(change) = msg {
1065 assert_eq!(change.new_tick_size, "0.01");
1066 assert_eq!(change.old_tick_size, "0.1");
1067 }
1068 }
1069
1070 #[rstest]
1071 fn test_user_ws_message_order() {
1072 let msg: UserWsMessage = load("ws_user_order_msg.json");
1073
1074 let UserWsMessage::Order(order) = msg else {
1075 panic!("expected order message");
1076 };
1077 assert_eq!(
1078 order.asset_id,
1079 "10000000000000000000000000000000000000000000000000000000000000000000000000001"
1080 );
1081 assert_eq!(order.associate_trades, Some(Vec::new()));
1082 assert_eq!(order.created_at.as_deref(), Some(""));
1083 assert_eq!(order.expiration.as_deref(), Some("0"));
1084 assert_eq!(
1085 order.id,
1086 "0x1111111111111111111111111111111111111111111111111111111111111111"
1087 );
1088 assert_eq!(
1089 order.maker_address.as_ref().map(Ustr::as_str),
1090 Some("0x1111111111111111111111111111111111111111")
1091 );
1092 assert_eq!(
1093 order.market,
1094 "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
1095 );
1096 assert_eq!(
1097 order.order_owner.as_ref().map(Ustr::as_str),
1098 Some("11111111-2222-3333-4444-555555555555")
1099 );
1100 assert_eq!(order.order_type, Some(PolymarketOrderType::FOK));
1101 assert_eq!(order.original_size, "1");
1102 assert_eq!(
1103 order.outcome.as_ref().map(PolymarketOutcome::as_str),
1104 Some("")
1105 );
1106 assert_eq!(order.owner, "11111111-2222-3333-4444-555555555555");
1107 assert_eq!(order.price, "0.01");
1108 assert_eq!(order.side, PolymarketOrderSide::Buy);
1109 assert_eq!(order.size_matched, "");
1110 assert_eq!(
1111 order.status.as_ref().map(|status| status.status),
1112 Some(PolymarketOrderStatus::Canceled)
1113 );
1114 assert_eq!(order.timestamp, "1786179547007");
1115 assert_eq!(order.event_type, PolymarketEventType::Cancellation);
1116 }
1117
1118 #[rstest]
1119 fn test_user_ws_message_order_optional_fields_absent() {
1120 let json = r#"{
1122 "event_type":"order",
1123 "id":"order-1",
1124 "owner":"owner-1",
1125 "market":"market-1",
1126 "asset_id":"asset-1",
1127 "side":"SELL",
1128 "original_size":"2",
1129 "size_matched":"0",
1130 "price":"0.5",
1131 "type":"PLACEMENT",
1132 "timestamp":"1786179547008"
1133 }"#;
1134 let UserWsMessage::Order(order) = serde_json::from_str(json).unwrap() else {
1135 panic!("expected order message");
1136 };
1137
1138 assert_eq!(order.asset_id, "asset-1");
1139 assert!(order.associate_trades.is_none());
1140 assert!(order.created_at.is_none());
1141 assert!(order.expiration.is_none());
1142 assert_eq!(order.id, "order-1");
1143 assert!(order.maker_address.is_none());
1144 assert_eq!(order.market, "market-1");
1145 assert!(order.order_owner.is_none());
1146 assert!(order.order_type.is_none());
1147 assert_eq!(order.original_size, "2");
1148 assert!(order.outcome.is_none());
1149 assert_eq!(order.owner, "owner-1");
1150 assert_eq!(order.price, "0.5");
1151 assert_eq!(order.side, PolymarketOrderSide::Sell);
1152 assert_eq!(order.size_matched, "0");
1153 assert!(order.status.is_none());
1154 assert_eq!(order.timestamp, "1786179547008");
1155 assert_eq!(order.event_type, PolymarketEventType::Placement);
1156 }
1157
1158 #[rstest]
1159 fn test_user_ws_message_trade() {
1160 let msg: UserWsMessage = load("ws_user_trade_msg.json");
1161
1162 assert!(matches!(msg, UserWsMessage::Trade(_)));
1163 if let UserWsMessage::Trade(trade) = msg {
1164 assert_eq!(trade.event_type, PolymarketEventType::Trade);
1165 assert_eq!(trade.status, PolymarketTradeStatus::Confirmed);
1166 assert!(trade.transaction_hash.is_none());
1167 }
1168 }
1169
1170 #[rstest]
1171 #[case("ws_user_order_msg.json")]
1172 #[case("ws_user_order_fok_killed.json")]
1173 #[case("ws_user_trade_msg.json")]
1174 fn test_user_ws_message_parse(#[case] filename: &str) {
1175 let text = load_text(filename);
1176 let expected: UserWsMessage =
1177 serde_json::from_str(&text).expect("user fixture should deserialize");
1178
1179 let actual = UserWsMessage::parse(&text).expect("user fixture should parse");
1180
1181 assert_eq!(actual, expected);
1182 }
1183
1184 #[rstest]
1185 fn test_user_ws_message_parse_with_reordered_event_type() {
1186 let expected: UserWsMessage = load("ws_user_trade_msg.json");
1187 let mut value: serde_json::Value = load("ws_user_trade_msg.json");
1188 let object = value
1189 .as_object_mut()
1190 .expect("user fixture should be an object");
1191 let event_type = object
1192 .remove("event_type")
1193 .expect("user fixture should contain event_type");
1194 object.insert("event_type".to_string(), event_type);
1195 let text = serde_json::to_string(&value).expect("user fixture should serialize");
1196
1197 assert!(!text.starts_with(r#"{"event_type":"#));
1198 assert_eq!(
1199 UserWsMessage::parse(&text).expect("reordered user fixture should parse"),
1200 expected
1201 );
1202 }
1203
1204 #[rstest]
1205 fn test_user_ws_message_parse_rejects_duplicate_event_type() {
1206 let text = load_text("ws_user_order_msg.json").replacen(
1207 r#""event_type": "order""#,
1208 r#""event_type": "order", "event_type": "order""#,
1209 1,
1210 );
1211 let expected = serde_json::from_str::<UserWsMessage>(&text)
1212 .expect_err("derived parser should reject a duplicate event_type");
1213 let actual = UserWsMessage::parse(&text)
1214 .expect_err("optimized parser should reject a duplicate event_type");
1215
1216 assert_eq!(actual.to_string(), expected.to_string());
1217 }
1218
1219 #[rstest]
1220 fn test_user_ws_message_parse_batch() {
1221 let text = load_text("ws_user_batch_msg.json");
1222 let expected: Vec<UserWsMessage> =
1223 serde_json::from_str(&text).expect("user batch fixture should deserialize");
1224
1225 let actual = UserWsMessage::parse_batch(&text).expect("user batch fixture should parse");
1226
1227 assert_eq!(actual, expected);
1228 }
1229
1230 #[rstest]
1231 fn test_user_ws_message_parse_batch_with_reordered_event_type() {
1232 let expected: Vec<UserWsMessage> = load("ws_user_batch_msg.json");
1233 let mut value: serde_json::Value = load("ws_user_batch_msg.json");
1234 let first = value
1235 .as_array_mut()
1236 .expect("user batch fixture should be an array")[0]
1237 .as_object_mut()
1238 .expect("user batch element should be an object");
1239 let event_type = first
1240 .remove("event_type")
1241 .expect("user batch element should contain event_type");
1242 first.insert("event_type".to_string(), event_type);
1243 let text = serde_json::to_string(&value).expect("user batch fixture should serialize");
1244
1245 assert_eq!(
1246 UserWsMessage::parse_batch(&text).expect("reordered user batch should parse"),
1247 expected
1248 );
1249 }
1250
1251 #[rstest]
1252 fn test_user_ws_message_parse_batch_rejects_invalid_element() {
1253 let mut value: serde_json::Value = load("ws_user_batch_msg.json");
1254 value
1255 .as_array_mut()
1256 .expect("user batch fixture should be an array")[1]
1257 .as_object_mut()
1258 .expect("user batch element should be an object")
1259 .remove("event_type");
1260 let text = serde_json::to_string(&value).expect("user batch fixture should serialize");
1261
1262 assert!(UserWsMessage::parse_batch(&text).is_err());
1263 }
1264
1265 #[rstest]
1268 fn test_user_ws_message_parse_batch_rejects_duplicate_event_type() {
1269 let element = load_text("ws_user_order_msg.json").replacen(
1270 r#""event_type": "order""#,
1271 r#""event_type": "order", "event_type": "order""#,
1272 1,
1273 );
1274 let text = format!("[{element}]");
1275
1276 assert!(UserWsMessage::parse_batch(&text).is_err());
1277 }
1278
1279 #[rstest]
1282 fn test_user_ws_message_parse_batch_skips_unknown_event_type() {
1283 let expected: Vec<UserWsMessage> = load("ws_user_batch_msg.json");
1284 let mut value: serde_json::Value = load("ws_user_batch_msg.json");
1285 let elements = value
1286 .as_array_mut()
1287 .expect("user batch fixture should be an array");
1288 elements.insert(1, auto_redeem_element());
1289 let text = serde_json::to_string(&value).expect("user batch fixture should serialize");
1290
1291 let actual =
1292 UserWsMessage::parse_batch(&text).expect("batch with unknown event_type should parse");
1293
1294 assert_eq!(actual, expected);
1295 }
1296
1297 #[rstest]
1298 fn test_user_ws_message_parse_batch_all_unknown_event_types() {
1299 let text = serde_json::to_string(&serde_json::Value::Array(vec![
1300 auto_redeem_element(),
1301 auto_redeem_element(),
1302 ]))
1303 .expect("unknown batch should serialize");
1304
1305 let actual =
1306 UserWsMessage::parse_batch(&text).expect("batch of unknown event types should parse");
1307
1308 assert!(actual.is_empty());
1309 }
1310
1311 #[rstest]
1312 fn test_market_ws_message_new_market() {
1313 let msg: MarketWsMessage = load("ws_market_new_market_msg.json");
1314 let raw: serde_json::Value = load("ws_market_new_market_msg.json");
1315
1316 let MarketWsMessage::NewMarket(nm) = msg else {
1317 panic!("expected new market message");
1318 };
1319 assert_eq!(nm.id, "market-001");
1320 assert_eq!(
1321 nm.question,
1322 "Map 1 Rounds Handicap: Sangal (-6.5) vs zeste (+6.5)"
1323 );
1324 assert_eq!(
1325 nm.market,
1326 "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
1327 );
1328 assert_eq!(nm.slug, "sanitized-new-market");
1329 assert_eq!(nm.description, raw["description"].as_str().unwrap());
1330 assert_eq!(
1331 nm.assets_ids,
1332 vec![
1333 "10000000000000000000000000000000000000000000000000000000000000000000000000001",
1334 "10000000000000000000000000000000000000000000000000000000000000000000000000002",
1335 ]
1336 );
1337 assert_eq!(nm.outcomes, vec!["Sangal", "zeste"]);
1338 assert_eq!(nm.timestamp, "1786179115414");
1339 assert!(nm.tags.is_empty());
1340 assert_eq!(
1341 nm.condition_id,
1342 "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
1343 );
1344 assert!(!nm.active);
1345 assert_eq!(nm.clob_token_ids, nm.assets_ids);
1346 assert_eq!(nm.order_price_min_tick_size.as_deref(), Some("0.01"));
1347 assert_eq!(
1348 nm.group_item_title.as_deref(),
1349 Some("Map 1 Rounds Handicap: Sangal (-6.5) vs zeste (+6.5)")
1350 );
1351 assert_eq!(
1352 nm.sports_market_type.as_deref(),
1353 Some("round_handicap_game_1")
1354 );
1355 assert_eq!(nm.line.as_deref(), Some("-6.5"));
1356 assert_eq!(
1357 nm.game_start_time.as_deref(),
1358 Some("2026-08-08 09:00:00+00")
1359 );
1360 assert_eq!(nm.taker_base_fee, Some(Decimal::from(1000)));
1361 assert_eq!(nm.fees_enabled, Some(true));
1362 let schedule = nm.fee_schedule.as_ref().expect("captured fee schedule");
1363 assert_eq!(schedule.exponent, Decimal::ONE);
1364 assert_eq!(schedule.rate, Decimal::new(5, 2));
1365 assert!(schedule.taker_only);
1366 assert_eq!(schedule.rebate_rate, Decimal::new(15, 2));
1367 let event = nm.event_message.as_ref().expect("captured event metadata");
1368 assert_eq!(event.id, "event-001");
1369 assert_eq!(event.ticker, "sanitized-event");
1370 assert_eq!(event.slug, "sanitized-event");
1371 assert_eq!(
1372 event.title,
1373 "Counter-Strike: Sangal vs zeste (BO3) - Esports World Cup Open Qualifier Group 16"
1374 );
1375 assert_eq!(
1376 event.description,
1377 raw["event_message"]["description"].as_str().unwrap()
1378 );
1379 }
1380
1381 #[rstest]
1382 fn test_market_ws_message_resolved() {
1383 let msg: MarketWsMessage = load("ws_market_resolved_msg.json");
1384
1385 assert!(matches!(msg, MarketWsMessage::MarketResolved(_)));
1386 if let MarketWsMessage::MarketResolved(mr) = msg {
1387 assert_eq!(mr.id, "1031769");
1388 assert_eq!(mr.winning_outcome, "Yes");
1389 assert_eq!(mr.assets_ids.len(), 2);
1390 assert_eq!(
1391 mr.winning_asset_id,
1392 "76043073756653678226373981964075571318267289248134717369284518995922789326425"
1393 );
1394 }
1395 }
1396
1397 #[rstest]
1398 fn test_market_ws_message_best_bid_ask() {
1399 let msg: MarketWsMessage = load("ws_market_best_bid_ask_msg.json");
1400
1401 assert!(matches!(msg, MarketWsMessage::BestBidAsk(_)));
1402 if let MarketWsMessage::BestBidAsk(bba) = msg {
1403 assert_eq!(bba.best_bid, "0.73");
1404 assert_eq!(bba.best_ask, "0.77");
1405 assert_eq!(bba.spread, "0.04");
1406 }
1407 }
1408}