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]
411 fn test_user_trade() {
412 let trade: PolymarketUserTrade = load("ws_user_trade.json");
413
414 assert_eq!(trade.event_type, PolymarketEventType::Trade);
415 assert_eq!(trade.status, PolymarketTradeStatus::Confirmed);
416 assert_eq!(trade.side, PolymarketOrderSide::Buy);
417 assert_eq!(trade.trader_side, PolymarketLiquiditySide::Taker);
418 assert_eq!(trade.price, "0.5");
419 assert_eq!(trade.size, "25.0");
420 assert_eq!(trade.fee_rate_bps, "0");
421 assert_eq!(trade.bucket_index, 1);
422 assert_eq!(trade.maker_orders.len(), 1);
423 assert_eq!(
424 trade.taker_order_id,
425 "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12"
426 );
427 }
428
429 #[rstest]
430 fn test_market_ws_message_book() {
431 let msg: MarketWsMessage = load("ws_market_book_msg.json");
432
433 assert!(matches!(msg, MarketWsMessage::Book(_)));
434 if let MarketWsMessage::Book(snap) = msg {
435 assert_eq!(snap.bids.len(), 2);
436 assert_eq!(snap.asks.len(), 2);
437 assert_eq!(snap.timestamp, "1703875200000");
438 }
439 }
440
441 #[rstest]
442 fn test_market_ws_message_price_change() {
443 let msg: MarketWsMessage = load("ws_market_price_change_msg.json");
444
445 assert!(matches!(msg, MarketWsMessage::PriceChange(_)));
446 if let MarketWsMessage::PriceChange(quotes) = msg {
447 assert_eq!(quotes.price_changes.len(), 1);
448 }
449 }
450
451 #[rstest]
452 fn test_market_ws_message_last_trade_price() {
453 let msg: MarketWsMessage = load("ws_market_last_trade_msg.json");
454
455 assert!(matches!(msg, MarketWsMessage::LastTradePrice(_)));
456 if let MarketWsMessage::LastTradePrice(trade) = msg {
457 assert_eq!(trade.price, "0.51");
458 }
459 }
460
461 #[rstest]
462 fn test_market_ws_message_tick_size_change() {
463 let msg: MarketWsMessage = load("ws_market_tick_size_msg.json");
464
465 assert!(matches!(msg, MarketWsMessage::TickSizeChange(_)));
466 if let MarketWsMessage::TickSizeChange(change) = msg {
467 assert_eq!(change.new_tick_size, "0.01");
468 assert_eq!(change.old_tick_size, "0.1");
469 }
470 }
471
472 #[rstest]
473 fn test_user_ws_message_order() {
474 let msg: UserWsMessage = load("ws_user_order_msg.json");
475
476 assert!(matches!(msg, UserWsMessage::Order(_)));
477 if let UserWsMessage::Order(order) = msg {
478 assert_eq!(order.event_type, PolymarketEventType::Placement);
479 assert_eq!(order.side, PolymarketOrderSide::Buy);
480 }
481 }
482
483 #[rstest]
484 fn test_user_ws_message_trade() {
485 let msg: UserWsMessage = load("ws_user_trade_msg.json");
486
487 assert!(matches!(msg, UserWsMessage::Trade(_)));
488 if let UserWsMessage::Trade(trade) = msg {
489 assert_eq!(trade.event_type, PolymarketEventType::Trade);
490 assert_eq!(trade.status, PolymarketTradeStatus::Confirmed);
491 }
492 }
493
494 #[rstest]
495 fn test_market_ws_message_new_market() {
496 let msg: MarketWsMessage = load("ws_market_new_market_msg.json");
497
498 assert!(matches!(msg, MarketWsMessage::NewMarket(_)));
499 if let MarketWsMessage::NewMarket(nm) = msg {
500 assert_eq!(nm.id, "1031769");
501 assert_eq!(nm.slug, "nvda-above-240-on-january-30-2026");
502 assert_eq!(
503 nm.condition_id,
504 "0x311d0c4b6671ab54af4970c06fcf58662516f5168997bdda209ec3db5aa6b0c1"
505 );
506 assert!(nm.active);
507 assert_eq!(nm.outcomes.len(), 2);
508 assert_eq!(nm.clob_token_ids.len(), 2);
509 assert_eq!(nm.order_price_min_tick_size.as_deref(), Some("0.01"));
510
511 let event = nm
512 .event_message
513 .as_ref()
514 .expect("event_message should be parsed");
515 assert_eq!(event.id, "125819");
516 assert_eq!(event.ticker, "nvda-above-in-january-2026");
517 assert_eq!(event.slug, "nvda-above-in-january-2026");
518 assert_eq!(
519 event.title,
520 "Will NVIDIA (NVDA) close above ___ end of January?"
521 );
522 }
523 }
524
525 #[rstest]
526 fn test_market_ws_message_resolved() {
527 let msg: MarketWsMessage = load("ws_market_resolved_msg.json");
528
529 assert!(matches!(msg, MarketWsMessage::MarketResolved(_)));
530 if let MarketWsMessage::MarketResolved(mr) = msg {
531 assert_eq!(mr.id, "1031769");
532 assert_eq!(mr.winning_outcome, "Yes");
533 assert_eq!(mr.assets_ids.len(), 2);
534 assert_eq!(
535 mr.winning_asset_id,
536 "76043073756653678226373981964075571318267289248134717369284518995922789326425"
537 );
538 }
539 }
540
541 #[rstest]
542 fn test_market_ws_message_best_bid_ask() {
543 let msg: MarketWsMessage = load("ws_market_best_bid_ask_msg.json");
544
545 assert!(matches!(msg, MarketWsMessage::BestBidAsk(_)));
546 if let MarketWsMessage::BestBidAsk(bba) = msg {
547 assert_eq!(bba.best_bid, "0.73");
548 assert_eq!(bba.best_ask, "0.77");
549 assert_eq!(bba.spread, "0.04");
550 }
551 }
552}