1use serde::{Deserialize, Serialize};
19use ustr::Ustr;
20
21use crate::common::{
22 enums::{
23 PolymarketEventType, PolymarketLiquiditySide, PolymarketOrderSide, PolymarketOrderStatus,
24 PolymarketOrderType, PolymarketOutcome, PolymarketTradeStatus,
25 },
26 models::PolymarketMakerOrder,
27};
28
29#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
33pub struct PolymarketUserOrder {
34 pub asset_id: Ustr,
35 pub associate_trades: Option<Vec<String>>,
36 pub created_at: String,
37 pub expiration: Option<String>,
38 pub id: String,
39 pub maker_address: Ustr,
40 pub market: Ustr,
41 pub order_owner: Ustr,
42 pub order_type: PolymarketOrderType,
43 pub original_size: String,
44 pub outcome: PolymarketOutcome,
45 pub owner: Ustr,
46 pub price: String,
47 pub side: PolymarketOrderSide,
48 pub size_matched: String,
49 pub status: PolymarketOrderStatus,
50 pub timestamp: String,
51 #[serde(rename = "type")]
52 pub event_type: PolymarketEventType,
53}
54
55#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
59pub struct PolymarketUserTrade {
60 pub asset_id: Ustr,
61 pub bucket_index: u64,
62 pub fee_rate_bps: String,
63 pub id: String,
64 pub last_update: String,
65 pub maker_address: Ustr,
66 pub maker_orders: Vec<PolymarketMakerOrder>,
67 pub market: Ustr,
68 pub match_time: String,
69 pub outcome: PolymarketOutcome,
70 pub owner: Ustr,
71 pub price: String,
72 pub side: PolymarketOrderSide,
73 pub size: String,
74 pub status: PolymarketTradeStatus,
75 pub taker_order_id: String,
76 pub timestamp: String,
77 pub trade_owner: Ustr,
78 pub trader_side: PolymarketLiquiditySide,
79 #[serde(rename = "type")]
80 pub event_type: PolymarketEventType,
81}
82
83#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
85pub struct PolymarketBookLevel {
86 pub price: String,
87 pub size: String,
88}
89
90#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
92pub struct PolymarketBookSnapshot {
93 pub market: Ustr,
94 pub asset_id: Ustr,
95 pub bids: Vec<PolymarketBookLevel>,
96 pub asks: Vec<PolymarketBookLevel>,
97 pub timestamp: String,
98}
99
100#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
102pub struct PolymarketQuote {
103 pub asset_id: Ustr,
104 pub price: String,
105 pub side: PolymarketOrderSide,
106 pub size: String,
107 pub hash: String,
108 #[serde(default)]
109 pub best_bid: Option<String>,
110 #[serde(default)]
111 pub best_ask: Option<String>,
112}
113
114#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
116pub struct PolymarketQuotes {
117 pub market: Ustr,
118 pub price_changes: Vec<PolymarketQuote>,
119 pub timestamp: String,
120}
121
122#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
124pub struct PolymarketTrade {
125 pub market: Ustr,
126 pub asset_id: Ustr,
127 pub fee_rate_bps: String,
128 pub price: String,
129 pub side: PolymarketOrderSide,
130 pub size: String,
131 pub timestamp: String,
132}
133
134#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
136pub struct PolymarketTickSizeChange {
137 pub market: Ustr,
138 pub asset_id: Ustr,
139 pub new_tick_size: String,
140 pub old_tick_size: String,
141 pub timestamp: String,
142}
143
144#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
146pub struct PolymarketNewMarketEvent {
147 pub id: String,
148 pub ticker: String,
149 pub slug: String,
150 pub title: String,
151 pub description: String,
152}
153
154#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
158pub struct PolymarketNewMarket {
159 pub id: String,
160 pub question: String,
161 pub market: Ustr,
162 pub slug: String,
163 pub description: String,
164 pub assets_ids: Vec<String>,
165 pub outcomes: Vec<String>,
166 pub timestamp: String,
167 pub tags: Vec<String>,
168 pub condition_id: String,
169 pub active: bool,
170 pub clob_token_ids: Vec<String>,
171 #[serde(default)]
172 pub order_price_min_tick_size: Option<String>,
173 #[serde(default)]
174 pub group_item_title: Option<String>,
175 #[serde(default)]
176 pub event_message: Option<PolymarketNewMarketEvent>,
177}
178
179#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
183pub struct PolymarketMarketResolved {
184 pub id: String,
185 pub market: Ustr,
186 pub assets_ids: Vec<String>,
187 pub winning_asset_id: String,
188 pub winning_outcome: String,
189 pub timestamp: String,
190 pub tags: Vec<String>,
191}
192
193#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
198pub struct PolymarketBestBidAsk {
199 pub market: Ustr,
200 pub asset_id: Ustr,
201 pub best_bid: String,
202 pub best_ask: String,
203 pub spread: String,
204 pub timestamp: String,
205}
206
207#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
209#[serde(tag = "event_type")]
210pub enum MarketWsMessage {
211 #[serde(rename = "book")]
212 Book(PolymarketBookSnapshot),
213 #[serde(rename = "price_change")]
214 PriceChange(PolymarketQuotes),
215 #[serde(rename = "last_trade_price")]
216 LastTradePrice(PolymarketTrade),
217 #[serde(rename = "tick_size_change")]
218 TickSizeChange(PolymarketTickSizeChange),
219 #[serde(rename = "new_market")]
220 NewMarket(Box<PolymarketNewMarket>),
221 #[serde(rename = "market_resolved")]
222 MarketResolved(PolymarketMarketResolved),
223 #[serde(rename = "best_bid_ask")]
224 BestBidAsk(PolymarketBestBidAsk),
225}
226
227#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
229#[serde(tag = "event_type")]
230pub enum UserWsMessage {
231 #[serde(rename = "order")]
232 Order(PolymarketUserOrder),
233 #[serde(rename = "trade")]
234 Trade(PolymarketUserTrade),
235}
236
237#[derive(Debug)]
239pub enum PolymarketWsMessage {
240 Market(MarketWsMessage),
241 User(UserWsMessage),
242 Reconnected,
244}
245
246#[derive(Debug, Serialize)]
248pub struct PolymarketWsAuth {
249 #[serde(rename = "apiKey")]
250 pub api_key: String,
251 pub secret: String,
252 pub passphrase: String,
253}
254
255#[derive(Debug, Serialize)]
260pub struct MarketInitialSubscribeRequest {
261 pub assets_ids: Vec<String>,
262 #[serde(rename = "type")]
263 pub msg_type: &'static str,
264 #[serde(skip_serializing_if = "std::ops::Not::not")]
265 pub custom_feature_enabled: bool,
266}
267
268#[derive(Debug, Serialize)]
273pub struct MarketSubscribeRequest {
274 pub assets_ids: Vec<String>,
275 pub operation: &'static str,
276 #[serde(skip_serializing_if = "std::ops::Not::not")]
277 pub custom_feature_enabled: bool,
278}
279
280#[derive(Debug, Serialize)]
284pub struct MarketUnsubscribeRequest {
285 pub assets_ids: Vec<String>,
286 pub operation: &'static str,
287}
288
289#[derive(Debug, Serialize)]
293pub struct UserSubscribeRequest {
294 pub auth: PolymarketWsAuth,
295 pub markets: Vec<String>,
296 pub assets_ids: Vec<String>,
297 #[serde(rename = "type")]
298 pub msg_type: &'static str,
299}
300
301#[cfg(test)]
302mod tests {
303 use rstest::rstest;
304
305 use super::*;
306 use crate::common::enums::{
307 PolymarketEventType, PolymarketLiquiditySide, PolymarketOrderSide, PolymarketOrderStatus,
308 PolymarketOrderType, PolymarketOutcome, PolymarketTradeStatus,
309 };
310
311 fn load<T: serde::de::DeserializeOwned>(filename: &str) -> T {
312 let path = format!("test_data/{filename}");
313 let content = std::fs::read_to_string(path).expect("Failed to read test data");
314 serde_json::from_str(&content).expect("Failed to parse test data")
315 }
316
317 #[rstest]
318 fn test_book_snapshot() {
319 let snap: PolymarketBookSnapshot = load("ws_book_snapshot.json");
320
321 assert_eq!(
322 snap.asset_id.as_str(),
323 "71321045679252212594626385532706912750332728571942532289631379312455583992563"
324 );
325 assert_eq!(snap.bids.len(), 3);
326 assert_eq!(snap.asks.len(), 3);
327 assert_eq!(snap.bids[0].price, "0.48");
328 assert_eq!(snap.bids[0].size, "500.0");
329 assert_eq!(snap.asks[0].price, "0.53");
330 assert_eq!(snap.timestamp, "1703875200000");
331 }
332
333 #[rstest]
334 fn test_book_snapshot_roundtrip() {
335 let snap: PolymarketBookSnapshot = load("ws_book_snapshot.json");
336 let json = serde_json::to_string(&snap).unwrap();
337 let snap2: PolymarketBookSnapshot = serde_json::from_str(&json).unwrap();
338 assert_eq!(snap, snap2);
339 }
340
341 #[rstest]
342 fn test_quotes() {
343 let quotes: PolymarketQuotes = load("ws_quotes.json");
344
345 assert_eq!(quotes.price_changes.len(), 2);
346 assert_eq!(quotes.price_changes[0].side, PolymarketOrderSide::Buy);
347 assert_eq!(quotes.price_changes[0].price, "0.51");
348 assert_eq!(quotes.price_changes[0].best_bid.as_deref(), Some("0.51"));
349 assert_eq!(quotes.price_changes[0].best_ask.as_deref(), Some("0.52"));
350 assert_eq!(quotes.price_changes[1].side, PolymarketOrderSide::Sell);
351 assert_eq!(quotes.timestamp, "1703875201000");
352 }
353
354 #[rstest]
355 fn test_last_trade() {
356 let trade: PolymarketTrade = load("ws_last_trade.json");
357
358 assert_eq!(trade.price, "0.51");
359 assert_eq!(trade.size, "25.0");
360 assert_eq!(trade.side, PolymarketOrderSide::Buy);
361 assert_eq!(trade.fee_rate_bps, "0");
362 assert_eq!(trade.timestamp, "1703875202000");
363 }
364
365 #[rstest]
366 fn test_tick_size_change() {
367 let msg: PolymarketTickSizeChange = load("ws_tick_size_change.json");
368
369 assert_eq!(msg.new_tick_size, "0.01");
370 assert_eq!(msg.old_tick_size, "0.1");
371 assert_eq!(msg.timestamp, "1703875210000");
372 }
373
374 #[rstest]
375 fn test_user_order_placement() {
376 let order: PolymarketUserOrder = load("ws_user_order_placement.json");
377
378 assert_eq!(order.event_type, PolymarketEventType::Placement);
379 assert_eq!(order.status, PolymarketOrderStatus::Live);
380 assert_eq!(order.side, PolymarketOrderSide::Buy);
381 assert_eq!(order.order_type, PolymarketOrderType::GTC);
382 assert_eq!(order.outcome, PolymarketOutcome::yes());
383 assert_eq!(order.original_size, "100.0");
384 assert_eq!(order.size_matched, "0.0");
385 assert!(order.associate_trades.is_none());
386 assert!(order.expiration.is_none());
387 }
388
389 #[rstest]
390 fn test_user_order_update() {
391 let order: PolymarketUserOrder = load("ws_user_order_update.json");
392
393 assert_eq!(order.event_type, PolymarketEventType::Update);
394 assert_eq!(order.size_matched, "25.0");
395 assert_eq!(
396 order.associate_trades.as_deref(),
397 Some(&["trade-0xabcdef1234".to_string()][..])
398 );
399 }
400
401 #[rstest]
402 fn test_user_order_cancellation() {
403 let order: PolymarketUserOrder = load("ws_user_order_cancellation.json");
404
405 assert_eq!(order.event_type, PolymarketEventType::Cancellation);
406 assert_eq!(order.status, PolymarketOrderStatus::Canceled);
407 assert_eq!(order.size_matched, "0.0");
408 }
409
410 #[rstest]
414 fn test_user_order_fok_killed() {
415 let msg: UserWsMessage = load("ws_user_order_fok_killed.json");
416
417 let UserWsMessage::Order(order) = msg else {
418 panic!("Expected UserWsMessage::Order");
419 };
420 assert_eq!(order.event_type, PolymarketEventType::Cancellation);
421 assert_eq!(order.status, PolymarketOrderStatus::Canceled);
422 assert_eq!(order.order_type, PolymarketOrderType::FOK);
423 assert_eq!(order.size_matched, "");
424 assert_eq!(order.created_at, "");
425 assert_eq!(order.outcome.as_str(), "");
426 }
427
428 #[rstest]
429 fn test_user_trade() {
430 let trade: PolymarketUserTrade = load("ws_user_trade.json");
431
432 assert_eq!(trade.event_type, PolymarketEventType::Trade);
433 assert_eq!(trade.status, PolymarketTradeStatus::Confirmed);
434 assert_eq!(trade.side, PolymarketOrderSide::Buy);
435 assert_eq!(trade.trader_side, PolymarketLiquiditySide::Taker);
436 assert_eq!(trade.price, "0.5");
437 assert_eq!(trade.size, "25.0");
438 assert_eq!(trade.fee_rate_bps, "0");
439 assert_eq!(trade.bucket_index, 1);
440 assert_eq!(trade.maker_orders.len(), 1);
441 assert_eq!(
442 trade.taker_order_id,
443 "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12"
444 );
445 }
446
447 #[rstest]
448 fn test_market_ws_message_book() {
449 let msg: MarketWsMessage = load("ws_market_book_msg.json");
450
451 assert!(matches!(msg, MarketWsMessage::Book(_)));
452 if let MarketWsMessage::Book(snap) = msg {
453 assert_eq!(snap.bids.len(), 2);
454 assert_eq!(snap.asks.len(), 2);
455 assert_eq!(snap.timestamp, "1703875200000");
456 }
457 }
458
459 #[rstest]
460 fn test_market_ws_message_price_change() {
461 let msg: MarketWsMessage = load("ws_market_price_change_msg.json");
462
463 assert!(matches!(msg, MarketWsMessage::PriceChange(_)));
464 if let MarketWsMessage::PriceChange(quotes) = msg {
465 assert_eq!(quotes.price_changes.len(), 1);
466 }
467 }
468
469 #[rstest]
470 fn test_market_ws_message_last_trade_price() {
471 let msg: MarketWsMessage = load("ws_market_last_trade_msg.json");
472
473 assert!(matches!(msg, MarketWsMessage::LastTradePrice(_)));
474 if let MarketWsMessage::LastTradePrice(trade) = msg {
475 assert_eq!(trade.price, "0.51");
476 }
477 }
478
479 #[rstest]
480 fn test_market_ws_message_tick_size_change() {
481 let msg: MarketWsMessage = load("ws_market_tick_size_msg.json");
482
483 assert!(matches!(msg, MarketWsMessage::TickSizeChange(_)));
484 if let MarketWsMessage::TickSizeChange(change) = msg {
485 assert_eq!(change.new_tick_size, "0.01");
486 assert_eq!(change.old_tick_size, "0.1");
487 }
488 }
489
490 #[rstest]
491 fn test_user_ws_message_order() {
492 let msg: UserWsMessage = load("ws_user_order_msg.json");
493
494 assert!(matches!(msg, UserWsMessage::Order(_)));
495 if let UserWsMessage::Order(order) = msg {
496 assert_eq!(order.event_type, PolymarketEventType::Placement);
497 assert_eq!(order.side, PolymarketOrderSide::Buy);
498 }
499 }
500
501 #[rstest]
502 fn test_user_ws_message_trade() {
503 let msg: UserWsMessage = load("ws_user_trade_msg.json");
504
505 assert!(matches!(msg, UserWsMessage::Trade(_)));
506 if let UserWsMessage::Trade(trade) = msg {
507 assert_eq!(trade.event_type, PolymarketEventType::Trade);
508 assert_eq!(trade.status, PolymarketTradeStatus::Confirmed);
509 }
510 }
511
512 #[rstest]
513 fn test_market_ws_message_new_market() {
514 let msg: MarketWsMessage = load("ws_market_new_market_msg.json");
515
516 assert!(matches!(msg, MarketWsMessage::NewMarket(_)));
517 if let MarketWsMessage::NewMarket(nm) = msg {
518 assert_eq!(nm.id, "1031769");
519 assert_eq!(nm.slug, "nvda-above-240-on-january-30-2026");
520 assert_eq!(
521 nm.market.as_str(),
522 "0x311d0c4b6671ab54af4970c06fcf58662516f5168997bdda209ec3db5aa6b0c1"
523 );
524 assert_eq!(
525 nm.condition_id,
526 "0x311d0c4b6671ab54af4970c06fcf58662516f5168997bdda209ec3db5aa6b0c1"
527 );
528 assert!(nm.active);
529 assert_eq!(nm.outcomes.len(), 2);
530 assert_eq!(nm.clob_token_ids.len(), 2);
531 assert_eq!(nm.order_price_min_tick_size.as_deref(), Some("0.01"));
532
533 let event = nm
534 .event_message
535 .as_ref()
536 .expect("event_message should be parsed");
537 assert_eq!(event.id, "125819");
538 assert_eq!(event.ticker, "nvda-above-in-january-2026");
539 assert_eq!(event.slug, "nvda-above-in-january-2026");
540 assert_eq!(
541 event.title,
542 "Will NVIDIA (NVDA) close above ___ end of January?"
543 );
544 }
545 }
546
547 #[rstest]
548 fn test_market_ws_message_resolved() {
549 let msg: MarketWsMessage = load("ws_market_resolved_msg.json");
550
551 assert!(matches!(msg, MarketWsMessage::MarketResolved(_)));
552 if let MarketWsMessage::MarketResolved(mr) = msg {
553 assert_eq!(mr.id, "1031769");
554 assert_eq!(mr.winning_outcome, "Yes");
555 assert_eq!(mr.assets_ids.len(), 2);
556 assert_eq!(
557 mr.winning_asset_id,
558 "76043073756653678226373981964075571318267289248134717369284518995922789326425"
559 );
560 }
561 }
562
563 #[rstest]
564 fn test_market_ws_message_best_bid_ask() {
565 let msg: MarketWsMessage = load("ws_market_best_bid_ask_msg.json");
566
567 assert!(matches!(msg, MarketWsMessage::BestBidAsk(_)));
568 if let MarketWsMessage::BestBidAsk(bba) = msg {
569 assert_eq!(bba.best_bid, "0.73");
570 assert_eq!(bba.best_ask, "0.77");
571 assert_eq!(bba.spread, "0.04");
572 }
573 }
574}