1use std::str::FromStr;
29
30use nautilus_common::cache::fifo::{FifoCache, FifoCacheMap};
31use nautilus_core::{UUID4, UnixNanos, collections::AtomicMap, time::AtomicTime};
32use nautilus_live::ExecutionEventEmitter;
33use nautilus_model::{
34 enums::{LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce},
35 events::{
36 OrderAccepted, OrderCanceled, OrderEventAny, OrderExpired, OrderFilled, OrderRejected,
37 OrderUpdated,
38 },
39 identifiers::{AccountId, VenueOrderId},
40 instruments::{Instrument, InstrumentAny},
41 reports::{FillReport, OrderStatusReport},
42 types::{Money, Price, Quantity},
43};
44use rust_decimal::Decimal;
45use ustr::Ustr;
46
47use super::{
48 messages::{PolymarketUserOrder, PolymarketUserTrade, UserWsMessage},
49 parse::parse_timestamp_ms,
50};
51use crate::{
52 common::enums::{PolymarketLiquiditySide, PolymarketOrderStatus},
53 execution::{
54 get_pusd_currency,
55 identity::{OrderIdentity, OrderIdentityRegistry},
56 order_fill_tracker::OrderFillTrackerMap,
57 parse::{
58 build_maker_fill_report, compute_commission, determine_order_side,
59 instrument_taker_fee, make_composite_trade_id, parse_liquidity_side,
60 },
61 pending::PendingSubmitTracker,
62 },
63};
64
65#[derive(Debug)]
67pub(crate) struct AccountRefreshRequest;
68
69#[derive(Debug, Default)]
71pub(crate) struct WsDispatchState {
72 pub processed_fills: FifoCache<String, 10_000>,
73 terminal_cancel_reports: FifoCacheMap<VenueOrderId, OrderStatusReport, 10_000>,
77}
78
79#[derive(Debug)]
81pub(crate) struct WsDispatchContext<'a> {
82 pub token_instruments: &'a AtomicMap<Ustr, InstrumentAny>,
83 pub fill_tracker: &'a OrderFillTrackerMap,
84 pub pending_submits: &'a PendingSubmitTracker,
85 pub order_identities: &'a OrderIdentityRegistry,
86 pub emitter: &'a ExecutionEventEmitter,
87 pub account_id: AccountId,
88 pub clock: &'static AtomicTime,
89 pub user_address: &'a str,
90 pub user_api_key: &'a str,
91}
92
93pub(crate) fn dispatch_user_message(
95 message: &UserWsMessage,
96 ctx: &WsDispatchContext<'_>,
97 state: &mut WsDispatchState,
98) -> Option<AccountRefreshRequest> {
99 match message {
100 UserWsMessage::Order(order) => {
101 dispatch_order_update(order, ctx, state);
102 None
103 }
104 UserWsMessage::Trade(trade) => dispatch_trade_update(trade, ctx, state),
105 }
106}
107
108fn dispatch_order_update(
109 order: &PolymarketUserOrder,
110 ctx: &WsDispatchContext<'_>,
111 state: &mut WsDispatchState,
112) {
113 let instruments = ctx.token_instruments.load();
114 let instrument = match instruments.get(&order.asset_id) {
115 Some(i) => i,
116 None => {
117 log::warn!("Unknown asset_id in order update: {}", order.asset_id);
118 return;
119 }
120 };
121
122 let ts_event = parse_timestamp_ms(&order.timestamp).unwrap_or_else(|_| ctx.clock.get_time_ns());
123 let venue_order_id = VenueOrderId::from(order.id.as_str());
124
125 let ts_init = ctx.clock.get_time_ns();
126 let mut report =
127 build_ws_order_status_report(order, instrument, ctx.account_id, ts_event, ts_init);
128 let local_client_order_id = ctx.pending_submits.client_order_id(&venue_order_id);
129 let mut is_accepted = ctx.fill_tracker.contains(&venue_order_id);
130 report.client_order_id = local_client_order_id;
131
132 let buffered_fills = if local_client_order_id.is_some()
134 && !is_accepted
135 && report.order_status != OrderStatus::Rejected
136 {
137 is_accepted = true;
138 ctx.fill_tracker.register_and_take_pending_fills(
139 venue_order_id,
140 local_client_order_id,
141 report.quantity,
142 report.order_side,
143 report.instrument_id,
144 instrument.size_precision(),
145 instrument.price_precision(),
146 )
147 } else if is_accepted {
148 ctx.fill_tracker
149 .take_pending_fills(venue_order_id, local_client_order_id)
150 } else {
151 Vec::new()
152 };
153
154 if let Some(tracked_filled) = ctx.fill_tracker.get_cumulative_filled(&venue_order_id) {
157 let tracked_qty = Quantity::new(tracked_filled, instrument.size_precision());
158 if report.filled_qty > tracked_qty {
159 log::debug!(
160 "Capping filled_qty for {venue_order_id} from {} to {} (awaiting trade messages)",
161 report.filled_qty,
162 tracked_qty,
163 );
164 report.filled_qty = tracked_qty;
165 }
166 }
167
168 if report.order_status == OrderStatus::Canceled {
172 state
173 .terminal_cancel_reports
174 .insert(venue_order_id, report.clone());
175 }
176
177 let identity = ctx.order_identities.get(&venue_order_id);
180 if is_accepted || local_client_order_id.is_some() {
181 match identity {
182 Some(identity) => emit_tracked_order_status(&report, &identity, ts_event, ctx),
183 None => ctx.emitter.send_order_status_report(report),
184 }
185 } else if let Some(report) = ctx
186 .fill_tracker
187 .accept_or_buffer_report(venue_order_id, report)
188 {
189 match ctx.order_identities.get(&venue_order_id) {
191 Some(identity) => emit_tracked_order_status(&report, &identity, ts_event, ctx),
192 None => ctx.emitter.send_order_status_report(report),
193 }
194 }
195
196 for fill in buffered_fills {
197 match identity {
198 Some(identity) => emit_order_filled(&identity, &fill, ctx),
199 None => ctx.emitter.send_fill_report(fill),
200 }
201 }
202
203 if order.status == PolymarketOrderStatus::Matched {
206 let price_precision = instrument.price_precision();
207 let price = Decimal::from_str(&order.price)
208 .ok()
209 .and_then(|d| Price::from_decimal_dp(d, price_precision).ok())
210 .unwrap_or_else(|| Price::zero(price_precision));
211
212 if let Some(dust_fill) = ctx.fill_tracker.check_dust_and_build_fill(
213 &venue_order_id,
214 ctx.account_id,
215 &order.id,
216 price.as_f64(),
217 get_pusd_currency(),
218 ts_event,
219 ts_init,
220 ) {
221 match ctx.order_identities.get(&venue_order_id) {
222 Some(identity) => emit_order_filled(&identity, &dust_fill, ctx),
223 None => ctx.emitter.send_fill_report(dust_fill),
224 }
225 }
226 }
227}
228
229fn dispatch_trade_update(
230 trade: &PolymarketUserTrade,
231 ctx: &WsDispatchContext<'_>,
232 state: &mut WsDispatchState,
233) -> Option<AccountRefreshRequest> {
234 if !trade.status.is_finalized()
235 && !matches!(
236 trade.status,
237 crate::common::enums::PolymarketTradeStatus::Matched
238 )
239 {
240 log::debug!(
241 "Skipping trade with status {:?}: {}",
242 trade.status,
243 trade.id
244 );
245 return None;
246 }
247
248 let dedup_key = format!("{}-{}", trade.id, trade.taker_order_id);
249 let is_duplicate = state.processed_fills.contains(&dedup_key);
250
251 let needs_refresh = trade.status.is_finalized();
252
253 if is_duplicate {
254 log::debug!("Duplicate fill skipped: {dedup_key}");
255 return if needs_refresh {
256 Some(AccountRefreshRequest)
257 } else {
258 None
259 };
260 }
261 state.processed_fills.add(dedup_key);
262
263 let is_maker = trade.trader_side == PolymarketLiquiditySide::Maker;
264 let liquidity_side = parse_liquidity_side(trade.trader_side);
265 let ts_event = parse_timestamp_ms(&trade.timestamp).unwrap_or_else(|_| ctx.clock.get_time_ns());
266 let ts_init = ctx.clock.get_time_ns();
267
268 if is_maker {
269 dispatch_maker_fills(trade, ctx, state, liquidity_side, ts_event, ts_init);
270 } else {
271 dispatch_taker_fill(trade, ctx, state, liquidity_side, ts_event, ts_init);
272 }
273
274 if needs_refresh {
275 Some(AccountRefreshRequest)
276 } else {
277 None
278 }
279}
280
281fn dispatch_maker_fills(
282 trade: &PolymarketUserTrade,
283 ctx: &WsDispatchContext<'_>,
284 state: &WsDispatchState,
285 liquidity_side: LiquiditySide,
286 ts_event: UnixNanos,
287 ts_init: UnixNanos,
288) {
289 let user_orders: Vec<_> = trade
290 .maker_orders
291 .iter()
292 .filter(|mo| mo.maker_address == ctx.user_address || mo.owner == ctx.user_api_key)
293 .collect();
294
295 if user_orders.is_empty() {
296 log::warn!("No matching maker orders for user in trade: {}", trade.id);
297 return;
298 }
299
300 let instruments = ctx.token_instruments.load();
301
302 for mo in user_orders {
303 let asset_id = Ustr::from(mo.asset_id.as_str());
304 let instrument = match instruments.get(&asset_id) {
305 Some(i) => i,
306 None => {
307 log::warn!("Unknown asset_id in maker order: {asset_id}");
308 continue;
309 }
310 };
311 let mut report = build_maker_fill_report(
312 mo,
313 &trade.id,
314 trade.trader_side,
315 trade.side,
316 trade.asset_id.as_str(),
317 ctx.account_id,
318 instrument.id(),
319 instrument.price_precision(),
320 instrument.size_precision(),
321 crate::execution::get_pusd_currency(),
322 liquidity_side,
323 ts_event,
324 ts_init,
325 );
326 let maker_venue_order_id = report.venue_order_id;
327 report.client_order_id = ctx.pending_submits.client_order_id(&maker_venue_order_id);
328 report.last_qty = ctx
329 .fill_tracker
330 .snap_fill_qty(&maker_venue_order_id, report.last_qty);
331
332 if let Some(report) = ctx
333 .fill_tracker
334 .accept_or_buffer_fill(maker_venue_order_id, report)
335 {
336 match ctx.order_identities.get(&maker_venue_order_id) {
337 Some(identity) => emit_order_filled(&identity, &report, ctx),
338 None => ctx.emitter.send_fill_report(report),
339 }
340 reemit_terminal_cancel(maker_venue_order_id, state, ctx);
341 }
342 }
343}
344
345fn dispatch_taker_fill(
346 trade: &PolymarketUserTrade,
347 ctx: &WsDispatchContext<'_>,
348 state: &WsDispatchState,
349 liquidity_side: LiquiditySide,
350 ts_event: UnixNanos,
351 ts_init: UnixNanos,
352) {
353 let instruments = ctx.token_instruments.load();
354 let instrument = match instruments.get(&trade.asset_id) {
355 Some(i) => i,
356 None => {
357 log::warn!("Unknown asset_id in trade: {}", trade.asset_id);
358 return;
359 }
360 };
361
362 let venue_order_id = VenueOrderId::from(trade.taker_order_id.as_str());
363
364 let mut report = build_ws_taker_fill_report(
365 trade,
366 instrument,
367 ctx.account_id,
368 liquidity_side,
369 ts_event,
370 ts_init,
371 );
372 report.client_order_id = ctx.pending_submits.client_order_id(&venue_order_id);
373 report.last_qty = ctx
374 .fill_tracker
375 .snap_fill_qty(&venue_order_id, report.last_qty);
376
377 if let Some(report) = ctx
378 .fill_tracker
379 .accept_or_buffer_fill(venue_order_id, report)
380 {
381 match ctx.order_identities.get(&venue_order_id) {
382 Some(identity) => emit_order_filled(&identity, &report, ctx),
383 None => ctx.emitter.send_fill_report(report),
384 }
385 reemit_terminal_cancel(venue_order_id, state, ctx);
386 }
387}
388
389fn reemit_terminal_cancel(
399 venue_order_id: VenueOrderId,
400 state: &WsDispatchState,
401 ctx: &WsDispatchContext<'_>,
402) {
403 if ctx.fill_tracker.is_fully_filled(&venue_order_id) {
404 return;
405 }
406
407 if let Some(cancel_report) = state.terminal_cancel_reports.get(&venue_order_id) {
408 log::debug!("Re-emitting cancel for {venue_order_id} after fill to restore terminal state");
409 match ctx.order_identities.get(&venue_order_id) {
410 Some(identity) => {
411 emit_order_canceled(&identity, venue_order_id, cancel_report.ts_last, ctx);
412 }
413 None => ctx.emitter.send_order_status_report(cancel_report.clone()),
414 }
415 }
416}
417
418fn build_ws_order_status_report(
419 order: &PolymarketUserOrder,
420 instrument: &InstrumentAny,
421 account_id: AccountId,
422 ts_event: UnixNanos,
423 ts_init: UnixNanos,
424) -> OrderStatusReport {
425 let venue_order_id = VenueOrderId::from(order.id.as_str());
426 let order_status =
427 crate::execution::parse::resolve_order_status(order.status, order.event_type);
428 let order_side = OrderSide::from(order.side);
429 let time_in_force = TimeInForce::from(order.order_type);
430 let size_precision = instrument.size_precision();
431 let price_precision = instrument.price_precision();
432 let quantity = Decimal::from_str(&order.original_size)
433 .ok()
434 .and_then(|d| Quantity::from_decimal_dp(d, size_precision).ok())
435 .unwrap_or_else(|| Quantity::zero(size_precision));
436 let filled_qty = Decimal::from_str(&order.size_matched)
437 .ok()
438 .and_then(|d| Quantity::from_decimal_dp(d, size_precision).ok())
439 .unwrap_or_else(|| Quantity::zero(size_precision));
440 let price = Decimal::from_str(&order.price)
441 .ok()
442 .and_then(|d| Price::from_decimal_dp(d, price_precision).ok())
443 .unwrap_or_else(|| Price::zero(price_precision));
444
445 let mut report = OrderStatusReport::new(
446 account_id,
447 instrument.id(),
448 None,
449 venue_order_id,
450 order_side,
451 OrderType::Limit,
452 time_in_force,
453 order_status,
454 quantity,
455 filled_qty,
456 ts_event,
457 ts_event,
458 ts_init,
459 None,
460 );
461 report.price = Some(price);
462 report
463}
464
465fn build_ws_taker_fill_report(
466 trade: &PolymarketUserTrade,
467 instrument: &InstrumentAny,
468 account_id: AccountId,
469 liquidity_side: LiquiditySide,
470 ts_event: UnixNanos,
471 ts_init: UnixNanos,
472) -> FillReport {
473 let venue_order_id = VenueOrderId::from(trade.taker_order_id.as_str());
474 let trade_id = make_composite_trade_id(&trade.id, &trade.taker_order_id);
475 let order_side = determine_order_side(
476 trade.trader_side,
477 trade.side,
478 trade.asset_id.as_str(),
479 trade.asset_id.as_str(),
480 );
481
482 let size_precision = instrument.size_precision();
483 let price_precision = instrument.price_precision();
484 let size_dec = Decimal::from_str(&trade.size).unwrap_or_default();
485 let price_dec = Decimal::from_str(&trade.price).unwrap_or_default();
486 let last_qty = Quantity::from_decimal_dp(size_dec, size_precision)
487 .unwrap_or_else(|_| Quantity::zero(size_precision));
488 let last_px = Price::from_decimal_dp(price_dec, price_precision)
489 .unwrap_or_else(|_| Price::zero(price_precision));
490
491 let fee_rate = instrument_taker_fee(instrument);
492 let commission_value = compute_commission(fee_rate, size_dec, price_dec, liquidity_side);
493 let pusd = crate::execution::get_pusd_currency();
494
495 FillReport {
496 account_id,
497 instrument_id: instrument.id(),
498 venue_order_id,
499 trade_id,
500 order_side,
501 last_qty,
502 last_px,
503 commission: Money::new(commission_value, pusd),
504 liquidity_side,
505 avg_px: None,
506 report_id: UUID4::new(),
507 ts_event,
508 ts_init,
509 client_order_id: None,
510 venue_position_id: None,
511 }
512}
513
514fn emit_tracked_order_status(
520 report: &OrderStatusReport,
521 identity: &OrderIdentity,
522 ts_event: UnixNanos,
523 ctx: &WsDispatchContext<'_>,
524) {
525 let venue_order_id = report.venue_order_id;
526 match report.order_status {
527 OrderStatus::Accepted => ensure_accepted(identity, venue_order_id, ts_event, ctx),
528 OrderStatus::PartiallyFilled | OrderStatus::Filled => {
529 ensure_accepted(identity, venue_order_id, ts_event, ctx);
530 }
531 OrderStatus::Canceled => {
532 ensure_accepted(identity, venue_order_id, ts_event, ctx);
533 emit_order_canceled(identity, venue_order_id, ts_event, ctx);
534 }
535 OrderStatus::Expired => {
536 ensure_accepted(identity, venue_order_id, ts_event, ctx);
537 emit_order_expired(identity, venue_order_id, ts_event, ctx);
538 }
539 OrderStatus::Rejected => {
540 let reason = report
541 .cancel_reason
542 .clone()
543 .unwrap_or_else(|| "REJECTED".to_string());
544 emit_order_rejected(identity, &reason, ts_event, ctx);
545 }
546 other => log::debug!("No order event for status {other:?} on {venue_order_id}"),
547 }
548}
549
550fn ensure_accepted(
556 identity: &OrderIdentity,
557 venue_order_id: VenueOrderId,
558 ts_event: UnixNanos,
559 ctx: &WsDispatchContext<'_>,
560) {
561 if !ctx.order_identities.mark_accepted(venue_order_id) {
562 return;
563 }
564 let accepted = OrderAccepted::new(
565 ctx.emitter.trader_id(),
566 identity.strategy_id,
567 identity.instrument_id,
568 identity.client_order_id,
569 venue_order_id,
570 ctx.account_id,
571 UUID4::new(),
572 ts_event,
573 ctx.clock.get_time_ns(),
574 false,
575 );
576 ctx.emitter
577 .send_order_event(OrderEventAny::Accepted(accepted));
578}
579
580fn emit_order_filled(identity: &OrderIdentity, fill: &FillReport, ctx: &WsDispatchContext<'_>) {
582 ensure_accepted(identity, fill.venue_order_id, fill.ts_event, ctx);
583
584 if let Some(new_qty) = ctx.fill_tracker.buy_overfill_bump(&fill.venue_order_id) {
585 emit_buy_overfill_update(identity, fill.venue_order_id, new_qty, fill.ts_event, ctx);
586 }
587
588 let filled = OrderFilled::new(
589 ctx.emitter.trader_id(),
590 identity.strategy_id,
591 identity.instrument_id,
592 identity.client_order_id,
593 fill.venue_order_id,
594 ctx.account_id,
595 fill.trade_id,
596 identity.order_side,
597 identity.order_type,
598 fill.last_qty,
599 fill.last_px,
600 get_pusd_currency(),
601 fill.liquidity_side,
602 UUID4::new(),
603 fill.ts_event,
604 fill.ts_init,
605 false,
606 fill.venue_position_id,
607 Some(fill.commission),
608 );
609 ctx.emitter.send_order_event(OrderEventAny::Filled(filled));
610}
611
612fn emit_buy_overfill_update(
618 identity: &OrderIdentity,
619 venue_order_id: VenueOrderId,
620 new_qty: Quantity,
621 ts_event: UnixNanos,
622 ctx: &WsDispatchContext<'_>,
623) {
624 let updated = OrderUpdated::new(
625 ctx.emitter.trader_id(),
626 identity.strategy_id,
627 identity.instrument_id,
628 identity.client_order_id,
629 new_qty,
630 UUID4::new(),
631 ts_event,
632 ctx.clock.get_time_ns(),
633 false,
634 Some(venue_order_id),
635 Some(ctx.account_id),
636 None,
637 None,
638 None,
639 false,
640 );
641 ctx.emitter
642 .send_order_event(OrderEventAny::Updated(updated));
643}
644
645fn emit_order_canceled(
646 identity: &OrderIdentity,
647 venue_order_id: VenueOrderId,
648 ts_event: UnixNanos,
649 ctx: &WsDispatchContext<'_>,
650) {
651 let canceled = OrderCanceled::new(
652 ctx.emitter.trader_id(),
653 identity.strategy_id,
654 identity.instrument_id,
655 identity.client_order_id,
656 UUID4::new(),
657 ts_event,
658 ctx.clock.get_time_ns(),
659 false,
660 Some(venue_order_id),
661 Some(ctx.account_id),
662 );
663 ctx.emitter
664 .send_order_event(OrderEventAny::Canceled(canceled));
665}
666
667fn emit_order_expired(
668 identity: &OrderIdentity,
669 venue_order_id: VenueOrderId,
670 ts_event: UnixNanos,
671 ctx: &WsDispatchContext<'_>,
672) {
673 let expired = OrderExpired::new(
674 ctx.emitter.trader_id(),
675 identity.strategy_id,
676 identity.instrument_id,
677 identity.client_order_id,
678 UUID4::new(),
679 ts_event,
680 ctx.clock.get_time_ns(),
681 false,
682 Some(venue_order_id),
683 Some(ctx.account_id),
684 );
685 ctx.emitter
686 .send_order_event(OrderEventAny::Expired(expired));
687}
688
689fn emit_order_rejected(
690 identity: &OrderIdentity,
691 reason: &str,
692 ts_event: UnixNanos,
693 ctx: &WsDispatchContext<'_>,
694) {
695 let rejected = OrderRejected::new(
696 ctx.emitter.trader_id(),
697 identity.strategy_id,
698 identity.instrument_id,
699 identity.client_order_id,
700 ctx.account_id,
701 Ustr::from(reason),
702 UUID4::new(),
703 ts_event,
704 ctx.clock.get_time_ns(),
705 false,
706 false,
707 );
708 ctx.emitter
709 .send_order_event(OrderEventAny::Rejected(rejected));
710}
711
712#[cfg(test)]
713mod tests {
714 use nautilus_common::messages::{ExecutionEvent, ExecutionReport};
715 use nautilus_core::time::AtomicTime;
716 use nautilus_model::{
717 enums::{AccountType, OrderStatus},
718 events::OrderEventAny,
719 identifiers::{ClientOrderId, InstrumentId, StrategyId, TraderId},
720 types::Currency,
721 };
722 use rstest::rstest;
723
724 use super::*;
725 use crate::http::{
726 models::GammaMarket,
727 parse::{create_instrument_from_def, parse_gamma_market},
728 };
729
730 fn register_identity(
732 order_identities: &OrderIdentityRegistry,
733 venue_order_id: VenueOrderId,
734 instrument_id: InstrumentId,
735 client_order_id: &str,
736 ) {
737 order_identities.register_order_identity(
738 venue_order_id,
739 OrderIdentity {
740 client_order_id: ClientOrderId::from(client_order_id),
741 strategy_id: StrategyId::from("S-001"),
742 instrument_id,
743 order_side: OrderSide::Buy,
744 order_type: OrderType::Limit,
745 },
746 );
747 }
748
749 fn load<T: serde::de::DeserializeOwned>(filename: &str) -> T {
750 let path = format!("test_data/{filename}");
751 let content = std::fs::read_to_string(path).expect("Failed to read test data");
752 serde_json::from_str(&content).expect("Failed to parse test data")
753 }
754
755 fn test_instrument() -> InstrumentAny {
756 let market: GammaMarket = load("gamma_market.json");
757 let defs = parse_gamma_market(&market).unwrap();
758 create_instrument_from_def(&defs[0], UnixNanos::from(1_000_000_000u64)).unwrap()
759 }
760
761 fn test_emitter() -> ExecutionEventEmitter {
762 ExecutionEventEmitter::new(
763 nautilus_core::time::get_atomic_clock_realtime(),
764 TraderId::from("TESTER-001"),
765 AccountId::from("POLY-001"),
766 AccountType::Cash,
767 Some(Currency::pUSD()),
768 )
769 }
770
771 #[rstest]
772 fn test_build_ws_order_status_report() {
773 let order: PolymarketUserOrder = load("ws_user_order_placement.json");
774 let instrument = test_instrument();
775 let ts_event = UnixNanos::from(1_000_000_000u64);
776 let ts_init = UnixNanos::from(2_000_000_000u64);
777
778 let report = build_ws_order_status_report(
779 &order,
780 &instrument,
781 AccountId::from("POLY-001"),
782 ts_event,
783 ts_init,
784 );
785
786 assert_eq!(report.order_side, OrderSide::Buy);
787 assert_eq!(report.order_type, OrderType::Limit);
788 assert!(report.price.is_some());
789 assert_eq!(report.ts_accepted, ts_event);
790 assert_eq!(report.ts_init, ts_init);
791 }
792
793 #[rstest]
794 fn test_build_ws_order_status_report_venue_cancel_maps_to_canceled() {
795 let order: PolymarketUserOrder = load("ws_user_order_venue_cancel.json");
796 let instrument = test_instrument();
797 let ts_event = UnixNanos::from(1_000_000_000u64);
798 let ts_init = UnixNanos::from(2_000_000_000u64);
799
800 let report = build_ws_order_status_report(
801 &order,
802 &instrument,
803 AccountId::from("POLY-001"),
804 ts_event,
805 ts_init,
806 );
807
808 assert_eq!(report.order_status, OrderStatus::Canceled);
809 }
810
811 #[rstest]
812 fn test_build_ws_taker_fill_report() {
813 let trade: PolymarketUserTrade = load("ws_user_trade.json");
814 let instrument = test_instrument();
815 let ts_event = UnixNanos::from(1_000_000_000u64);
816 let ts_init = UnixNanos::from(2_000_000_000u64);
817
818 let report = build_ws_taker_fill_report(
819 &trade,
820 &instrument,
821 AccountId::from("POLY-001"),
822 LiquiditySide::Taker,
823 ts_event,
824 ts_init,
825 );
826
827 assert_eq!(report.order_side, OrderSide::Buy);
828 assert_eq!(report.liquidity_side, LiquiditySide::Taker);
829 assert_eq!(report.ts_event, ts_event);
830 assert_eq!(report.ts_init, ts_init);
831 }
832
833 #[rstest]
834 fn test_dispatch_order_message_buffers_when_not_accepted() {
835 let order: PolymarketUserOrder = load("ws_user_order_placement.json");
836 let instrument = test_instrument();
837
838 let token_instruments = AtomicMap::new();
839 token_instruments.insert(order.asset_id, instrument);
840
841 let fill_tracker = OrderFillTrackerMap::new();
842 let pending_submits = PendingSubmitTracker::default();
843 let order_identities = OrderIdentityRegistry::default();
844 let emitter = test_emitter();
845
846 let ctx = WsDispatchContext {
847 token_instruments: &token_instruments,
848 fill_tracker: &fill_tracker,
849 pending_submits: &pending_submits,
850 order_identities: &order_identities,
851 emitter: &emitter,
852 account_id: AccountId::from("POLY-001"),
853 clock: nautilus_core::time::get_atomic_clock_realtime(),
854 user_address: "0xtest",
855 user_api_key: "test-key",
856 };
857 let mut state = WsDispatchState::default();
858
859 let result = dispatch_user_message(&UserWsMessage::Order(order.clone()), &ctx, &mut state);
860 assert!(result.is_none());
861
862 let venue_order_id = VenueOrderId::from(order.id.as_str());
864 assert!(fill_tracker.has_pending_report(&venue_order_id));
865 }
866
867 #[rstest]
868 fn test_dispatch_order_message_uses_pending_submit_client_order_id() {
869 let order: PolymarketUserOrder = load("ws_user_order_placement.json");
870 let instrument = test_instrument();
871
872 let token_instruments = AtomicMap::new();
873 token_instruments.insert(order.asset_id, instrument);
874
875 let fill_tracker = OrderFillTrackerMap::new();
876 let pending_submits = PendingSubmitTracker::default();
877 let order_identities = OrderIdentityRegistry::default();
878 let mut emitter = test_emitter();
879 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
880 emitter.set_sender(sender);
881
882 let venue_order_id = VenueOrderId::from(order.id.as_str());
883 let client_order_id = ClientOrderId::from("O-UNKNOWN-SUBMIT");
884 pending_submits.insert(venue_order_id, client_order_id);
885 register_identity(
886 &order_identities,
887 venue_order_id,
888 test_instrument().id(),
889 "O-UNKNOWN-SUBMIT",
890 );
891
892 let ctx = WsDispatchContext {
893 token_instruments: &token_instruments,
894 fill_tracker: &fill_tracker,
895 pending_submits: &pending_submits,
896 order_identities: &order_identities,
897 emitter: &emitter,
898 account_id: AccountId::from("POLY-001"),
899 clock: nautilus_core::time::get_atomic_clock_realtime(),
900 user_address: "0xtest",
901 user_api_key: "test-key",
902 };
903 let mut state = WsDispatchState::default();
904
905 let _ = dispatch_user_message(&UserWsMessage::Order(order), &ctx, &mut state);
906
907 let event = receiver.try_recv().expect("expected accepted event");
909 match event {
910 ExecutionEvent::Order(OrderEventAny::Accepted(accepted)) => {
911 assert_eq!(accepted.client_order_id, client_order_id);
912 }
913 other => panic!("Expected accepted event, was {other:?}"),
914 }
915
916 assert!(!fill_tracker.has_pending_report(&venue_order_id));
917 }
918
919 #[rstest]
920 fn test_dispatch_trade_dedup() {
921 let trade: PolymarketUserTrade = load("ws_user_trade.json");
922 let instrument = test_instrument();
923
924 let token_instruments = AtomicMap::new();
925 token_instruments.insert(trade.asset_id, instrument);
926
927 let fill_tracker = OrderFillTrackerMap::new();
928 let pending_submits = PendingSubmitTracker::default();
929 let order_identities = OrderIdentityRegistry::default();
930 let emitter = test_emitter();
931
932 let ctx = WsDispatchContext {
933 token_instruments: &token_instruments,
934 fill_tracker: &fill_tracker,
935 pending_submits: &pending_submits,
936 order_identities: &order_identities,
937 emitter: &emitter,
938 account_id: AccountId::from("POLY-001"),
939 clock: nautilus_core::time::get_atomic_clock_realtime(),
940 user_address: "0xtest",
941 user_api_key: "test-key",
942 };
943 let mut state = WsDispatchState::default();
944
945 let venue_order_id = VenueOrderId::from(trade.taker_order_id.as_str());
946
947 let _ = dispatch_user_message(&UserWsMessage::Trade(trade.clone()), &ctx, &mut state);
949 assert_eq!(fill_tracker.pending_fills_for(&venue_order_id).len(), 1);
950
951 let _ = dispatch_user_message(&UserWsMessage::Trade(trade), &ctx, &mut state);
953 assert_eq!(fill_tracker.pending_fills_for(&venue_order_id).len(), 1);
954 }
955
956 #[rstest]
957 fn test_dispatch_trade_uses_pending_submit_client_order_id() {
958 let trade: PolymarketUserTrade = load("ws_user_trade.json");
959 let instrument = test_instrument();
960
961 let token_instruments = AtomicMap::new();
962 token_instruments.insert(trade.asset_id, instrument);
963
964 let fill_tracker = OrderFillTrackerMap::new();
965 let pending_submits = PendingSubmitTracker::default();
966 let order_identities = OrderIdentityRegistry::default();
967 let emitter = test_emitter();
968
969 let venue_order_id = VenueOrderId::from(trade.taker_order_id.as_str());
970 let client_order_id = ClientOrderId::from("O-UNKNOWN-FILL");
971 pending_submits.insert(venue_order_id, client_order_id);
972
973 let ctx = WsDispatchContext {
974 token_instruments: &token_instruments,
975 fill_tracker: &fill_tracker,
976 pending_submits: &pending_submits,
977 order_identities: &order_identities,
978 emitter: &emitter,
979 account_id: AccountId::from("POLY-001"),
980 clock: nautilus_core::time::get_atomic_clock_realtime(),
981 user_address: "0xtest",
982 user_api_key: "test-key",
983 };
984 let mut state = WsDispatchState::default();
985
986 let _ = dispatch_user_message(&UserWsMessage::Trade(trade), &ctx, &mut state);
987
988 let fills = fill_tracker.pending_fills_for(&venue_order_id);
989 assert_eq!(fills[0].client_order_id, Some(client_order_id));
990 }
991
992 #[rstest]
993 fn test_dispatch_order_matched_caps_filled_qty_when_no_trades_tracked() {
994 let order: PolymarketUserOrder = load("ws_user_order_matched.json");
995 let instrument = test_instrument();
996
997 let token_instruments = AtomicMap::new();
998 token_instruments.insert(order.asset_id, instrument.clone());
999
1000 let fill_tracker = OrderFillTrackerMap::new();
1001 let venue_order_id = VenueOrderId::from(order.id.as_str());
1002
1003 fill_tracker.register(
1005 venue_order_id,
1006 Quantity::from("100"),
1007 OrderSide::Buy,
1008 instrument.id(),
1009 instrument.size_precision(),
1010 instrument.price_precision(),
1011 );
1012
1013 let pending_submits = PendingSubmitTracker::default();
1014 let order_identities = OrderIdentityRegistry::default();
1017 let mut emitter = test_emitter();
1018 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
1019 emitter.set_sender(sender);
1020
1021 let ctx = WsDispatchContext {
1022 token_instruments: &token_instruments,
1023 fill_tracker: &fill_tracker,
1024 pending_submits: &pending_submits,
1025 order_identities: &order_identities,
1026 emitter: &emitter,
1027 account_id: AccountId::from("POLY-001"),
1028 clock: nautilus_core::time::get_atomic_clock_realtime(),
1029 user_address: "0xtest",
1030 user_api_key: "test-key",
1031 };
1032 let mut state = WsDispatchState::default();
1033
1034 dispatch_user_message(&UserWsMessage::Order(order), &ctx, &mut state);
1035
1036 let event = receiver.try_recv().expect("Expected report");
1037 match event {
1038 ExecutionEvent::Report(report) => match report {
1039 ExecutionReport::Order(order_report) => {
1040 assert_eq!(order_report.filled_qty, Quantity::from("0"));
1041 }
1042 other => panic!("Expected order report, was {other:?}"),
1043 },
1044 other => panic!("Expected report event, was {other:?}"),
1045 }
1046 }
1047
1048 #[rstest]
1049 fn test_dispatch_order_matched_uses_tracked_fills_for_filled_qty() {
1050 let order: PolymarketUserOrder = load("ws_user_order_matched.json");
1051 let instrument = test_instrument();
1052
1053 let token_instruments = AtomicMap::new();
1054 token_instruments.insert(order.asset_id, instrument.clone());
1055
1056 let fill_tracker = OrderFillTrackerMap::new();
1057 let venue_order_id = VenueOrderId::from(order.id.as_str());
1058
1059 fill_tracker.register(
1061 venue_order_id,
1062 Quantity::from("100"),
1063 OrderSide::Buy,
1064 instrument.id(),
1065 instrument.size_precision(),
1066 instrument.price_precision(),
1067 );
1068 fill_tracker.record_fill(&venue_order_id, 50.0, 0.5, UnixNanos::from(1_000u64));
1069
1070 let pending_submits = PendingSubmitTracker::default();
1071 let order_identities = OrderIdentityRegistry::default();
1074 let mut emitter = test_emitter();
1075 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
1076 emitter.set_sender(sender);
1077
1078 let ctx = WsDispatchContext {
1079 token_instruments: &token_instruments,
1080 fill_tracker: &fill_tracker,
1081 pending_submits: &pending_submits,
1082 order_identities: &order_identities,
1083 emitter: &emitter,
1084 account_id: AccountId::from("POLY-001"),
1085 clock: nautilus_core::time::get_atomic_clock_realtime(),
1086 user_address: "0xtest",
1087 user_api_key: "test-key",
1088 };
1089 let mut state = WsDispatchState::default();
1090
1091 dispatch_user_message(&UserWsMessage::Order(order), &ctx, &mut state);
1092
1093 let event = receiver.try_recv().expect("Expected report");
1094 match event {
1095 ExecutionEvent::Report(report) => match report {
1096 ExecutionReport::Order(order_report) => {
1097 assert_eq!(order_report.filled_qty, Quantity::from("50"));
1098 }
1099 other => panic!("Expected order report, was {other:?}"),
1100 },
1101 other => panic!("Expected report event, was {other:?}"),
1102 }
1103 }
1104
1105 #[rstest]
1106 fn test_dispatch_order_matched_dust_fill_uses_local_ts_init() {
1107 let order: PolymarketUserOrder = load("ws_user_order_matched.json");
1108 let instrument = test_instrument();
1109
1110 let token_instruments = AtomicMap::new();
1111 token_instruments.insert(order.asset_id, instrument.clone());
1112
1113 let fill_tracker = OrderFillTrackerMap::new();
1114 let venue_order_id = VenueOrderId::from(order.id.as_str());
1115 fill_tracker.register(
1116 venue_order_id,
1117 Quantity::from("100"),
1118 OrderSide::Buy,
1119 instrument.id(),
1120 instrument.size_precision(),
1121 instrument.price_precision(),
1122 );
1123 fill_tracker.record_fill(&venue_order_id, 99.995, 0.5, UnixNanos::from(1_000u64));
1124
1125 let pending_submits = PendingSubmitTracker::default();
1126 let order_identities = OrderIdentityRegistry::default();
1127 register_identity(
1128 &order_identities,
1129 venue_order_id,
1130 instrument.id(),
1131 "O-MATCHED",
1132 );
1133 order_identities.mark_accepted(venue_order_id);
1134 let mut emitter = test_emitter();
1135 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
1136 emitter.set_sender(sender);
1137
1138 let clock = Box::leak(Box::new(AtomicTime::new(
1139 false,
1140 UnixNanos::from(2_000_000_000u64),
1141 )));
1142
1143 let ctx = WsDispatchContext {
1144 token_instruments: &token_instruments,
1145 fill_tracker: &fill_tracker,
1146 pending_submits: &pending_submits,
1147 order_identities: &order_identities,
1148 emitter: &emitter,
1149 account_id: AccountId::from("POLY-001"),
1150 clock,
1151 user_address: "0xtest",
1152 user_api_key: "test-key",
1153 };
1154 let mut state = WsDispatchState::default();
1155
1156 dispatch_user_message(&UserWsMessage::Order(order), &ctx, &mut state);
1157
1158 let event = receiver.try_recv().expect("Expected dust filled event");
1161 match event {
1162 ExecutionEvent::Order(OrderEventAny::Filled(filled)) => {
1163 assert_eq!(
1164 filled.ts_event,
1165 UnixNanos::from(1_703_875_201_000_000_000u64)
1166 );
1167 assert_eq!(filled.ts_init, UnixNanos::from(2_000_000_000u64));
1168 }
1169 other => panic!("Expected filled event, was {other:?}"),
1170 }
1171 }
1172
1173 #[rstest]
1174 fn test_cancel_reemitted_after_fill_for_canceled_order() {
1175 let cancel_order: PolymarketUserOrder = load("ws_user_order_cancellation.json");
1176 let trade: PolymarketUserTrade = load("ws_user_trade.json");
1177 let instrument = test_instrument();
1178
1179 let token_instruments = AtomicMap::new();
1180 token_instruments.insert(cancel_order.asset_id, instrument.clone());
1181
1182 let fill_tracker = OrderFillTrackerMap::new();
1183 let venue_order_id = VenueOrderId::from(cancel_order.id.as_str());
1184
1185 fill_tracker.register(
1187 venue_order_id,
1188 Quantity::from("100"),
1189 OrderSide::Buy,
1190 instrument.id(),
1191 instrument.size_precision(),
1192 instrument.price_precision(),
1193 );
1194
1195 let pending_submits = PendingSubmitTracker::default();
1196 let order_identities = OrderIdentityRegistry::default();
1197 register_identity(
1198 &order_identities,
1199 venue_order_id,
1200 instrument.id(),
1201 "O-CANCEL",
1202 );
1203 order_identities.mark_accepted(venue_order_id);
1204 let mut emitter = test_emitter();
1205 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
1206 emitter.set_sender(sender);
1207
1208 let ctx = WsDispatchContext {
1209 token_instruments: &token_instruments,
1210 fill_tracker: &fill_tracker,
1211 pending_submits: &pending_submits,
1212 order_identities: &order_identities,
1213 emitter: &emitter,
1214 account_id: AccountId::from("POLY-001"),
1215 clock: nautilus_core::time::get_atomic_clock_realtime(),
1216 user_address: "0xtest",
1217 user_api_key: "test-key",
1218 };
1219 let mut state = WsDispatchState::default();
1220
1221 dispatch_user_message(&UserWsMessage::Order(cancel_order), &ctx, &mut state);
1223 let cancel_event = receiver.try_recv().expect("Expected canceled event");
1224 match &cancel_event {
1225 ExecutionEvent::Order(OrderEventAny::Canceled(c)) => {
1226 assert_eq!(c.venue_order_id, Some(venue_order_id));
1227 }
1228 other => panic!("Expected canceled event, was {other:?}"),
1229 }
1230
1231 dispatch_user_message(&UserWsMessage::Trade(trade), &ctx, &mut state);
1233
1234 let fill_event = receiver.try_recv().expect("Expected filled event");
1236 match &fill_event {
1237 ExecutionEvent::Order(OrderEventAny::Filled(f)) => {
1238 assert_eq!(f.venue_order_id, venue_order_id);
1239 }
1240 other => panic!("Expected filled event, was {other:?}"),
1241 }
1242
1243 let reemitted_cancel = receiver
1244 .try_recv()
1245 .expect("Expected re-emitted canceled event");
1246
1247 match &reemitted_cancel {
1248 ExecutionEvent::Order(OrderEventAny::Canceled(c)) => {
1249 assert_eq!(c.venue_order_id, Some(venue_order_id));
1250 }
1251 other => panic!("Expected canceled event, was {other:?}"),
1252 }
1253 }
1254
1255 #[rstest]
1256 fn test_cancel_not_reemitted_when_fill_completes_order() {
1257 let cancel_order: PolymarketUserOrder = load("ws_user_order_cancellation.json");
1258 let trade: PolymarketUserTrade = load("ws_user_trade.json");
1259 let instrument = test_instrument();
1260
1261 let token_instruments = AtomicMap::new();
1262 token_instruments.insert(cancel_order.asset_id, instrument.clone());
1263
1264 let fill_tracker = OrderFillTrackerMap::new();
1265 let venue_order_id = VenueOrderId::from(cancel_order.id.as_str());
1266
1267 fill_tracker.register(
1269 venue_order_id,
1270 Quantity::from("25"),
1271 OrderSide::Buy,
1272 instrument.id(),
1273 instrument.size_precision(),
1274 instrument.price_precision(),
1275 );
1276
1277 let pending_submits = PendingSubmitTracker::default();
1278 let order_identities = OrderIdentityRegistry::default();
1279 register_identity(
1280 &order_identities,
1281 venue_order_id,
1282 instrument.id(),
1283 "O-CANCEL-FULL",
1284 );
1285 order_identities.mark_accepted(venue_order_id);
1286 let mut emitter = test_emitter();
1287 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
1288 emitter.set_sender(sender);
1289
1290 let ctx = WsDispatchContext {
1291 token_instruments: &token_instruments,
1292 fill_tracker: &fill_tracker,
1293 pending_submits: &pending_submits,
1294 order_identities: &order_identities,
1295 emitter: &emitter,
1296 account_id: AccountId::from("POLY-001"),
1297 clock: nautilus_core::time::get_atomic_clock_realtime(),
1298 user_address: "0xtest",
1299 user_api_key: "test-key",
1300 };
1301 let mut state = WsDispatchState::default();
1302
1303 dispatch_user_message(&UserWsMessage::Order(cancel_order), &ctx, &mut state);
1305 let _cancel = receiver.try_recv().expect("Expected canceled event");
1306
1307 dispatch_user_message(&UserWsMessage::Trade(trade), &ctx, &mut state);
1308 let _fill = receiver.try_recv().expect("Expected filled event");
1309
1310 assert!(
1312 receiver.try_recv().is_err(),
1313 "Should not re-emit cancel when fill completes the order"
1314 );
1315 }
1316
1317 #[rstest]
1318 fn test_cancel_saved_before_acceptance() {
1319 let cancel_order: PolymarketUserOrder = load("ws_user_order_cancellation.json");
1320 let instrument = test_instrument();
1321
1322 let token_instruments = AtomicMap::new();
1323 token_instruments.insert(cancel_order.asset_id, instrument);
1324
1325 let fill_tracker = OrderFillTrackerMap::new();
1327 let venue_order_id = VenueOrderId::from(cancel_order.id.as_str());
1328
1329 let pending_submits = PendingSubmitTracker::default();
1330 let order_identities = OrderIdentityRegistry::default();
1331 let emitter = test_emitter();
1332
1333 let ctx = WsDispatchContext {
1334 token_instruments: &token_instruments,
1335 fill_tracker: &fill_tracker,
1336 pending_submits: &pending_submits,
1337 order_identities: &order_identities,
1338 emitter: &emitter,
1339 account_id: AccountId::from("POLY-001"),
1340 clock: nautilus_core::time::get_atomic_clock_realtime(),
1341 user_address: "0xtest",
1342 user_api_key: "test-key",
1343 };
1344 let mut state = WsDispatchState::default();
1345
1346 dispatch_user_message(&UserWsMessage::Order(cancel_order), &ctx, &mut state);
1348
1349 assert!(fill_tracker.has_pending_report(&venue_order_id));
1351 assert!(state.terminal_cancel_reports.get(&venue_order_id).is_some());
1352 }
1353
1354 #[rstest]
1366 fn test_issue_3797_interleaved_cancel_fill_sequence() {
1367 use crate::common::{
1368 enums::{
1369 PolymarketEventType, PolymarketLiquiditySide, PolymarketOrderSide,
1370 PolymarketOrderStatus, PolymarketOrderType, PolymarketOutcome,
1371 PolymarketTradeStatus,
1372 },
1373 models::PolymarketMakerOrder,
1374 };
1375
1376 let instrument = test_instrument();
1377 let asset_id = instrument.id().symbol.inner();
1378
1379 let order_id =
1380 "0xe743f6c823ecdfa9ddaaf08673b2441d15a38d89e14dcb25b3b70c284be4f6ad".to_string();
1381 let venue_order_id = VenueOrderId::from(order_id.as_str());
1382
1383 let token_instruments = AtomicMap::new();
1384 token_instruments.insert(asset_id, instrument.clone());
1385
1386 let fill_tracker = OrderFillTrackerMap::new();
1387 fill_tracker.register(
1388 venue_order_id,
1389 Quantity::from("20"),
1390 OrderSide::Buy,
1391 instrument.id(),
1392 instrument.size_precision(),
1393 instrument.price_precision(),
1394 );
1395
1396 let pending_submits = PendingSubmitTracker::default();
1397 let order_identities = OrderIdentityRegistry::default();
1398 register_identity(&order_identities, venue_order_id, instrument.id(), "O-3797");
1399 order_identities.mark_accepted(venue_order_id);
1400 let mut emitter = test_emitter();
1401 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
1402 emitter.set_sender(sender);
1403
1404 let ctx = WsDispatchContext {
1405 token_instruments: &token_instruments,
1406 fill_tracker: &fill_tracker,
1407 pending_submits: &pending_submits,
1408 order_identities: &order_identities,
1409 emitter: &emitter,
1410 account_id: AccountId::from("POLY-001"),
1411 clock: nautilus_core::time::get_atomic_clock_realtime(),
1412 user_address: "0xabc",
1413 user_api_key: "xxx",
1414 };
1415 let mut state = WsDispatchState::default();
1416
1417 let make_order =
1419 |size_matched: &str, ts: &str, event_type: PolymarketEventType| PolymarketUserOrder {
1420 asset_id,
1421 associate_trades: None,
1422 created_at: "1775074735".to_string(),
1423 expiration: Some("0".to_string()),
1424 id: order_id.clone(),
1425 maker_address: Ustr::from("0xabc"),
1426 market: Ustr::from("0x4134"),
1427 order_owner: Ustr::from("xxx"),
1428 order_type: PolymarketOrderType::GTC,
1429 original_size: "20".to_string(),
1430 outcome: PolymarketOutcome::yes(),
1431 owner: Ustr::from("xxx"),
1432 price: "0.18".to_string(),
1433 side: PolymarketOrderSide::Buy,
1434 size_matched: size_matched.to_string(),
1435 status: PolymarketOrderStatus::Canceled,
1436 timestamp: ts.to_string(),
1437 event_type,
1438 };
1439
1440 let make_trade = |trade_id: &str, matched_amount: f64, ts: &str| PolymarketUserTrade {
1442 asset_id,
1443 bucket_index: 0,
1444 fee_rate_bps: "1000".to_string(),
1445 id: trade_id.to_string(),
1446 last_update: "1775074738".to_string(),
1447 maker_address: Ustr::from("0xother"),
1448 maker_orders: vec![PolymarketMakerOrder {
1449 asset_id,
1450 maker_address: "0xabc".to_string(),
1451 matched_amount: Decimal::from_f64_retain(matched_amount).unwrap_or(Decimal::ZERO),
1452 order_id: order_id.clone(),
1453 outcome: PolymarketOutcome::yes(),
1454 owner: "xxx".to_string(),
1455 price: Decimal::from_f64_retain(0.18).unwrap_or(Decimal::ZERO),
1456 side: None,
1457 }],
1458 market: Ustr::from("0x4134"),
1459 match_time: "1775074735".to_string(),
1460 outcome: PolymarketOutcome::yes(),
1461 owner: Ustr::from("other-owner"),
1462 price: "0.82".to_string(),
1463 side: PolymarketOrderSide::Buy,
1464 size: "1.219511".to_string(),
1465 status: PolymarketTradeStatus::Matched,
1466 taker_order_id: "0xtaker01".to_string(),
1467 timestamp: ts.to_string(),
1468 trade_owner: Ustr::from("other-owner"),
1469 trader_side: PolymarketLiquiditySide::Maker,
1470 event_type: PolymarketEventType::Trade,
1471 };
1472
1473 let msg_a = make_order("0", "1775074738031", PolymarketEventType::Cancellation);
1475 dispatch_user_message(&UserWsMessage::Order(msg_a), &ctx, &mut state);
1476
1477 let evt = receiver.try_recv().expect("(A) canceled event");
1478 match &evt {
1479 ExecutionEvent::Order(OrderEventAny::Canceled(c)) => {
1480 assert_eq!(c.venue_order_id, Some(venue_order_id));
1481 }
1482 other => panic!("(A) expected canceled event, was {other:?}"),
1483 }
1484
1485 let msg_b = make_trade("trade-b", 1.219511, "1775074738032");
1487 dispatch_user_message(&UserWsMessage::Trade(msg_b), &ctx, &mut state);
1488
1489 let evt = receiver.try_recv().expect("(B) filled event");
1490 match &evt {
1491 ExecutionEvent::Order(OrderEventAny::Filled(f)) => {
1492 assert_eq!(f.venue_order_id, venue_order_id);
1493 }
1494 other => panic!("(B) expected filled event, was {other:?}"),
1495 }
1496 let evt = receiver.try_recv().expect("(B) re-emitted cancel");
1498 match &evt {
1499 ExecutionEvent::Order(OrderEventAny::Canceled(c)) => {
1500 assert_eq!(c.venue_order_id, Some(venue_order_id));
1501 }
1502 other => panic!("(B) expected re-emitted cancel, was {other:?}"),
1503 }
1504
1505 let msg_c = make_order("1.219511", "1775074738034", PolymarketEventType::Update);
1507 dispatch_user_message(&UserWsMessage::Order(msg_c), &ctx, &mut state);
1508
1509 let evt = receiver.try_recv().expect("(C) canceled event");
1510 match &evt {
1511 ExecutionEvent::Order(OrderEventAny::Canceled(c)) => {
1512 assert_eq!(c.venue_order_id, Some(venue_order_id));
1513 }
1514 other => panic!("(C) expected canceled event, was {other:?}"),
1515 }
1516
1517 let msg_d = make_order("2.560972", "1775074738038", PolymarketEventType::Update);
1519 dispatch_user_message(&UserWsMessage::Order(msg_d), &ctx, &mut state);
1520
1521 let evt = receiver.try_recv().expect("(D) canceled event");
1522 match &evt {
1523 ExecutionEvent::Order(OrderEventAny::Canceled(c)) => {
1524 assert_eq!(c.venue_order_id, Some(venue_order_id));
1525 }
1526 other => panic!("(D) expected canceled event, was {other:?}"),
1527 }
1528
1529 let msg_e = make_trade("trade-e", 1.341461, "1775074738036");
1531 dispatch_user_message(&UserWsMessage::Trade(msg_e), &ctx, &mut state);
1532
1533 let evt = receiver.try_recv().expect("(E) filled event");
1534 match &evt {
1535 ExecutionEvent::Order(OrderEventAny::Filled(f)) => {
1536 assert_eq!(f.venue_order_id, venue_order_id);
1537 }
1538 other => panic!("(E) expected filled event, was {other:?}"),
1539 }
1540
1541 let evt = receiver.try_recv().expect("(E) re-emitted cancel");
1543 match &evt {
1544 ExecutionEvent::Order(OrderEventAny::Canceled(c)) => {
1545 assert_eq!(c.venue_order_id, Some(venue_order_id));
1546 }
1547 other => panic!("(E) expected re-emitted cancel, was {other:?}"),
1548 }
1549
1550 assert!(
1552 receiver.try_recv().is_err(),
1553 "No further events expected after the sequence"
1554 );
1555 }
1556
1557 #[rstest]
1558 fn test_dispatch_taker_fill_snaps_overfill_to_submitted_qty() {
1559 use crate::common::enums::{
1564 PolymarketEventType, PolymarketOrderSide, PolymarketOutcome, PolymarketTradeStatus,
1565 };
1566
1567 let instrument = test_instrument();
1568 let asset_id = instrument.id().symbol.inner();
1569 let token_instruments = AtomicMap::new();
1570 token_instruments.insert(asset_id, instrument.clone());
1571
1572 let fill_tracker = OrderFillTrackerMap::new();
1573 let venue_order_id = VenueOrderId::from("0xtaker-overfill");
1574 let submitted = Quantity::new(714.285710, instrument.size_precision());
1576 fill_tracker.register(
1577 venue_order_id,
1578 submitted,
1579 OrderSide::Buy,
1580 instrument.id(),
1581 instrument.size_precision(),
1582 instrument.price_precision(),
1583 );
1584
1585 let pending_submits = PendingSubmitTracker::default();
1586 let order_identities = OrderIdentityRegistry::default();
1587 register_identity(
1588 &order_identities,
1589 venue_order_id,
1590 instrument.id(),
1591 "O-OVERFILL",
1592 );
1593 order_identities.mark_accepted(venue_order_id);
1594 let mut emitter = test_emitter();
1595 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
1596 emitter.set_sender(sender);
1597
1598 let ctx = WsDispatchContext {
1599 token_instruments: &token_instruments,
1600 fill_tracker: &fill_tracker,
1601 pending_submits: &pending_submits,
1602 order_identities: &order_identities,
1603 emitter: &emitter,
1604 account_id: AccountId::from("POLY-001"),
1605 clock: nautilus_core::time::get_atomic_clock_realtime(),
1606 user_address: "0xtest",
1607 user_api_key: "test-key",
1608 };
1609 let mut state = WsDispatchState::default();
1610
1611 let trade = PolymarketUserTrade {
1612 asset_id,
1613 bucket_index: 0,
1614 fee_rate_bps: "0".to_string(),
1615 id: "trade-overfill".to_string(),
1616 last_update: "1700000001".to_string(),
1617 maker_address: Ustr::from("0xmaker"),
1618 maker_orders: vec![],
1619 market: Ustr::from("0xmarket"),
1620 match_time: "1700000000".to_string(),
1621 outcome: PolymarketOutcome::yes(),
1622 owner: Ustr::from("00000000-0000-0000-0000-000000000001"),
1623 price: "0.014".to_string(),
1624 side: PolymarketOrderSide::Buy,
1625 size: "714.285714".to_string(),
1628 status: PolymarketTradeStatus::Matched,
1629 taker_order_id: venue_order_id.as_str().to_string(),
1630 timestamp: "1700000000000".to_string(),
1631 trade_owner: Ustr::from("00000000-0000-0000-0000-000000000001"),
1632 trader_side: PolymarketLiquiditySide::Taker,
1633 event_type: PolymarketEventType::Trade,
1634 };
1635
1636 dispatch_user_message(&UserWsMessage::Trade(trade), &ctx, &mut state);
1637
1638 let cumulative = fill_tracker
1642 .get_cumulative_filled(&venue_order_id)
1643 .expect("order must be registered");
1644 let expected_snapped = submitted.as_f64();
1645 let drift = (cumulative - expected_snapped).abs();
1646 assert!(
1647 drift < 1e-9,
1648 "cumulative_filled {cumulative} must be snapped to submitted {expected_snapped}",
1649 );
1650
1651 let event = receiver.try_recv().expect("expected a filled event");
1654 match event {
1655 ExecutionEvent::Order(OrderEventAny::Filled(filled)) => {
1656 assert_eq!(
1657 filled.last_qty, submitted,
1658 "filled qty must be snapped to submitted",
1659 );
1660 assert_eq!(filled.venue_order_id, venue_order_id);
1661 }
1662 other => panic!("expected filled event, was {other:?}"),
1663 }
1664 }
1665
1666 #[rstest]
1667 fn test_dispatch_taker_fill_gross_overfill_raises_qty_then_fills() {
1668 use crate::common::enums::{
1672 PolymarketEventType, PolymarketOrderSide, PolymarketOutcome, PolymarketTradeStatus,
1673 };
1674
1675 let instrument = test_instrument();
1676 let asset_id = instrument.id().symbol.inner();
1677 let size_precision = instrument.size_precision();
1678 let token_instruments = AtomicMap::new();
1679 token_instruments.insert(asset_id, instrument.clone());
1680
1681 let fill_tracker = OrderFillTrackerMap::new();
1682 let venue_order_id = VenueOrderId::from("0xtaker-gross-overfill");
1683 let submitted = Quantity::new(30.0, size_precision);
1684 fill_tracker.register(
1685 venue_order_id,
1686 submitted,
1687 OrderSide::Buy,
1688 instrument.id(),
1689 size_precision,
1690 instrument.price_precision(),
1691 );
1692
1693 let pending_submits = PendingSubmitTracker::default();
1694 let order_identities = OrderIdentityRegistry::default();
1695 register_identity(
1696 &order_identities,
1697 venue_order_id,
1698 instrument.id(),
1699 "O-GROSS-OVERFILL",
1700 );
1701 order_identities.mark_accepted(venue_order_id);
1702 let mut emitter = test_emitter();
1703 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
1704 emitter.set_sender(sender);
1705
1706 let ctx = WsDispatchContext {
1707 token_instruments: &token_instruments,
1708 fill_tracker: &fill_tracker,
1709 pending_submits: &pending_submits,
1710 order_identities: &order_identities,
1711 emitter: &emitter,
1712 account_id: AccountId::from("POLY-001"),
1713 clock: nautilus_core::time::get_atomic_clock_realtime(),
1714 user_address: "0xtest",
1715 user_api_key: "test-key",
1716 };
1717 let mut state = WsDispatchState::default();
1718
1719 let trade = PolymarketUserTrade {
1721 asset_id,
1722 bucket_index: 0,
1723 fee_rate_bps: "0".to_string(),
1724 id: "trade-gross-overfill".to_string(),
1725 last_update: "1700000001".to_string(),
1726 maker_address: Ustr::from("0xmaker"),
1727 maker_orders: vec![],
1728 market: Ustr::from("0xmarket"),
1729 match_time: "1700000000".to_string(),
1730 outcome: PolymarketOutcome::yes(),
1731 owner: Ustr::from("00000000-0000-0000-0000-000000000001"),
1732 price: "0.014".to_string(),
1733 side: PolymarketOrderSide::Buy,
1734 size: "33.846152".to_string(),
1735 status: PolymarketTradeStatus::Matched,
1736 taker_order_id: venue_order_id.as_str().to_string(),
1737 timestamp: "1700000000000".to_string(),
1738 trade_owner: Ustr::from("00000000-0000-0000-0000-000000000001"),
1739 trader_side: PolymarketLiquiditySide::Taker,
1740 event_type: PolymarketEventType::Trade,
1741 };
1742
1743 dispatch_user_message(&UserWsMessage::Trade(trade), &ctx, &mut state);
1744
1745 let expected_qty = Quantity::new(33.846152, size_precision);
1746
1747 match receiver.try_recv().expect("expected an updated event") {
1749 ExecutionEvent::Order(OrderEventAny::Updated(updated)) => {
1750 assert_eq!(updated.quantity, expected_qty);
1751 assert_eq!(updated.venue_order_id, Some(venue_order_id));
1752 }
1753 other => panic!("expected updated event raising qty to the fill, was {other:?}"),
1754 }
1755
1756 match receiver.try_recv().expect("expected a filled event") {
1757 ExecutionEvent::Order(OrderEventAny::Filled(filled)) => {
1758 assert_eq!(filled.last_qty, expected_qty);
1759 assert_eq!(filled.venue_order_id, venue_order_id);
1760 }
1761 other => panic!("expected filled event, was {other:?}"),
1762 }
1763 }
1764
1765 #[rstest]
1768 #[case(crate::common::enums::PolymarketOrderStatus::Unmatched, "Rejected")]
1769 #[case(
1770 crate::common::enums::PolymarketOrderStatus::CanceledMarketResolved,
1771 "Expired"
1772 )]
1773 fn test_dispatch_order_terminal_status_emits_event(
1774 #[case] status: crate::common::enums::PolymarketOrderStatus,
1775 #[case] expected: &str,
1776 ) {
1777 use crate::common::enums::{
1778 PolymarketEventType, PolymarketOrderSide, PolymarketOrderType, PolymarketOutcome,
1779 };
1780
1781 let instrument = test_instrument();
1782 let asset_id = instrument.id().symbol.inner();
1783 let order_id = "0xterminal-order".to_string();
1784 let venue_order_id = VenueOrderId::from(order_id.as_str());
1785
1786 let token_instruments = AtomicMap::new();
1787 token_instruments.insert(asset_id, instrument.clone());
1788
1789 let fill_tracker = OrderFillTrackerMap::new();
1790 fill_tracker.register(
1791 venue_order_id,
1792 Quantity::from("10"),
1793 OrderSide::Buy,
1794 instrument.id(),
1795 instrument.size_precision(),
1796 instrument.price_precision(),
1797 );
1798
1799 let pending_submits = PendingSubmitTracker::default();
1800 let order_identities = OrderIdentityRegistry::default();
1801 register_identity(
1802 &order_identities,
1803 venue_order_id,
1804 instrument.id(),
1805 "O-TERMINAL",
1806 );
1807 order_identities.mark_accepted(venue_order_id);
1808 let mut emitter = test_emitter();
1809 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
1810 emitter.set_sender(sender);
1811
1812 let ctx = WsDispatchContext {
1813 token_instruments: &token_instruments,
1814 fill_tracker: &fill_tracker,
1815 pending_submits: &pending_submits,
1816 order_identities: &order_identities,
1817 emitter: &emitter,
1818 account_id: AccountId::from("POLY-001"),
1819 clock: nautilus_core::time::get_atomic_clock_realtime(),
1820 user_address: "0xabc",
1821 user_api_key: "xxx",
1822 };
1823 let mut state = WsDispatchState::default();
1824
1825 let order = PolymarketUserOrder {
1826 asset_id,
1827 associate_trades: None,
1828 created_at: "1775074735".to_string(),
1829 expiration: Some("0".to_string()),
1830 id: order_id,
1831 maker_address: Ustr::from("0xabc"),
1832 market: Ustr::from("0x4134"),
1833 order_owner: Ustr::from("xxx"),
1834 order_type: PolymarketOrderType::FOK,
1835 original_size: "10".to_string(),
1836 outcome: PolymarketOutcome::yes(),
1837 owner: Ustr::from("xxx"),
1838 price: "0.50".to_string(),
1839 side: PolymarketOrderSide::Buy,
1840 size_matched: "0".to_string(),
1841 status,
1842 timestamp: "1775074738031".to_string(),
1843 event_type: PolymarketEventType::Placement,
1844 };
1845
1846 dispatch_user_message(&UserWsMessage::Order(order), &ctx, &mut state);
1847
1848 let event = receiver.try_recv().expect("expected terminal order event");
1849 match event {
1850 ExecutionEvent::Order(order_event) => {
1851 assert!(
1852 format!("{order_event:?}").starts_with(expected),
1853 "expected {expected}, was {order_event:?}"
1854 );
1855 assert_eq!(
1856 order_event.client_order_id(),
1857 ClientOrderId::from("O-TERMINAL")
1858 );
1859 }
1860 other => panic!("expected order event, was {other:?}"),
1861 }
1862 }
1863}