1use std::fmt::Debug;
44
45use ahash::{AHashMap, AHashSet};
46use anyhow::Context;
47use indexmap::IndexMap;
48use nautilus_common::cache::fifo::{FifoCache, FifoCacheMap};
49use nautilus_core::{
50 UUID4, UnixNanos, collections::AtomicMap, string::secret::REDACTED, time::AtomicTime,
51};
52use nautilus_live::{ExecutionEventEmitter, execution::context::OrderContext};
53use nautilus_model::{
54 enums::{LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce},
55 events::{
56 OrderAccepted, OrderCanceled, OrderEventAny, OrderExpired, OrderFillVoided, OrderFilled,
57 OrderRejected, OrderUpdated,
58 },
59 identifiers::{AccountId, ClientOrderId, InstrumentId, TradeId, VenueOrderId},
60 instruments::{Instrument, InstrumentAny},
61 reports::{FillReport, OrderStatusReport},
62 types::{Price, Quantity},
63};
64use rust_decimal::Decimal;
65use ustr::Ustr;
66
67use super::{
68 messages::{
69 PolymarketUserOrder, PolymarketUserOrderStatus, PolymarketUserTrade, UserWsMessage,
70 },
71 parse::parse_timestamp_ms,
72};
73use crate::{
74 common::{
75 enums::{
76 PolymarketOrderSide, PolymarketOrderStatus, PolymarketOrderType, PolymarketSignerType,
77 PolymarketTradeStatus,
78 },
79 parse::parse_decimal_exact,
80 },
81 execution::{
82 context::OrderContextRegistry,
83 fill_tracker::{BufferedFill, FillCorrectionMetadata, OrderFillTrackerMap},
84 get_pusd_currency, is_post_only_crossing,
85 parse::parse_order_status_report,
86 pending::PendingSubmitTracker,
87 reconciliation::admit_selected_trade,
88 settlement::{
89 AdmissionContext, AdmissionError, AdmittedLeg, AdmittedTrade, SettlementAction,
90 SettlementRegistry, TradeEvidence, admit_trade_evidence,
91 },
92 },
93 http::{
94 error::sanitize_error_text,
95 models::{PolymarketOpenOrder, PolymarketTradeReport},
96 },
97};
98
99#[derive(Debug)]
101pub(crate) struct AccountRefreshRequest;
102
103#[derive(Debug, Default)]
108pub(crate) struct WsDispatchState {
109 pub reconciled_fills: FifoCache<(TradeId, VenueOrderId), 10_000>,
110
111 pending_terminal_orders: FifoCacheMap<VenueOrderId, PendingTerminalOrder, 10_000>,
112 terminal_cancel_reports: FifoCacheMap<VenueOrderId, OrderStatusReport, 10_000>,
113
114 pending_commands: AHashMap<ClientOrderId, PendingCommand>,
115 inflight_cancel_markets: AHashSet<InstrumentId>,
116 replaced_venue_order_ids: FifoCache<VenueOrderId, 10_000>,
117 closed_modify_venue_order_ids: FifoCacheMap<VenueOrderId, UnixNanos, 10_000>,
118}
119
120impl WsDispatchState {
121 pub(crate) fn begin_modify(
122 &mut self,
123 client_order_id: ClientOrderId,
124 old_venue_order_id: VenueOrderId,
125 instrument_id: InstrumentId,
126 ) -> bool {
127 if self.pending_commands.contains_key(&client_order_id)
128 || self.replaced_venue_order_ids.contains(&old_venue_order_id)
129 || self.inflight_cancel_markets.contains(&instrument_id)
130 {
131 return false;
132 }
133
134 self.pending_commands.insert(
135 client_order_id,
136 PendingCommand::Modify {
137 old_venue_order_id,
138 instrument_id,
139 cancel_ts: None,
140 replacement: None,
141 },
142 );
143 true
144 }
145
146 pub(crate) fn is_modifying(&self, client_order_id: &ClientOrderId) -> bool {
147 matches!(
148 self.pending_commands.get(client_order_id),
149 Some(PendingCommand::Modify { .. })
150 )
151 }
152
153 pub(crate) fn confirm_modify_cancel(
154 &mut self,
155 client_order_id: ClientOrderId,
156 expected_old_venue_order_id: VenueOrderId,
157 ts_event: UnixNanos,
158 ) -> bool {
159 let Some(PendingCommand::Modify {
160 old_venue_order_id,
161 cancel_ts,
162 ..
163 }) = self.pending_commands.get_mut(&client_order_id)
164 else {
165 return false;
166 };
167
168 if *old_venue_order_id != expected_old_venue_order_id {
169 return false;
170 }
171
172 cancel_ts.get_or_insert(ts_event);
173 true
174 }
175
176 pub(crate) fn set_modify_replacement(
177 &mut self,
178 client_order_id: ClientOrderId,
179 venue_order_id: VenueOrderId,
180 quantity: Quantity,
181 leg_quantity: Quantity,
182 prior_filled: Quantity,
183 price: Price,
184 ) -> bool {
185 let Some(PendingCommand::Modify { replacement, .. }) =
186 self.pending_commands.get_mut(&client_order_id)
187 else {
188 return false;
189 };
190
191 *replacement = Some(PendingModifyReplacement {
192 venue_order_id,
193 quantity,
194 leg_quantity,
195 prior_filled,
196 price,
197 });
198 true
199 }
200
201 #[expect(
211 clippy::too_many_arguments,
212 reason = "promotion coordinates dispatch, contexts, fills, emission, and source-specific time"
213 )]
214 pub(crate) fn promote_modify_replacement(
215 &mut self,
216 venue_order_id: VenueOrderId,
217 order_contexts: &OrderContextRegistry,
218 fill_tracker: &OrderFillTrackerMap,
219 emitter: &ExecutionEventEmitter,
220 clock: &'static AtomicTime,
221 resolve_context: impl FnOnce(VenueOrderId) -> Option<(OrderContext, AccountId)>,
222 event_time: impl FnOnce() -> UnixNanos,
223 ) -> Option<ModifyPromotionActivity> {
224 let promotion = self.claim_modify_replacement(venue_order_id)?;
225 let (mut context, account_id) = resolve_context(promotion.old_venue_order_id)?;
226 context.quantity = promotion.quantity;
227 context.price = Some(promotion.price);
228 order_contexts.register_context(venue_order_id, context);
229 order_contexts.mark_accepted(venue_order_id);
230
231 let ts_event = event_time();
232
233 let updated = OrderUpdated::new(
234 emitter.trader_id(),
235 context.identity.strategy_id,
236 context.identity.instrument_id,
237 promotion.client_order_id,
238 promotion.quantity,
239 UUID4::new(),
240 ts_event,
241 clock.get_time_ns(),
242 false,
243 Some(venue_order_id),
244 Some(account_id),
245 Some(promotion.price),
246 None,
247 None,
248 false,
249 );
250 emitter.send_order_event(OrderEventAny::Updated(updated));
251
252 let fills = fill_tracker.register_and_take_pending_fills(
253 venue_order_id,
254 Some(promotion.client_order_id),
255 promotion.leg_quantity,
256 promotion.quantity.saturating_sub(promotion.leg_quantity),
257 promotion.prior_filled,
258 context.identity.order_side,
259 );
260 let reports = fill_tracker.take_pending_reports(&venue_order_id);
261 Some(ModifyPromotionActivity {
262 client_order_id: promotion.client_order_id,
263 fills,
264 reports,
265 })
266 }
267
268 pub(crate) fn claim_modify_replacement(
269 &mut self,
270 venue_order_id: VenueOrderId,
271 ) -> Option<ModifyPromotion> {
272 let promotion = self.pending_modify_promotion(venue_order_id)?;
273 self.pending_commands.remove(&promotion.client_order_id)?;
274 self.replaced_venue_order_ids
275 .add(promotion.old_venue_order_id);
276 Some(promotion)
277 }
278
279 pub(crate) fn finish_modify_without_replacement(
280 &mut self,
281 client_order_id: ClientOrderId,
282 expected_old_venue_order_id: VenueOrderId,
283 cancellation_proven: bool,
284 ts_event: UnixNanos,
285 ) -> Option<(VenueOrderId, Option<UnixNanos>)> {
286 let Some(PendingCommand::Modify {
287 old_venue_order_id, ..
288 }) = self.pending_commands.get(&client_order_id)
289 else {
290 return None;
291 };
292
293 if *old_venue_order_id != expected_old_venue_order_id {
294 return None;
295 }
296
297 let PendingCommand::Modify {
298 old_venue_order_id,
299 cancel_ts,
300 ..
301 } = self.pending_commands.remove(&client_order_id)?
302 else {
303 return None;
304 };
305
306 let cancel_ts = self
307 .terminal_cancel_reports
308 .get(&old_venue_order_id)
309 .map(|report| report.ts_last)
310 .or(cancel_ts)
311 .or(cancellation_proven.then_some(ts_event));
312
313 if let Some(cancel_ts) = cancel_ts {
314 self.closed_modify_venue_order_ids
315 .insert(old_venue_order_id, cancel_ts);
316 }
317
318 Some((old_venue_order_id, cancel_ts))
319 }
320
321 pub(crate) fn finish_unsubmitted_modifies(
322 &mut self,
323 ) -> Vec<(ClientOrderId, VenueOrderId, Option<UnixNanos>)> {
324 let terminal_cancel_reports = &self.terminal_cancel_reports;
325 let closed_modify_venue_order_ids = &mut self.closed_modify_venue_order_ids;
326 let mut finished = Vec::new();
327
328 self.pending_commands
329 .retain(|client_order_id, command| match command {
330 PendingCommand::Modify {
331 old_venue_order_id,
332 cancel_ts,
333 replacement: None,
334 ..
335 } => {
336 let cancel_ts = terminal_cancel_reports
337 .get(old_venue_order_id)
338 .map(|report| report.ts_last)
339 .or(*cancel_ts);
340 if let Some(cancel_ts) = cancel_ts {
341 closed_modify_venue_order_ids.insert(*old_venue_order_id, cancel_ts);
342 }
343
344 finished.push((*client_order_id, *old_venue_order_id, cancel_ts));
345 false
346 }
347 _ => true,
348 });
349
350 finished
351 }
352
353 pub(crate) fn pending_modify_promotion(
354 &self,
355 venue_order_id: VenueOrderId,
356 ) -> Option<ModifyPromotion> {
357 self.pending_commands
358 .iter()
359 .find_map(|(client_order_id, command)| match command {
360 PendingCommand::Modify {
361 old_venue_order_id,
362 replacement: Some(replacement),
363 ..
364 } if replacement.venue_order_id == venue_order_id => Some(ModifyPromotion {
365 client_order_id: *client_order_id,
366 old_venue_order_id: *old_venue_order_id,
367 venue_order_id,
368 quantity: replacement.quantity,
369 leg_quantity: replacement.leg_quantity,
370 prior_filled: replacement.prior_filled,
371 price: replacement.price,
372 }),
373 _ => None,
374 })
375 }
376
377 pub(crate) fn pending_modify_promotions(&self) -> Vec<ModifyPromotion> {
378 self.pending_commands
379 .iter()
380 .filter_map(|(client_order_id, command)| match command {
381 PendingCommand::Modify {
382 old_venue_order_id,
383 replacement: Some(replacement),
384 ..
385 } => Some(ModifyPromotion {
386 client_order_id: *client_order_id,
387 old_venue_order_id: *old_venue_order_id,
388 venue_order_id: replacement.venue_order_id,
389 quantity: replacement.quantity,
390 leg_quantity: replacement.leg_quantity,
391 prior_filled: replacement.prior_filled,
392 price: replacement.price,
393 }),
394 _ => None,
395 })
396 .collect()
397 }
398
399 pub(crate) fn begin_cancels(&mut self, orders: &[(ClientOrderId, InstrumentId)]) -> bool {
400 if orders.iter().any(|(client_order_id, instrument_id)| {
401 self.pending_commands.contains_key(client_order_id)
402 || self.inflight_cancel_markets.contains(instrument_id)
403 }) {
404 return false;
405 }
406
407 self.pending_commands.extend(
408 orders
409 .iter()
410 .map(|(client_order_id, _)| (*client_order_id, PendingCommand::Cancel)),
411 );
412 true
413 }
414
415 pub(crate) fn begin_available_cancels(
416 &mut self,
417 orders: &[(ClientOrderId, InstrumentId)],
418 ) -> Option<Vec<ClientOrderId>> {
419 if orders.iter().any(|(client_order_id, instrument_id)| {
420 matches!(
421 self.pending_commands.get(client_order_id),
422 Some(PendingCommand::Modify { .. })
423 ) || self.inflight_cancel_markets.contains(instrument_id)
424 }) {
425 return None;
426 }
427
428 let client_order_ids = orders
429 .iter()
430 .filter_map(|(client_order_id, _)| {
431 if self.pending_commands.contains_key(client_order_id) {
432 return None;
433 }
434
435 self.pending_commands
436 .insert(*client_order_id, PendingCommand::Cancel);
437 Some(*client_order_id)
438 })
439 .collect();
440 Some(client_order_ids)
441 }
442
443 pub(crate) fn finish_cancels(&mut self, client_order_ids: &[ClientOrderId]) {
444 for client_order_id in client_order_ids {
445 if matches!(
446 self.pending_commands.get(client_order_id),
447 Some(PendingCommand::Cancel)
448 ) {
449 self.pending_commands.remove(client_order_id);
450 }
451 }
452 }
453
454 pub(crate) fn begin_market_cancel(&mut self, instrument_id: InstrumentId) -> bool {
455 if self.pending_commands.values().any(|command| match command {
456 PendingCommand::Modify {
457 instrument_id: pending_instrument_id,
458 ..
459 } => *pending_instrument_id == instrument_id,
460 PendingCommand::Cancel => false,
461 }) {
462 return false;
463 }
464
465 self.inflight_cancel_markets.insert(instrument_id)
466 }
467
468 pub(crate) fn finish_market_cancel(&mut self, instrument_id: InstrumentId) {
469 self.inflight_cancel_markets.remove(&instrument_id);
470 }
471
472 pub(crate) fn record_terminal_cancel_report(&mut self, report: OrderStatusReport) {
473 self.terminal_cancel_reports
474 .insert(report.venue_order_id, report);
475 }
476
477 pub(crate) fn record_reconciled_fill(
478 &mut self,
479 trade_id: TradeId,
480 venue_order_id: VenueOrderId,
481 ) {
482 self.reconciled_fills.add((trade_id, venue_order_id));
483 }
484
485 pub(crate) fn replaced_venue_order_id(&self, venue_order_id: VenueOrderId) -> bool {
486 self.replaced_venue_order_ids.contains(&venue_order_id)
487 }
488
489 pub(crate) fn reset_session(&mut self) {
490 let retained_cancel_reports = self
491 .pending_commands
492 .values()
493 .filter_map(|command| match command {
494 PendingCommand::Modify {
495 old_venue_order_id, ..
496 } => self
497 .terminal_cancel_reports
498 .get(old_venue_order_id)
499 .cloned(),
500 PendingCommand::Cancel => None,
501 })
502 .collect::<Vec<_>>();
503
504 self.pending_terminal_orders.clear();
505 self.terminal_cancel_reports.clear();
506 for report in retained_cancel_reports {
507 self.terminal_cancel_reports
508 .insert(report.venue_order_id, report);
509 }
510
511 self.pending_commands
512 .retain(|_, command| matches!(command, PendingCommand::Modify { .. }));
513 self.inflight_cancel_markets.clear();
514 }
515
516 fn suppress_modify_cancel(&self, venue_order_id: VenueOrderId) -> bool {
517 self.closed_modify_venue_order_ids
518 .contains_key(&venue_order_id)
519 || self.suppress_modify_cancel_reemit(venue_order_id)
520 }
521
522 pub(crate) fn suppress_modify_cancel_reemit(&self, venue_order_id: VenueOrderId) -> bool {
523 self.replaced_venue_order_ids.contains(&venue_order_id)
524 || self.pending_commands.values().any(|command| {
525 matches!(
526 command,
527 PendingCommand::Modify {
528 old_venue_order_id,
529 ..
530 } if *old_venue_order_id == venue_order_id
531 )
532 })
533 }
534}
535
536#[derive(Clone, Copy, Debug)]
537enum PendingCommand {
538 Modify {
539 old_venue_order_id: VenueOrderId,
540 instrument_id: InstrumentId,
541 cancel_ts: Option<UnixNanos>,
542 replacement: Option<PendingModifyReplacement>,
543 },
544 Cancel,
545}
546
547#[derive(Clone, Copy, Debug)]
548struct PendingModifyReplacement {
549 venue_order_id: VenueOrderId,
550 quantity: Quantity,
551 leg_quantity: Quantity,
552 prior_filled: Quantity,
553 price: Price,
554}
555
556#[derive(Clone, Copy, Debug)]
557pub(crate) struct ModifyPromotion {
558 pub(crate) client_order_id: ClientOrderId,
559 pub(crate) old_venue_order_id: VenueOrderId,
560 pub(crate) venue_order_id: VenueOrderId,
561 pub(crate) quantity: Quantity,
562 pub(crate) leg_quantity: Quantity,
563 pub(crate) prior_filled: Quantity,
564 pub(crate) price: Price,
565}
566
567#[derive(Debug)]
569pub(crate) struct ModifyPromotionActivity {
570 pub(crate) client_order_id: ClientOrderId,
571 pub(crate) fills: Vec<BufferedFill>,
572 pub(crate) reports: Vec<OrderStatusReport>,
573}
574
575#[derive(Clone, Debug)]
576struct PendingTerminalOrder {
577 trade_ids: Vec<String>,
578 ts_event: UnixNanos,
579}
580
581pub(crate) struct WsDispatchContext<'a> {
583 pub token_instruments: &'a AtomicMap<Ustr, InstrumentAny>,
584 pub fill_tracker: &'a OrderFillTrackerMap,
585 pub settlement: &'a SettlementRegistry,
586 pub pending_submits: &'a PendingSubmitTracker,
587 pub order_contexts: &'a OrderContextRegistry,
588 pub emitter: &'a ExecutionEventEmitter,
589 pub account_id: AccountId,
590 pub clock: &'static AtomicTime,
591 pub signer_type: PolymarketSignerType,
592 pub user_address: &'a str,
593 pub user_api_key: &'a str,
594}
595
596impl Debug for WsDispatchContext<'_> {
597 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
598 f.debug_struct(stringify!(WsDispatchContext))
599 .field("token_instruments", &self.token_instruments)
600 .field("fill_tracker", &self.fill_tracker)
601 .field("settlement", &self.settlement)
602 .field("pending_submits", &self.pending_submits)
603 .field("order_contexts", &self.order_contexts)
604 .field("emitter", &self.emitter)
605 .field("account_id", &self.account_id)
606 .field("clock", &self.clock)
607 .field("user_address", &self.user_address)
608 .field("user_api_key", &REDACTED)
609 .finish()
610 }
611}
612
613impl WsDispatchContext<'_> {
614 pub(crate) fn admission_context(&self) -> AdmissionContext<'_> {
615 AdmissionContext {
616 signer_type: self.signer_type,
617 user_address: self.user_address,
618 api_key: self.user_api_key,
619 pusd: get_pusd_currency(),
620 instruments: self.token_instruments,
621 fee_bases: Some(self.settlement),
622 }
623 }
624}
625
626pub(crate) fn dispatch_user_message(
628 message: &UserWsMessage,
629 ctx: &WsDispatchContext<'_>,
630 state: &mut WsDispatchState,
631) -> Option<AccountRefreshRequest> {
632 match message {
633 UserWsMessage::Order(order) => {
634 dispatch_order_update(order, ctx, state);
635 None
636 }
637 UserWsMessage::Trade(trade) => dispatch_trade_update(trade, ctx, state),
638 }
639}
640
641fn dispatch_order_update(
642 order: &PolymarketUserOrder,
643 ctx: &WsDispatchContext<'_>,
644 state: &mut WsDispatchState,
645) {
646 let Some(status) = order.status.as_ref() else {
647 log::warn!("Ignoring order update without status: {}", order.id);
648 return;
649 };
650
651 let Some(order_type) = order.order_type else {
652 log::warn!("Ignoring order update without order_type: {}", order.id);
653 return;
654 };
655
656 let instruments = ctx.token_instruments.load();
657 let instrument = match instruments.get(&order.asset_id) {
658 Some(i) => i,
659 None => {
660 log::warn!("Unknown asset_id in order update: {}", order.asset_id);
661 return;
662 }
663 };
664
665 let ts_event = parse_timestamp_ms(&order.timestamp).unwrap_or_else(|_| ctx.clock.get_time_ns());
666 let venue_order_id = VenueOrderId::from(order.id.as_str());
667
668 let ts_init = ctx.clock.get_time_ns();
669 let mut report = match build_ws_order_status_report(
670 order,
671 status,
672 order_type,
673 instrument,
674 ctx.account_id,
675 ts_event,
676 ts_init,
677 ) {
678 Ok(report) => report,
679 Err(e) => {
680 log::warn!("Ignoring invalid order update {}: {e}", order.id);
681 return;
682 }
683 };
684 let mut promoted_fills = Vec::new();
685 let mut promoted_reports = Vec::new();
686 let promoted_client_order_id = if state.pending_modify_promotion(venue_order_id).is_some() {
687 if report.order_status == OrderStatus::Rejected {
688 reject_modify_replacement(venue_order_id, &report, ts_event, ctx, state);
689 return;
690 }
691
692 promote_modify_replacement_from_ws(
693 venue_order_id,
694 ts_event,
695 ctx,
696 state,
697 &mut promoted_fills,
698 &mut promoted_reports,
699 )
700 } else {
701 None
702 };
703
704 let local_client_order_id = promoted_client_order_id.or_else(|| {
705 ctx.order_contexts
706 .get(&venue_order_id)
707 .and_then(|_| ctx.pending_submits.client_order_id(&venue_order_id))
708 });
709
710 report.client_order_id = local_client_order_id;
711 let (is_accepted, mut buffered_fills) = take_status_update_fills(&report, ctx);
712
713 buffered_fills.splice(0..0, promoted_fills);
714
715 if let Some(tracked_filled) = ctx.fill_tracker.get_cumulative_filled(&venue_order_id)
718 && report.filled_qty > tracked_filled
719 {
720 log::debug!(
721 "Capping filled_qty for {venue_order_id} from {} to {} (awaiting trade messages)",
722 report.filled_qty,
723 tracked_filled,
724 );
725 report.filled_qty = tracked_filled;
726 }
727
728 if matches!(
732 report.order_status,
733 OrderStatus::Canceled | OrderStatus::Expired
734 ) {
735 state
736 .terminal_cancel_reports
737 .insert(venue_order_id, report.clone());
738 }
739
740 let suppress_cancel = matches!(
741 report.order_status,
742 OrderStatus::Canceled | OrderStatus::Expired
743 ) && state.suppress_modify_cancel(venue_order_id);
744
745 let context = ctx.order_contexts.get(&venue_order_id);
748
749 for fill in buffered_fills {
751 match context {
752 Some(context) => {
753 emit_buffered_order_filled(&context, &fill, ctx);
754 }
755 None => emit_buffered_fill_report(fill, ctx),
756 }
757 }
758
759 for buffered in promoted_reports {
760 if matches!(
761 buffered.order_status,
762 OrderStatus::Canceled | OrderStatus::Expired
763 ) {
764 state
765 .terminal_cancel_reports
766 .insert(venue_order_id, buffered.clone());
767 }
768
769 if let Some(context) = context {
770 emit_tracked_order_status(&buffered, &context, buffered.ts_last, ctx);
771 }
772 }
773
774 if suppress_cancel {
775 log::debug!("Suppressing stale cancel for modified venue leg {venue_order_id}");
776 return;
777 }
778
779 if is_accepted || local_client_order_id.is_some() {
780 match context {
781 Some(context) => emit_tracked_order_status(&report, &context, ts_event, ctx),
782 None => ctx.emitter.send_order_status_report(report),
783 }
784 } else if let Some(report) = ctx
785 .fill_tracker
786 .accept_or_buffer_report(venue_order_id, report)
787 {
788 match ctx.order_contexts.get(&venue_order_id) {
790 Some(context) => emit_tracked_order_status(&report, &context, ts_event, ctx),
791 None => ctx.emitter.send_order_status_report(report),
792 }
793 }
794
795 if status.status == PolymarketOrderStatus::Matched
796 && let Some(trade_ids) = order.associate_trades.clone().filter(|ids| !ids.is_empty())
797 {
798 state.pending_terminal_orders.insert(
799 venue_order_id,
800 PendingTerminalOrder {
801 trade_ids,
802 ts_event,
803 },
804 );
805 emit_quantity_normalization_if_ready(venue_order_id, ctx, state);
806 }
807}
808
809fn take_status_update_fills(
813 report: &OrderStatusReport,
814 ctx: &WsDispatchContext<'_>,
815) -> (bool, Vec<BufferedFill>) {
816 let venue_order_id = report.venue_order_id;
817 let is_accepted = ctx.fill_tracker.contains(&venue_order_id);
818
819 if report.client_order_id.is_some()
820 && !is_accepted
821 && report.order_status != OrderStatus::Rejected
822 {
823 let fills = ctx.fill_tracker.register_and_take_pending_fills(
824 venue_order_id,
825 report.client_order_id,
826 report.quantity,
827 Quantity::zero(report.quantity.precision),
828 Quantity::zero(report.quantity.precision),
829 report
830 .order_side
831 .expect("order status report side must be Buy or Sell"),
832 );
833 return (true, fills);
834 }
835
836 if is_accepted {
837 let fills = ctx
838 .fill_tracker
839 .take_pending_fills(venue_order_id, report.client_order_id);
840 return (true, fills);
841 }
842
843 (false, Vec::new())
844}
845
846fn promote_modify_replacement_from_ws(
847 venue_order_id: VenueOrderId,
848 ts_event: UnixNanos,
849 ctx: &WsDispatchContext<'_>,
850 state: &mut WsDispatchState,
851 buffered_fills: &mut Vec<BufferedFill>,
852 buffered_reports: &mut Vec<OrderStatusReport>,
853) -> Option<ClientOrderId> {
854 let activity = state.promote_modify_replacement(
855 venue_order_id,
856 ctx.order_contexts,
857 ctx.fill_tracker,
858 ctx.emitter,
859 ctx.clock,
860 |old_venue_order_id| {
861 let Some(context) = ctx.order_contexts.get(&old_venue_order_id) else {
862 log::error!(
863 "Cannot promote Polymarket replacement {venue_order_id}: old venue leg {old_venue_order_id} has no context"
864 );
865 return None;
866 };
867
868 Some((context, ctx.account_id))
869 },
870 || ts_event,
871 )?;
872
873 buffered_fills.extend(activity.fills);
874 buffered_reports.extend(activity.reports);
875 Some(activity.client_order_id)
876}
877
878fn reject_modify_replacement(
879 venue_order_id: VenueOrderId,
880 report: &OrderStatusReport,
881 ts_event: UnixNanos,
882 ctx: &WsDispatchContext<'_>,
883 state: &mut WsDispatchState,
884) {
885 let Some(promotion) = state.pending_modify_promotion(venue_order_id) else {
886 return;
887 };
888
889 let Some((old_venue_order_id, cancel_ts)) = state.finish_modify_without_replacement(
890 promotion.client_order_id,
891 promotion.old_venue_order_id,
892 true,
893 ts_event,
894 ) else {
895 return;
896 };
897
898 let Some(context) = ctx.order_contexts.get(&old_venue_order_id) else {
899 return;
900 };
901
902 let reason = report
903 .cancel_reason
904 .as_deref()
905 .unwrap_or("replacement order rejected");
906 ctx.emitter.emit_order_modify_rejected_event(
907 context.identity.strategy_id,
908 context.identity.instrument_id,
909 context.identity.client_order_id,
910 Some(old_venue_order_id),
911 &sanitize_error_text(reason),
912 ts_event,
913 );
914
915 if let Some(cancel_ts) = cancel_ts {
916 emit_order_canceled(&context, old_venue_order_id, cancel_ts, ctx);
917 }
918}
919
920fn emit_buffered_order_filled(
921 context: &OrderContext,
922 buffered: &BufferedFill,
923 ctx: &WsDispatchContext<'_>,
924) {
925 let fill = &buffered.report;
926 ensure_accepted(context, fill.venue_order_id, fill.ts_event, ctx);
927
928 let info = buffered
929 .correction
930 .as_ref()
931 .and_then(|correction| correction.info.clone());
932 let filled = build_order_filled(context, fill, info, ctx);
933 ctx.fill_tracker.emit_buffered_fill(
934 filled,
935 || buffered.claim(ctx.settlement),
936 |filled, new_qty| {
937 if let Some(new_qty) = new_qty {
938 emit_buy_overfill_update(context, fill.venue_order_id, new_qty, fill.ts_event, ctx);
939 }
940
941 ctx.settlement.note_leg_enqueued(&fill.trade_id);
942 ctx.emitter.send_order_event(OrderEventAny::Filled(filled));
943 },
944 );
945}
946
947fn emit_buffered_fill_report(buffered: BufferedFill, ctx: &WsDispatchContext<'_>) {
950 let report = &buffered.report;
951 if !buffered.claim(ctx.settlement) {
952 ctx.fill_tracker
953 .reverse_fill(&report.venue_order_id, report.last_qty);
954 return;
955 }
956
957 let trade_id = report.trade_id;
958 ctx.emitter.send_fill_report(buffered.report);
959 ctx.settlement.note_leg_reported(&trade_id);
960}
961
962fn is_ready_for_terminal_normalization(
963 venue_order_id: &VenueOrderId,
964 ctx: &WsDispatchContext<'_>,
965 state: &WsDispatchState,
966) -> bool {
967 state
968 .pending_terminal_orders
969 .get(venue_order_id)
970 .is_some_and(|pending| {
971 pending
972 .trade_ids
973 .iter()
974 .all(|trade_id| ctx.settlement.authorizes_terminal_bookkeeping(trade_id))
975 })
976}
977
978fn emit_quantity_normalization_if_ready(
979 venue_order_id: VenueOrderId,
980 ctx: &WsDispatchContext<'_>,
981 state: &mut WsDispatchState,
982) {
983 if !is_ready_for_terminal_normalization(&venue_order_id, ctx, state)
984 || ctx.settlement.blocks_terminal_bookkeeping(&venue_order_id)
985 {
986 return;
987 }
988
989 let Some(pending) = state.pending_terminal_orders.remove(&venue_order_id) else {
990 return;
991 };
992
993 let Some(context) = ctx.order_contexts.get(&venue_order_id) else {
994 log::warn!("Cannot normalize terminal order {venue_order_id} without a local context");
995 return;
996 };
997
998 if let Some(quantity) = ctx
999 .fill_tracker
1000 .check_terminal_quantity_normalization(&venue_order_id)
1001 {
1002 emit_terminal_quantity_update(&context, venue_order_id, quantity, pending.ts_event, ctx);
1003 }
1004}
1005
1006fn emit_taker_terminal_status(
1013 venue_order_id: VenueOrderId,
1014 ctx: &WsDispatchContext<'_>,
1015 ts_event: UnixNanos,
1016) {
1017 let Some(context) = ctx.order_contexts.get(&venue_order_id) else {
1018 return;
1019 };
1020
1021 if context.time_in_force != TimeInForce::Fok && context.time_in_force != TimeInForce::Ioc {
1022 return;
1023 }
1024
1025 if ctx.settlement.blocks_terminal_bookkeeping(&venue_order_id) {
1026 return;
1027 }
1028
1029 if context.time_in_force == TimeInForce::Fok {
1030 if let Some(quantity) = ctx
1031 .fill_tracker
1032 .check_terminal_quantity_normalization(&venue_order_id)
1033 {
1034 emit_terminal_quantity_update(&context, venue_order_id, quantity, ts_event, ctx);
1035 }
1036 return;
1037 }
1038
1039 if context.time_in_force == TimeInForce::Ioc
1040 && let Some(remainder) = ctx
1041 .fill_tracker
1042 .take_terminal_ioc_remainder(&venue_order_id)
1043 {
1044 log::debug!(
1045 "Closing terminal IOC order {venue_order_id} as Canceled (unfilled remainder={remainder})"
1046 );
1047 emit_order_canceled(&context, venue_order_id, ts_event, ctx);
1048 }
1049}
1050
1051fn dispatch_trade_update(
1052 trade: &PolymarketUserTrade,
1053 ctx: &WsDispatchContext<'_>,
1054 state: &mut WsDispatchState,
1055) -> Option<AccountRefreshRequest> {
1056 let admitted =
1057 match admit_trade_evidence(TradeEvidence::Stream(trade), &ctx.admission_context()) {
1058 Ok(admitted) => admitted,
1059 Err(AdmissionError::UnknownInstrument(token)) => {
1060 log::warn!(
1061 "Deferring trade {} until its instrument {token} is available",
1062 trade.id
1063 );
1064 return None;
1065 }
1066 Err(AdmissionError::Unowned) => {
1067 ctx.settlement.quarantine_known_trade(&trade.id);
1068
1069 log::warn!("Stream trade {} has no owned legs", trade.id);
1070 return None;
1071 }
1072 Err(AdmissionError::Untimestamped(e) | AdmissionError::Invalid(e)) => {
1073 log::error!("Cannot admit stream evidence for trade {}: {e}", trade.id);
1074 ctx.settlement.quarantine_invalid_trade(&trade.id);
1075 return None;
1076 }
1077 };
1078
1079 let actions = ctx.settlement.admit_stream_trade(&admitted);
1080 execute_settlement_actions(actions, trade_fill_info(trade).as_ref(), ctx, state);
1081
1082 if !ctx.settlement.is_trade_confirmed(&admitted.venue_trade_id) {
1083 return None;
1084 }
1085
1086 if ctx
1089 .settlement
1090 .authorizes_terminal_bookkeeping(&admitted.venue_trade_id)
1091 {
1092 let ts_event =
1093 parse_timestamp_ms(&trade.timestamp).unwrap_or_else(|_| ctx.clock.get_time_ns());
1094 confirm_trade_bookkeeping(&admitted, ts_event, ctx, state);
1095 }
1096 Some(AccountRefreshRequest)
1097}
1098
1099pub(crate) fn execute_settlement_actions(
1101 actions: Vec<SettlementAction>,
1102 fill_info: Option<&IndexMap<Ustr, Ustr>>,
1103 ctx: &WsDispatchContext<'_>,
1104 state: &mut WsDispatchState,
1105) {
1106 for action in actions {
1107 match action {
1108 SettlementAction::ApplyLeg {
1109 venue_trade_id,
1110 leg,
1111 } => {
1112 apply_authorized_leg(&venue_trade_id, &leg, fill_info.cloned(), ctx, state);
1113 }
1114 SettlementAction::VoidAppliedFill {
1115 venue_trade_id,
1116 fill,
1117 } => {
1118 emit_void_for_applied_fill(
1119 &venue_trade_id,
1120 &fill,
1121 ctx.fill_tracker,
1122 ctx.emitter,
1123 ctx.clock,
1124 );
1125 }
1126 }
1127 }
1128}
1129
1130pub(crate) fn apply_rest_trade_evidence(
1135 trade: &PolymarketTradeReport,
1136 ctx: &WsDispatchContext<'_>,
1137 state: &mut WsDispatchState,
1138) {
1139 let admitted = match admit_trade_evidence(TradeEvidence::Rest(trade), &ctx.admission_context())
1140 {
1141 Ok(admitted) => admitted,
1142 Err(e) => {
1143 log::warn!(
1144 "Targeted REST evidence for Polymarket trade {} did not pass admission: {e}; \
1145 retrying",
1146 trade.id
1147 );
1148 return;
1149 }
1150 };
1151
1152 apply_admitted_rest_trade(&admitted, trade, ctx, state);
1153}
1154
1155pub(crate) fn apply_uncertain_order_evidence(
1165 venue_order_id: VenueOrderId,
1166 order: &PolymarketOpenOrder,
1167 trades: &[PolymarketTradeReport],
1168 ctx: &WsDispatchContext<'_>,
1169 state: &mut WsDispatchState,
1170) -> bool {
1171 let Some((context, instrument)) = submitted_order_context(venue_order_id, order, ctx) else {
1172 return false;
1173 };
1174
1175 let trades_applied =
1176 apply_order_trade_evidence(venue_order_id, order, trades, ctx, |admitted, trade| {
1177 let actions = ctx.settlement.admit_rest_result(admitted);
1178 execute_settlement_actions(actions, rest_trade_info(trade).as_ref(), ctx, state);
1179 });
1180
1181 if !trades_applied {
1182 return false;
1183 }
1184
1185 let report = match parse_order_status_report(
1186 order,
1187 instrument.id(),
1188 ctx.account_id,
1189 ctx.pending_submits.client_order_id(&venue_order_id),
1190 instrument.price_precision(),
1191 instrument.size_precision(),
1192 ctx.clock.get_time_ns(),
1193 ) {
1194 Ok(report) => report,
1195 Err(e) => {
1196 log::warn!("Cannot apply REST status of uncertain order {venue_order_id}: {e}");
1197 return false;
1198 }
1199 };
1200
1201 let (_, buffered_fills) = take_status_update_fills(&report, ctx);
1202
1203 for fill in buffered_fills {
1204 emit_buffered_order_filled(&context, &fill, ctx);
1205 }
1206
1207 if ctx.order_contexts.is_closed(venue_order_id)
1208 || (report.order_status == OrderStatus::Canceled
1209 && state.suppress_modify_cancel_reemit(venue_order_id))
1210 {
1211 return true;
1212 }
1213
1214 let faulted = order_trades_block_terminal_bookkeeping(venue_order_id, trades, ctx);
1215 let inferred_cancel = order.status == PolymarketOrderStatus::Matched
1217 && report.order_status == OrderStatus::Canceled;
1218
1219 if faulted && inferred_cancel {
1220 ensure_accepted(&context, venue_order_id, report.ts_last, ctx);
1221 } else {
1222 emit_tracked_order_status(&report, &context, report.ts_last, ctx);
1223 }
1224
1225 if report.order_status == OrderStatus::Filled && !faulted {
1228 emit_taker_terminal_status(venue_order_id, ctx, report.ts_last);
1229 }
1230
1231 true
1232}
1233
1234fn order_trades_block_terminal_bookkeeping(
1235 venue_order_id: VenueOrderId,
1236 trades: &[PolymarketTradeReport],
1237 ctx: &WsDispatchContext<'_>,
1238) -> bool {
1239 ctx.settlement.blocks_terminal_bookkeeping(&venue_order_id)
1240 || trades.iter().any(|trade| {
1241 trade_touches_order(trade, venue_order_id)
1242 && ctx.settlement.is_trade_hard_faulted(&trade.id)
1243 })
1244}
1245
1246fn trade_touches_order(trade: &PolymarketTradeReport, venue_order_id: VenueOrderId) -> bool {
1247 trade.taker_order_id == venue_order_id.as_str()
1248 || trade
1249 .maker_orders
1250 .iter()
1251 .any(|maker| maker.order_id == venue_order_id.as_str())
1252}
1253
1254pub(crate) fn apply_stream_gap_order_evidence(
1261 venue_order_id: VenueOrderId,
1262 order: &PolymarketOpenOrder,
1263 trades: &[PolymarketTradeReport],
1264 ctx: &WsDispatchContext<'_>,
1265 state: &mut WsDispatchState,
1266) -> bool {
1267 if submitted_order_context(venue_order_id, order, ctx).is_none() {
1268 return false;
1269 }
1270
1271 apply_order_trade_evidence(venue_order_id, order, trades, ctx, |admitted, trade| {
1272 if ctx.settlement.knows_trade(admitted) {
1273 return;
1274 }
1275
1276 log::info!(
1277 "Discovered Polymarket trade {} on order {venue_order_id} missed during a user \
1278 stream disconnect",
1279 admitted.venue_trade_id
1280 );
1281 apply_admitted_rest_trade(admitted, trade, ctx, state);
1282 })
1283}
1284
1285fn apply_admitted_rest_trade(
1286 admitted: &AdmittedTrade,
1287 trade: &PolymarketTradeReport,
1288 ctx: &WsDispatchContext<'_>,
1289 state: &mut WsDispatchState,
1290) {
1291 let actions = ctx.settlement.admit_rest_result(admitted);
1292 execute_settlement_actions(actions, rest_trade_info(trade).as_ref(), ctx, state);
1293
1294 if ctx
1295 .settlement
1296 .authorizes_terminal_bookkeeping(&admitted.venue_trade_id)
1297 {
1298 confirm_trade_bookkeeping(admitted, ctx.clock.get_time_ns(), ctx, state);
1299 }
1300}
1301
1302fn submitted_order_context(
1305 venue_order_id: VenueOrderId,
1306 order: &PolymarketOpenOrder,
1307 ctx: &WsDispatchContext<'_>,
1308) -> Option<(OrderContext, InstrumentAny)> {
1309 let (Some(context), Some(instrument)) = (
1310 ctx.order_contexts.get(&venue_order_id),
1311 ctx.token_instruments.get_cloned(&order.asset_id),
1312 ) else {
1313 return None;
1314 };
1315
1316 if order.id != venue_order_id.as_str()
1317 || instrument.id() != context.identity.instrument_id
1318 || OrderSide::from(order.side) != context.identity.order_side
1319 {
1320 log::warn!(
1321 "REST order evidence for uncertain order {venue_order_id} contradicts the submitted \
1322 order"
1323 );
1324 return None;
1325 }
1326
1327 Some((context, instrument))
1328}
1329
1330fn apply_order_trade_evidence(
1335 venue_order_id: VenueOrderId,
1336 order: &PolymarketOpenOrder,
1337 trades: &[PolymarketTradeReport],
1338 ctx: &WsDispatchContext<'_>,
1339 mut apply: impl FnMut(&AdmittedTrade, &PolymarketTradeReport),
1340) -> bool {
1341 let Some(order_trades) = uncertain_order_trades(venue_order_id, trades) else {
1342 return false;
1343 };
1344
1345 if order_trades
1346 .iter()
1347 .any(|trade| trade.status.is_pending_settlement())
1348 {
1349 return false;
1350 }
1351
1352 let mut confirmed_qty = Decimal::ZERO;
1353
1354 for trade in order_trades {
1355 let admitted =
1356 match admit_trade_evidence(TradeEvidence::Rest(trade), &ctx.admission_context()) {
1357 Ok(admitted) => admitted,
1358 Err(e) => {
1359 log::warn!(
1360 "Cannot admit REST evidence for trade {} of uncertain order \
1361 {venue_order_id}: {e}",
1362 trade.id
1363 );
1364 return false;
1365 }
1366 };
1367
1368 if admitted.status == PolymarketTradeStatus::Confirmed {
1369 confirmed_qty += admitted
1370 .legs
1371 .iter()
1372 .filter(|leg| leg.venue_order_id == venue_order_id)
1373 .map(|leg| leg.last_qty.as_decimal())
1374 .sum::<Decimal>();
1375 }
1376
1377 apply(&admitted, trade);
1378 }
1379
1380 if confirmed_qty < order.size_matched {
1382 log::debug!(
1383 "Confirmed REST trades cover {confirmed_qty} of {} matched for uncertain order \
1384 {venue_order_id}; retrying",
1385 order.size_matched
1386 );
1387 return false;
1388 }
1389
1390 true
1391}
1392
1393fn uncertain_order_trades(
1396 venue_order_id: VenueOrderId,
1397 trades: &[PolymarketTradeReport],
1398) -> Option<Vec<&PolymarketTradeReport>> {
1399 let mut selected = AHashMap::new();
1400 let mut order_trades = Vec::new();
1401
1402 for trade in trades.iter().filter(|trade| {
1403 trade.taker_order_id == venue_order_id.as_str()
1404 || trade
1405 .maker_orders
1406 .iter()
1407 .any(|maker| maker.order_id == venue_order_id.as_str())
1408 }) {
1409 match admit_selected_trade(&mut selected, trade) {
1410 Ok(true) => order_trades.push(trade),
1411 Ok(false) => {}
1412 Err(e) => {
1413 log::warn!(
1414 "REST evidence for uncertain order {venue_order_id} is contradictory: {e}"
1415 );
1416 return None;
1417 }
1418 }
1419 }
1420
1421 Some(order_trades)
1422}
1423
1424fn apply_authorized_leg(
1428 venue_trade_id: &str,
1429 leg: &AdmittedLeg,
1430 fill_info: Option<IndexMap<Ustr, Ustr>>,
1431 ctx: &WsDispatchContext<'_>,
1432 state: &mut WsDispatchState,
1433) {
1434 let venue_order_id = leg.venue_order_id;
1435
1436 if state
1438 .reconciled_fills
1439 .contains(&(leg.trade_id, venue_order_id))
1440 {
1441 ctx.settlement.note_leg_reported(&leg.trade_id);
1442 return;
1443 }
1444
1445 let mut report = leg.fill_report(ctx.account_id, ctx.clock.get_time_ns());
1446 report.client_order_id = ctx.pending_submits.client_order_id(&venue_order_id);
1447
1448 let correction_info = fill_info.clone();
1449
1450 let correction = FillCorrectionMetadata {
1451 venue_trade_id: venue_trade_id.to_string(),
1452 info: fill_info,
1453 };
1454
1455 let mut promoted_reports = Vec::new();
1456
1457 if state.pending_modify_promotion(venue_order_id).is_some() {
1458 let mut buffered_fills = Vec::new();
1459 report.client_order_id = promote_modify_replacement_from_ws(
1460 venue_order_id,
1461 report.ts_event,
1462 ctx,
1463 state,
1464 &mut buffered_fills,
1465 &mut promoted_reports,
1466 );
1467 emit_promoted_ws_fills(venue_order_id, buffered_fills, ctx);
1468 }
1469
1470 if let Some(report) = ctx
1471 .fill_tracker
1472 .accept_or_buffer_fill(venue_order_id, report, correction)
1473 {
1474 match ctx.order_contexts.get(&venue_order_id) {
1475 Some(context) => {
1476 emit_order_filled(&context, &report, correction_info, ctx);
1477 }
1478 None => {
1479 ctx.emitter.send_fill_report(report);
1480 ctx.settlement.note_leg_reported(&leg.trade_id);
1481 }
1482 }
1483
1484 reemit_terminal_cancel(venue_order_id, state, ctx);
1485 }
1486
1487 emit_promoted_ws_reports(venue_order_id, promoted_reports, ctx, state);
1488}
1489
1490fn confirm_trade_bookkeeping(
1493 admitted: &AdmittedTrade,
1494 ts_event: UnixNanos,
1495 ctx: &WsDispatchContext<'_>,
1496 state: &mut WsDispatchState,
1497) {
1498 for leg in &admitted.legs {
1499 emit_quantity_normalization_if_ready(leg.venue_order_id, ctx, state);
1500
1501 if leg.liquidity_side == LiquiditySide::Taker {
1502 emit_taker_terminal_status(leg.venue_order_id, ctx, ts_event);
1503 }
1504 }
1505}
1506
1507fn emit_promoted_ws_fills(
1508 venue_order_id: VenueOrderId,
1509 buffered_fills: Vec<BufferedFill>,
1510 ctx: &WsDispatchContext<'_>,
1511) {
1512 let context = ctx.order_contexts.get(&venue_order_id);
1513
1514 for fill in buffered_fills {
1515 match context {
1516 Some(context) => emit_buffered_order_filled(&context, &fill, ctx),
1517 None => emit_buffered_fill_report(fill, ctx),
1518 }
1519 }
1520}
1521
1522fn emit_promoted_ws_reports(
1523 venue_order_id: VenueOrderId,
1524 buffered_reports: Vec<OrderStatusReport>,
1525 ctx: &WsDispatchContext<'_>,
1526 state: &mut WsDispatchState,
1527) {
1528 let context = ctx.order_contexts.get(&venue_order_id);
1529
1530 for report in buffered_reports {
1531 if matches!(
1532 report.order_status,
1533 OrderStatus::Canceled | OrderStatus::Expired
1534 ) {
1535 state.record_terminal_cancel_report(report.clone());
1536 }
1537
1538 match context {
1539 Some(context) => emit_tracked_order_status(&report, &context, report.ts_last, ctx),
1540 None => ctx.emitter.send_order_status_report(report),
1541 }
1542 }
1543}
1544
1545fn reemit_terminal_cancel(
1555 venue_order_id: VenueOrderId,
1556 state: &WsDispatchState,
1557 ctx: &WsDispatchContext<'_>,
1558) {
1559 if ctx.fill_tracker.is_fully_filled(&venue_order_id) {
1560 return;
1561 }
1562
1563 if state.suppress_modify_cancel_reemit(venue_order_id) {
1564 return;
1565 }
1566
1567 let cancel_ts = state
1568 .closed_modify_venue_order_ids
1569 .get(&venue_order_id)
1570 .copied()
1571 .or_else(|| {
1572 state
1573 .terminal_cancel_reports
1574 .get(&venue_order_id)
1575 .map(|report| report.ts_last)
1576 });
1577
1578 if let Some(cancel_ts) = cancel_ts {
1579 log::debug!("Re-emitting cancel for {venue_order_id} after fill to restore terminal state");
1580 match ctx.order_contexts.get(&venue_order_id) {
1581 Some(context) => {
1582 if state
1583 .terminal_cancel_reports
1584 .get(&venue_order_id)
1585 .is_some_and(|report| report.order_status == OrderStatus::Expired)
1586 {
1587 emit_order_expired(&context, venue_order_id, cancel_ts, ctx);
1588 } else {
1589 emit_order_canceled(&context, venue_order_id, cancel_ts, ctx);
1590 }
1591 }
1592 None => {
1593 if let Some(cancel_report) = state.terminal_cancel_reports.get(&venue_order_id) {
1594 ctx.emitter.send_order_status_report(cancel_report.clone());
1595 }
1596 }
1597 }
1598 }
1599}
1600
1601fn build_ws_order_status_report(
1602 order: &PolymarketUserOrder,
1603 status: &PolymarketUserOrderStatus,
1604 order_type: PolymarketOrderType,
1605 instrument: &InstrumentAny,
1606 account_id: AccountId,
1607 ts_event: UnixNanos,
1608 ts_init: UnixNanos,
1609) -> anyhow::Result<OrderStatusReport> {
1610 let venue_order_id = VenueOrderId::from(order.id.as_str());
1611 let order_status = gtd_expiry_status(
1612 crate::execution::parse::resolve_order_status(status.status, order.event_type),
1613 order_type,
1614 order.expiration.as_deref(),
1615 ts_event,
1616 );
1617 let order_side = OrderSide::from(order.side);
1618 let time_in_force = TimeInForce::from(order_type);
1619 let size_precision = instrument.size_precision();
1620 let price_precision = instrument.price_precision();
1621 let price_dec = parse_decimal_exact(&order.price)?;
1622 anyhow::ensure!(
1623 price_dec > Decimal::ZERO && price_dec < Decimal::ONE,
1624 "order price must be in (0, 1)"
1625 );
1626 let quantity_dec = parse_decimal_exact(&order.original_size)?;
1627 let filled_dec = if order.size_matched.is_empty() {
1629 Decimal::ZERO
1630 } else {
1631 parse_decimal_exact(&order.size_matched)?
1632 };
1633 anyhow::ensure!(
1634 quantity_dec > Decimal::ZERO && filled_dec >= Decimal::ZERO,
1635 "invalid order quantity"
1636 );
1637 let quantity = Quantity::from_decimal_dp(
1638 original_size_to_shares(quantity_dec, price_dec, order.side, order_type)?,
1639 size_precision,
1640 )?;
1641 let filled_qty = Quantity::from_decimal_dp(filled_dec, size_precision)?;
1642 let price = Price::from_decimal_dp(price_dec, price_precision)?;
1643
1644 let mut report = OrderStatusReport::new(
1645 account_id,
1646 instrument.id(),
1647 None,
1648 venue_order_id,
1649 order_side.into(),
1650 OrderType::Limit,
1651 time_in_force,
1652 order_status,
1653 quantity,
1654 filled_qty,
1655 ts_event,
1656 ts_event,
1657 ts_init,
1658 None,
1659 );
1660 report.price = Some(price);
1661 report.expire_time = order
1662 .expiration
1663 .as_deref()
1664 .and_then(crate::execution::parse::parse_expiration_nanos)
1665 .map(UnixNanos::from);
1666
1667 if order_status == OrderStatus::Rejected {
1668 report.cancel_reason.clone_from(&status.reason);
1669 }
1670
1671 Ok(report)
1672}
1673
1674fn gtd_expiry_status(
1680 status: OrderStatus,
1681 order_type: PolymarketOrderType,
1682 expiration: Option<&str>,
1683 ts_event: UnixNanos,
1684) -> OrderStatus {
1685 const SECURITY_THRESHOLD_NS: u64 = 60_000_000_000;
1686
1687 if status != OrderStatus::Canceled || order_type != PolymarketOrderType::GTD {
1688 return status;
1689 }
1690
1691 let Some(expire_ns) = expiration.and_then(crate::execution::parse::parse_expiration_nanos)
1692 else {
1693 return status;
1694 };
1695
1696 if ts_event.as_u64().saturating_add(SECURITY_THRESHOLD_NS) >= expire_ns {
1697 OrderStatus::Expired
1698 } else {
1699 status
1700 }
1701}
1702
1703fn original_size_to_shares(
1714 original_size: Decimal,
1715 price: Decimal,
1716 side: PolymarketOrderSide,
1717 order_type: PolymarketOrderType,
1718) -> anyhow::Result<Decimal> {
1719 if side != PolymarketOrderSide::Buy
1720 || !matches!(
1721 order_type,
1722 PolymarketOrderType::FAK | PolymarketOrderType::FOK
1723 )
1724 {
1725 return Ok(original_size);
1726 }
1727
1728 original_size
1729 .checked_div(price)
1730 .context("order share quantity overflow")
1731}
1732
1733fn emit_tracked_order_status(
1739 report: &OrderStatusReport,
1740 context: &OrderContext,
1741 ts_event: UnixNanos,
1742 ctx: &WsDispatchContext<'_>,
1743) {
1744 let venue_order_id = report.venue_order_id;
1745 match report.order_status {
1746 OrderStatus::Accepted => ensure_accepted(context, venue_order_id, ts_event, ctx),
1747 OrderStatus::PartiallyFilled | OrderStatus::Filled => {
1748 ensure_accepted(context, venue_order_id, ts_event, ctx);
1749 }
1750 OrderStatus::Canceled => {
1751 ensure_accepted(context, venue_order_id, ts_event, ctx);
1752 emit_order_canceled(context, venue_order_id, ts_event, ctx);
1753 }
1754 OrderStatus::Expired => {
1755 ensure_accepted(context, venue_order_id, ts_event, ctx);
1756 emit_order_expired(context, venue_order_id, ts_event, ctx);
1757 }
1758 OrderStatus::Rejected => {
1759 let reason = report
1760 .cancel_reason
1761 .clone()
1762 .unwrap_or_else(|| "REJECTED".to_string());
1763
1764 emit_order_rejected(context, &reason, ts_event, ctx);
1765 }
1766 other => log::debug!("No order event for status {other:?} on {venue_order_id}"),
1767 }
1768}
1769
1770fn ensure_accepted(
1775 context: &OrderContext,
1776 venue_order_id: VenueOrderId,
1777 ts_event: UnixNanos,
1778 ctx: &WsDispatchContext<'_>,
1779) {
1780 if !ctx.order_contexts.mark_accepted(venue_order_id)
1781 || ctx.order_contexts.is_closed(venue_order_id)
1782 {
1783 return;
1784 }
1785
1786 let accepted = OrderAccepted::new(
1787 ctx.emitter.trader_id(),
1788 context.identity.strategy_id,
1789 context.identity.instrument_id,
1790 context.identity.client_order_id,
1791 venue_order_id,
1792 ctx.account_id,
1793 UUID4::new(),
1794 ts_event,
1795 ctx.clock.get_time_ns(),
1796 false,
1797 );
1798 ctx.emitter
1799 .send_order_event(OrderEventAny::Accepted(accepted));
1800}
1801
1802fn emit_order_filled(
1807 context: &OrderContext,
1808 fill: &FillReport,
1809 info: Option<IndexMap<Ustr, Ustr>>,
1810 ctx: &WsDispatchContext<'_>,
1811) {
1812 ensure_accepted(context, fill.venue_order_id, fill.ts_event, ctx);
1813
1814 if let Some(new_qty) = ctx.fill_tracker.buy_overfill_bump(&fill.venue_order_id) {
1815 emit_buy_overfill_update(context, fill.venue_order_id, new_qty, fill.ts_event, ctx);
1816 }
1817
1818 let filled = build_order_filled(context, fill, info, ctx);
1819
1820 ctx.settlement.note_leg_enqueued(&fill.trade_id);
1822 ctx.emitter.send_order_event(OrderEventAny::Filled(filled));
1823}
1824
1825fn build_order_filled(
1826 context: &OrderContext,
1827 fill: &FillReport,
1828 info: Option<IndexMap<Ustr, Ustr>>,
1829 ctx: &WsDispatchContext<'_>,
1830) -> OrderFilled {
1831 OrderFilled::new(
1832 ctx.emitter.trader_id(),
1833 context.identity.strategy_id,
1834 context.identity.instrument_id,
1835 context.identity.client_order_id,
1836 fill.venue_order_id,
1837 ctx.account_id,
1838 fill.trade_id,
1839 context.identity.order_side,
1840 context.identity.order_type,
1841 fill.last_qty,
1842 fill.last_px,
1843 get_pusd_currency(),
1844 fill.liquidity_side,
1845 UUID4::new(),
1846 fill.ts_event,
1847 fill.ts_init,
1848 false,
1849 fill.venue_position_id,
1850 Some(fill.commission),
1851 info,
1852 )
1853}
1854
1855pub(crate) fn emit_void_for_applied_fill(
1858 venue_trade_id: &str,
1859 fill: &OrderFilled,
1860 tracker: &OrderFillTrackerMap,
1861 emitter: &ExecutionEventEmitter,
1862 clock: &'static AtomicTime,
1863) {
1864 let ts_event = clock.get_time_ns();
1865
1866 let mut voided = OrderFillVoided::new(
1867 fill.trader_id,
1868 fill.strategy_id,
1869 fill.instrument_id,
1870 fill.client_order_id,
1871 fill.venue_order_id,
1872 fill.account_id,
1873 Ustr::from(&format!("{venue_trade_id}-FAILED-{}", fill.client_order_id)),
1874 fill.trade_id,
1875 fill.last_qty,
1876 fill.commission.clone(),
1877 fill.order_side,
1878 fill.order_type,
1879 fill.last_px,
1880 fill.currency.clone(),
1881 fill.liquidity_side,
1882 fill.position_id,
1883 Some(Ustr::from("FAILED")),
1884 fill.info.clone(),
1885 UUID4::new(),
1886 ts_event,
1887 clock.get_time_ns(),
1888 false,
1889 false,
1890 );
1891 voided.causation_id = Some(fill.event_id);
1892 tracker.reverse_fill(&fill.venue_order_id, fill.last_qty);
1893 emitter.send_order_event(OrderEventAny::FillVoided(voided));
1894}
1895
1896pub(crate) fn rest_trade_info(trade: &PolymarketTradeReport) -> Option<IndexMap<Ustr, Ustr>> {
1899 let value = serde_json::to_value(trade).ok()?;
1900 flatten_trade_value(&value)
1901}
1902
1903fn trade_fill_info(trade: &PolymarketUserTrade) -> Option<IndexMap<Ustr, Ustr>> {
1908 let value = serde_json::to_value(trade).ok()?;
1909 flatten_trade_value(&value)
1910}
1911
1912fn flatten_trade_value(value: &serde_json::Value) -> Option<IndexMap<Ustr, Ustr>> {
1913 let object = value.as_object()?;
1914 let mut info = IndexMap::with_capacity(object.len());
1915 for (key, val) in object {
1916 let val_str = match val {
1917 serde_json::Value::String(s) => s.clone(),
1918 other => other.to_string(),
1919 };
1920 info.insert(Ustr::from(key.as_str()), Ustr::from(val_str.as_str()));
1921 }
1922 Some(info)
1923}
1924
1925fn emit_buy_overfill_update(
1931 context: &OrderContext,
1932 venue_order_id: VenueOrderId,
1933 new_qty: Quantity,
1934 ts_event: UnixNanos,
1935 ctx: &WsDispatchContext<'_>,
1936) {
1937 let updated = OrderUpdated::new(
1938 ctx.emitter.trader_id(),
1939 context.identity.strategy_id,
1940 context.identity.instrument_id,
1941 context.identity.client_order_id,
1942 new_qty,
1943 UUID4::new(),
1944 ts_event,
1945 ctx.clock.get_time_ns(),
1946 false,
1947 Some(venue_order_id),
1948 Some(ctx.account_id),
1949 None,
1950 None,
1951 None,
1952 false,
1953 );
1954 ctx.emitter
1955 .send_order_event(OrderEventAny::Updated(updated));
1956}
1957
1958fn emit_terminal_quantity_update(
1960 context: &OrderContext,
1961 venue_order_id: VenueOrderId,
1962 quantity: Quantity,
1963 ts_event: UnixNanos,
1964 ctx: &WsDispatchContext<'_>,
1965) {
1966 let updated = OrderUpdated::new(
1967 ctx.emitter.trader_id(),
1968 context.identity.strategy_id,
1969 context.identity.instrument_id,
1970 context.identity.client_order_id,
1971 quantity,
1972 UUID4::new(),
1973 ts_event,
1974 ctx.clock.get_time_ns(),
1975 true,
1976 Some(venue_order_id),
1977 Some(ctx.account_id),
1978 None,
1979 None,
1980 None,
1981 false,
1982 );
1983 ctx.emitter
1984 .send_order_event(OrderEventAny::Updated(updated));
1985}
1986
1987fn emit_order_canceled(
1988 context: &OrderContext,
1989 venue_order_id: VenueOrderId,
1990 ts_event: UnixNanos,
1991 ctx: &WsDispatchContext<'_>,
1992) {
1993 let canceled = OrderCanceled::new(
1994 ctx.emitter.trader_id(),
1995 context.identity.strategy_id,
1996 context.identity.instrument_id,
1997 context.identity.client_order_id,
1998 UUID4::new(),
1999 ts_event,
2000 ctx.clock.get_time_ns(),
2001 false,
2002 Some(venue_order_id),
2003 Some(ctx.account_id),
2004 None,
2005 );
2006 ctx.emitter
2007 .send_order_event(OrderEventAny::Canceled(canceled));
2008}
2009
2010fn emit_order_expired(
2011 context: &OrderContext,
2012 venue_order_id: VenueOrderId,
2013 ts_event: UnixNanos,
2014 ctx: &WsDispatchContext<'_>,
2015) {
2016 let expired = OrderExpired::new(
2017 ctx.emitter.trader_id(),
2018 context.identity.strategy_id,
2019 context.identity.instrument_id,
2020 context.identity.client_order_id,
2021 UUID4::new(),
2022 ts_event,
2023 ctx.clock.get_time_ns(),
2024 false,
2025 Some(venue_order_id),
2026 Some(ctx.account_id),
2027 );
2028 ctx.emitter
2029 .send_order_event(OrderEventAny::Expired(expired));
2030}
2031
2032fn emit_order_rejected(
2033 context: &OrderContext,
2034 reason: &str,
2035 ts_event: UnixNanos,
2036 ctx: &WsDispatchContext<'_>,
2037) {
2038 let reason = sanitize_error_text(reason);
2039
2040 let rejected = OrderRejected::new(
2041 ctx.emitter.trader_id(),
2042 context.identity.strategy_id,
2043 context.identity.instrument_id,
2044 context.identity.client_order_id,
2045 ctx.account_id,
2046 Ustr::from(&reason),
2047 UUID4::new(),
2048 ts_event,
2049 ctx.clock.get_time_ns(),
2050 false,
2051 is_post_only_crossing(&reason),
2052 );
2053 ctx.emitter
2054 .send_order_event(OrderEventAny::Rejected(rejected));
2055}
2056
2057#[cfg(test)]
2058mod tests {
2059 use futures_util::FutureExt;
2060 use nautilus_common::messages::{ExecutionEvent, ExecutionReport};
2061 use nautilus_core::{Params, time::AtomicTime};
2062 use nautilus_live::execution::context::OrderIdentity;
2063 use nautilus_model::{
2064 enums::{AccountType, LiquiditySide, OrderSide, OrderStatus},
2065 events::OrderEventAny,
2066 identifiers::{ClientOrderId, InstrumentId, StrategyId, TraderId},
2067 orders::{Order, builder::OrderTestBuilder},
2068 types::{Currency, Money},
2069 };
2070 use rstest::rstest;
2071 use rust_decimal_macros::dec;
2072
2073 use super::*;
2074 use crate::{
2075 common::enums::{PolymarketLiquiditySide, PolymarketOutcome, PolymarketTradeStatus},
2076 execution::settlement::{
2077 admission::AdmittedTrade,
2078 registry::tests::{settlement_state, trade_hard_fault},
2079 state::SettlementState,
2080 },
2081 http::{
2082 models::GammaMarket,
2083 parse::{create_instrument_from_def, parse_gamma_market},
2084 },
2085 };
2086
2087 fn register_context(
2089 order_contexts: &OrderContextRegistry,
2090 venue_order_id: VenueOrderId,
2091 instrument_id: InstrumentId,
2092 client_order_id: &str,
2093 ) {
2094 order_contexts.register_context(
2095 venue_order_id,
2096 OrderContext {
2097 identity: OrderIdentity {
2098 client_order_id: ClientOrderId::from(client_order_id),
2099 strategy_id: StrategyId::from("S-001"),
2100 instrument_id,
2101 order_side: OrderSide::Buy,
2102 order_type: OrderType::Limit,
2103 },
2104 quantity: Quantity::from("10"),
2105 price: Some(Price::from("0.50")),
2106 trigger_price: None,
2107 trigger_type: None,
2108 time_in_force: TimeInForce::Gtc,
2109 is_post_only: false,
2110 is_reduce_only: false,
2111 is_quote_quantity: false,
2112 },
2113 );
2114 }
2115
2116 fn load<T: serde::de::DeserializeOwned>(filename: &str) -> T {
2117 let path = format!("test_data/{filename}");
2118 let content = std::fs::read_to_string(path).expect("Failed to read test data");
2119 serde_json::from_str(&content).expect("Failed to parse test data")
2120 }
2121
2122 fn test_instrument() -> InstrumentAny {
2123 let market: GammaMarket = load("gamma_market.json");
2124 let defs = parse_gamma_market(&market).unwrap();
2125 create_instrument_from_def(&defs[0], UnixNanos::from(1_000_000_000u64)).unwrap()
2126 }
2127
2128 fn bind_instrument(
2129 mut instrument: InstrumentAny,
2130 market: &str,
2131 outcome: PolymarketOutcome,
2132 ) -> InstrumentAny {
2133 let InstrumentAny::BinaryOption(binary) = &mut instrument else {
2134 panic!("expected BinaryOption test instrument");
2135 };
2136
2137 binary.outcome = Some(Ustr::from(outcome.as_str()));
2138 let mut info = binary.info.as_deref().cloned().unwrap_or_default();
2139 info.insert(
2140 "condition_id".to_string(),
2141 serde_json::Value::String(market.to_string()),
2142 );
2143 binary.info = Some(Params::new(info));
2144 instrument
2145 }
2146
2147 fn instrument_for_trade(trade: &PolymarketUserTrade) -> InstrumentAny {
2148 bind_instrument(test_instrument(), trade.market.as_str(), trade.outcome)
2149 }
2150
2151 fn set_taker_fee_rate(instrument: &mut InstrumentAny, rate: Decimal) {
2152 set_taker_fee_schedule(instrument, rate, Decimal::ONE);
2153 }
2154
2155 fn set_taker_fee_schedule(instrument: &mut InstrumentAny, rate: Decimal, exponent: Decimal) {
2156 let InstrumentAny::BinaryOption(binary) = instrument else {
2157 panic!("expected binary option test instrument");
2158 };
2159
2160 let mut info = binary.info.as_deref().cloned().unwrap_or_default();
2161 info.insert(
2162 "fee_schedule".into(),
2163 serde_json::json!({
2164 "exponent": exponent.to_string(),
2165 "rate": rate.to_string(),
2166 "takerOnly": true,
2167 "rebateRate": "0",
2168 }),
2169 );
2170 binary.info = Some(Params::new(info));
2171 }
2172
2173 fn test_emitter() -> ExecutionEventEmitter {
2174 ExecutionEventEmitter::new(
2175 nautilus_core::time::get_atomic_clock_realtime(),
2176 TraderId::from("TESTER-001"),
2177 AccountId::from("POLY-001"),
2178 AccountType::Cash,
2179 Some(Currency::pUSD()),
2180 )
2181 }
2182
2183 #[rstest]
2184 #[case::matching(|_: &mut PolymarketUserTrade, _: &mut InstrumentAny| {}, false, dec!(0))]
2185 #[case::quantity(|trade: &mut PolymarketUserTrade, _: &mut InstrumentAny| {
2186 trade.size = "26.0".to_string();
2187 trade.maker_orders[0].matched_amount = dec!(26);
2188 }, true, dec!(0))]
2189 #[case::price(|trade: &mut PolymarketUserTrade, _: &mut InstrumentAny| {
2190 trade.price = "0.6".to_string();
2191 trade.maker_orders[0].price = dec!(0.6);
2192 }, true, dec!(0))]
2193 #[case::side(|trade: &mut PolymarketUserTrade, _: &mut InstrumentAny| trade.side = PolymarketOrderSide::Sell, true, dec!(0))]
2194 #[case::fee_schedule(|_: &mut PolymarketUserTrade, instrument: &mut InstrumentAny| set_taker_fee_rate(instrument, dec!(0.01)), false, dec!(0))]
2195 fn test_dispatch_conflicting_trade_after_applied_fill(
2196 #[case] change: fn(&mut PolymarketUserTrade, &mut InstrumentAny),
2197 #[case] conflicts: bool,
2198 #[case] commission: Decimal,
2199 ) {
2200 let mut trade: PolymarketUserTrade = load("ws_user_trade.json");
2201 trade.status = PolymarketTradeStatus::Matched;
2202 let instrument = instrument_for_trade(&trade);
2203 let token_instruments = AtomicMap::new();
2204 token_instruments.insert(trade.asset_id, instrument.clone());
2205 let venue_order_id = VenueOrderId::from(trade.taker_order_id.as_str());
2206 let fill_tracker = OrderFillTrackerMap::new();
2207 fill_tracker.register(
2208 venue_order_id,
2209 Quantity::from("100.000000"),
2210 OrderSide::Buy,
2211 instrument.id(),
2212 instrument.size_precision(),
2213 instrument.price_precision(),
2214 );
2215 let pending_submits = PendingSubmitTracker::default();
2216 let order_contexts = OrderContextRegistry::default();
2217 register_context(
2218 &order_contexts,
2219 venue_order_id,
2220 instrument.id(),
2221 "O-CONFLICT",
2222 );
2223 order_contexts.mark_accepted(venue_order_id);
2224 let mut emitter = test_emitter();
2225 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
2226 emitter.set_sender(sender);
2227 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
2228 settlement.begin_session();
2229 settlement.note_order_submitted(venue_order_id);
2230
2231 let ctx = WsDispatchContext {
2232 signer_type: PolymarketSignerType::Owner,
2233 token_instruments: &token_instruments,
2234 fill_tracker: &fill_tracker,
2235 settlement: &settlement,
2236 pending_submits: &pending_submits,
2237 order_contexts: &order_contexts,
2238 emitter: &emitter,
2239 account_id: AccountId::from("POLY-001"),
2240 clock: nautilus_core::time::get_atomic_clock_realtime(),
2241 user_address: trade.maker_address.as_str(),
2242 user_api_key: trade.owner.as_str(),
2243 };
2244
2245 let mut state = WsDispatchState::default();
2246 dispatch_user_message(&UserWsMessage::Trade(trade.clone()), &ctx, &mut state);
2247
2248 let ExecutionEvent::Order(OrderEventAny::Filled(fill)) = receiver.try_recv().unwrap()
2249 else {
2250 panic!("expected original fill");
2251 };
2252
2253 settlement.observe_fill_applied(&fill);
2254 let mut incoming = trade.clone();
2255 incoming.status = PolymarketTradeStatus::Confirmed;
2256 let mut updated_instrument = instrument;
2257 change(&mut incoming, &mut updated_instrument);
2258 token_instruments.insert(trade.asset_id, updated_instrument);
2259 let admitted =
2260 admit_trade_evidence(TradeEvidence::Stream(&incoming), &ctx.admission_context())
2261 .unwrap();
2262
2263 dispatch_user_message(&UserWsMessage::Trade(incoming), &ctx, &mut state);
2264
2265 assert_eq!(fill.trade_id, TradeId::from(trade.id.as_str()));
2266 assert_eq!(fill.last_qty, Quantity::from("25.000000"));
2267 assert_eq!(fill.last_px.as_decimal(), dec!(0.5));
2268 assert_eq!(fill.order_side, OrderSide::Buy);
2269 assert_eq!(fill.commission, Some(Money::zero(Currency::pUSD())));
2270 assert_eq!(admitted.legs[0].commission.as_decimal(), commission);
2271 assert!(receiver.try_recv().is_err());
2272 assert_eq!(
2273 settlement_state(&settlement, &trade.id),
2274 Some(if conflicts {
2275 SettlementState::Quarantined
2276 } else {
2277 SettlementState::StreamConfirmed
2278 })
2279 );
2280 assert_eq!(
2281 settlement.pending_resolutions(),
2282 if conflicts {
2283 vec![trade.id.clone()]
2284 } else {
2285 vec![]
2286 }
2287 );
2288 assert_eq!(trade_hard_fault(&settlement, &trade.id), None);
2289 assert_eq!(
2290 settlement.ensure_resolved(None, "mass status").is_ok(),
2291 !conflicts
2292 );
2293 }
2294
2295 #[rstest]
2296 #[case::refresh_before_stream_confirm(true, false)]
2297 #[case::refresh_after_stream_confirm(false, false)]
2298 #[case::refresh_exponent(true, true)]
2299 fn test_fee_schedule_refresh_preserves_admitted_taker_commission(
2300 #[case] refresh_before_stream: bool,
2301 #[case] refresh_exponent: bool,
2302 ) {
2303 let mut trade: PolymarketUserTrade = load("ws_user_trade.json");
2304 trade.status = PolymarketTradeStatus::Matched;
2305 let mut instrument = instrument_for_trade(&trade);
2306 set_taker_fee_rate(&mut instrument, dec!(0.0721));
2307 let token_instruments = AtomicMap::new();
2308 token_instruments.insert(trade.asset_id, instrument.clone());
2309 let venue_order_id = VenueOrderId::from(trade.taker_order_id.as_str());
2310 let fill_tracker = OrderFillTrackerMap::new();
2311 fill_tracker.register(
2312 venue_order_id,
2313 Quantity::from("100.000000"),
2314 OrderSide::Buy,
2315 instrument.id(),
2316 instrument.size_precision(),
2317 instrument.price_precision(),
2318 );
2319 let pending_submits = PendingSubmitTracker::default();
2320 let order_contexts = OrderContextRegistry::default();
2321 register_context(
2322 &order_contexts,
2323 venue_order_id,
2324 instrument.id(),
2325 "O-FEE-BASIS",
2326 );
2327 order_contexts.mark_accepted(venue_order_id);
2328 let mut emitter = test_emitter();
2329 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
2330 emitter.set_sender(sender);
2331 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
2332 settlement.begin_session();
2333 settlement.note_order_submitted(venue_order_id);
2334
2335 let ctx = WsDispatchContext {
2336 signer_type: PolymarketSignerType::Owner,
2337 token_instruments: &token_instruments,
2338 fill_tracker: &fill_tracker,
2339 settlement: &settlement,
2340 pending_submits: &pending_submits,
2341 order_contexts: &order_contexts,
2342 emitter: &emitter,
2343 account_id: AccountId::from("POLY-001"),
2344 clock: nautilus_core::time::get_atomic_clock_realtime(),
2345 user_address: trade.maker_address.as_str(),
2346 user_api_key: trade.owner.as_str(),
2347 };
2348
2349 let mut state = WsDispatchState::default();
2350
2351 dispatch_user_message(&UserWsMessage::Trade(trade.clone()), &ctx, &mut state);
2352
2353 let ExecutionEvent::Order(OrderEventAny::Filled(fill)) = receiver.try_recv().unwrap()
2354 else {
2355 panic!("expected original fill");
2356 };
2357
2358 settlement.observe_fill_applied(&fill);
2359
2360 let refresh = |rate| {
2361 let mut updated = instrument.clone();
2362 set_taker_fee_rate(&mut updated, rate);
2363 token_instruments.insert(trade.asset_id, updated);
2364 };
2365
2366 if refresh_before_stream {
2367 if refresh_exponent {
2368 let mut updated = instrument.clone();
2369 set_taker_fee_schedule(&mut updated, dec!(0.0721), Decimal::TWO);
2370 token_instruments.insert(trade.asset_id, updated);
2371 } else {
2372 refresh(dec!(0.01));
2373 }
2374 }
2375
2376 let mut confirmed = trade.clone();
2377 confirmed.status = PolymarketTradeStatus::Confirmed;
2378 dispatch_user_message(&UserWsMessage::Trade(confirmed.clone()), &ctx, &mut state);
2379
2380 if !refresh_before_stream {
2381 refresh(dec!(0.01));
2382 }
2383
2384 let admitted_stream =
2385 admit_trade_evidence(TradeEvidence::Stream(&confirmed), &ctx.admission_context())
2386 .unwrap();
2387 assert_eq!(
2388 admitted_stream.legs[0].commission.as_decimal(),
2389 dec!(0.45062)
2390 );
2391
2392 let mut rest: PolymarketTradeReport = load("http_trade_report.json");
2393 rest.id = trade.id.clone();
2394 rest.taker_order_id = trade.taker_order_id.clone();
2395 apply_rest_trade_evidence(&rest, &ctx, &mut state);
2396 apply_rest_trade_evidence(&rest, &ctx, &mut state);
2397 assert_eq!(
2398 settlement_state(&settlement, &trade.id),
2399 Some(SettlementState::RestConfirmed)
2400 );
2401 assert_eq!(trade_hard_fault(&settlement, &trade.id), None);
2402 assert!(settlement.pending_resolutions().is_empty());
2403 assert!(settlement.ensure_resolved(None, "mass status").is_ok());
2404 let admitted =
2405 admit_trade_evidence(TradeEvidence::Rest(&rest), &ctx.admission_context()).unwrap();
2406 let report = settlement
2407 .build_fill_report(&trade.id, &admitted.legs[0], UnixNanos::from(1))
2408 .unwrap();
2409 assert_eq!(report.commission.as_decimal(), dec!(0.45062));
2410 assert_eq!(report.last_qty, fill.last_qty);
2411 assert_eq!(report.last_px, fill.last_px);
2412
2413 let mut fresh = trade.clone();
2414 fresh.id = format!("{}-fresh", trade.id);
2415 fresh.taker_order_id = format!("{}-fresh", trade.taker_order_id);
2416 fresh.status = PolymarketTradeStatus::Matched;
2417 let fresh_order_id = VenueOrderId::from(fresh.taker_order_id.as_str());
2418 fill_tracker.register(
2419 fresh_order_id,
2420 Quantity::from("100.000000"),
2421 OrderSide::Buy,
2422 instrument.id(),
2423 instrument.size_precision(),
2424 instrument.price_precision(),
2425 );
2426 register_context(
2427 &order_contexts,
2428 fresh_order_id,
2429 instrument.id(),
2430 "O-FEE-FRESH",
2431 );
2432 order_contexts.mark_accepted(fresh_order_id);
2433 settlement.note_order_submitted(fresh_order_id);
2434 dispatch_user_message(&UserWsMessage::Trade(fresh), &ctx, &mut state);
2435
2436 let ExecutionEvent::Order(OrderEventAny::Filled(fresh_fill)) = receiver.try_recv().unwrap()
2437 else {
2438 panic!("expected fresh fill");
2439 };
2440
2441 assert_eq!(fill.trade_id, TradeId::from(trade.id.as_str()));
2442 assert_eq!(fill.last_qty, Quantity::from("25.000000"));
2443 assert_eq!(fill.last_px.as_decimal(), dec!(0.5));
2444 assert_eq!(fill.commission.unwrap().as_decimal(), dec!(0.45062));
2445 assert_ne!(dec!(0.450625), fill.commission.unwrap().as_decimal());
2446
2447 let fresh_commission = if refresh_exponent {
2448 dec!(0.11265)
2449 } else {
2450 dec!(0.0625)
2451 };
2452
2453 assert_eq!(
2454 fresh_fill.commission.unwrap().as_decimal(),
2455 fresh_commission
2456 );
2457 assert!(receiver.try_recv().is_err());
2458 assert_eq!(
2459 fill_tracker.get_cumulative_filled(&venue_order_id),
2460 Some(fill.last_qty)
2461 );
2462 }
2463
2464 #[rstest]
2465 fn test_fee_schedule_refresh_keeps_maker_commission_zero() {
2466 let mut trade: PolymarketUserTrade = load("ws_user_trade.json");
2467 trade.status = PolymarketTradeStatus::Matched;
2468 trade.trader_side = PolymarketLiquiditySide::Maker;
2469 let mut instrument = instrument_for_trade(&trade);
2470 set_taker_fee_rate(&mut instrument, dec!(0.0721));
2471 let token_instruments = AtomicMap::new();
2472 token_instruments.insert(trade.asset_id, instrument.clone());
2473 let maker_order_id = VenueOrderId::from(trade.maker_orders[0].order_id.as_str());
2474 let fill_tracker = OrderFillTrackerMap::new();
2475 fill_tracker.register(
2476 maker_order_id,
2477 Quantity::from("100.000000"),
2478 OrderSide::Buy,
2479 instrument.id(),
2480 instrument.size_precision(),
2481 instrument.price_precision(),
2482 );
2483 let pending_submits = PendingSubmitTracker::default();
2484 let order_contexts = OrderContextRegistry::default();
2485 register_context(
2486 &order_contexts,
2487 maker_order_id,
2488 instrument.id(),
2489 "O-MAKER-FEE",
2490 );
2491 order_contexts.mark_accepted(maker_order_id);
2492 let mut emitter = test_emitter();
2493 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
2494 emitter.set_sender(sender);
2495 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
2496 settlement.begin_session();
2497 settlement.note_order_submitted(maker_order_id);
2498 let maker_address = trade.maker_orders[0].maker_address.clone();
2499
2500 let ctx = WsDispatchContext {
2501 signer_type: PolymarketSignerType::Owner,
2502 token_instruments: &token_instruments,
2503 fill_tracker: &fill_tracker,
2504 settlement: &settlement,
2505 pending_submits: &pending_submits,
2506 order_contexts: &order_contexts,
2507 emitter: &emitter,
2508 account_id: AccountId::from("POLY-001"),
2509 clock: nautilus_core::time::get_atomic_clock_realtime(),
2510 user_address: maker_address.as_str(),
2511 user_api_key: "ffffffff-ffff-ffff-ffff-ffffffffffff",
2512 };
2513
2514 let mut state = WsDispatchState::default();
2515
2516 dispatch_user_message(&UserWsMessage::Trade(trade.clone()), &ctx, &mut state);
2517
2518 let ExecutionEvent::Order(OrderEventAny::Filled(fill)) = receiver.try_recv().unwrap()
2519 else {
2520 panic!("expected maker fill");
2521 };
2522
2523 settlement.observe_fill_applied(&fill);
2524 set_taker_fee_rate(&mut instrument, dec!(0.01));
2525 token_instruments.insert(trade.asset_id, instrument);
2526 let mut confirmed = trade.clone();
2527 confirmed.status = PolymarketTradeStatus::Confirmed;
2528 dispatch_user_message(&UserWsMessage::Trade(confirmed), &ctx, &mut state);
2529
2530 assert!(fill.commission.unwrap().is_zero());
2531 assert!(receiver.try_recv().is_err());
2532 assert_eq!(
2533 settlement_state(&settlement, &trade.id),
2534 Some(SettlementState::StreamConfirmed)
2535 );
2536 assert_eq!(trade_hard_fault(&settlement, &trade.id), None);
2537 }
2538
2539 #[rstest]
2540 #[case::unknown(false, false, false)]
2541 #[case::applied_unowned_rest(true, false, false)]
2542 #[case::applied_matching_rest(true, false, true)]
2543 #[case::settled_unowned_rest(true, true, false)]
2544 #[case::settled_matching_rest(true, true, true)]
2545 fn test_dispatch_trade_missing_all_owned_legs(
2546 #[case] known: bool,
2547 #[case] rest_settled: bool,
2548 #[case] rest_owned: bool,
2549 ) {
2550 let mut trade: PolymarketUserTrade = load("ws_user_trade.json");
2551 trade.trader_side = PolymarketLiquiditySide::Maker;
2552 trade.side = PolymarketOrderSide::Sell;
2553 trade.status = PolymarketTradeStatus::Matched;
2554 let user_address = trade.maker_orders[0].maker_address.clone();
2555 let api_key = trade.maker_orders[0].owner.clone();
2556 let venue_order_id = VenueOrderId::from(trade.maker_orders[0].order_id.as_str());
2557 let instrument = instrument_for_trade(&trade);
2558 let token_instruments = AtomicMap::new();
2559 token_instruments.insert(trade.asset_id, instrument.clone());
2560 let fill_tracker = OrderFillTrackerMap::new();
2561 fill_tracker.register(
2562 venue_order_id,
2563 Quantity::from("100.000000"),
2564 OrderSide::Buy,
2565 instrument.id(),
2566 instrument.size_precision(),
2567 instrument.price_precision(),
2568 );
2569 let pending_submits = PendingSubmitTracker::default();
2570 let order_contexts = OrderContextRegistry::default();
2571 register_context(
2572 &order_contexts,
2573 venue_order_id,
2574 instrument.id(),
2575 "O-MISSING-MAKER",
2576 );
2577 order_contexts.mark_accepted(venue_order_id);
2578 let mut emitter = test_emitter();
2579 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
2580 emitter.set_sender(sender);
2581 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
2582 settlement.begin_session();
2583 settlement.note_order_submitted(venue_order_id);
2584
2585 let ctx = WsDispatchContext {
2586 signer_type: PolymarketSignerType::Owner,
2587 token_instruments: &token_instruments,
2588 fill_tracker: &fill_tracker,
2589 settlement: &settlement,
2590 pending_submits: &pending_submits,
2591 order_contexts: &order_contexts,
2592 emitter: &emitter,
2593 account_id: AccountId::from("POLY-001"),
2594 clock: nautilus_core::time::get_atomic_clock_realtime(),
2595 user_address: user_address.as_str(),
2596 user_api_key: api_key.as_str(),
2597 };
2598
2599 let mut state = WsDispatchState::default();
2600
2601 if known {
2602 dispatch_user_message(&UserWsMessage::Trade(trade.clone()), &ctx, &mut state);
2603
2604 let ExecutionEvent::Order(OrderEventAny::Filled(fill)) = receiver.try_recv().unwrap()
2605 else {
2606 panic!("expected original maker fill");
2607 };
2608
2609 assert_eq!(fill.venue_order_id, venue_order_id);
2610 assert_eq!(fill.liquidity_side, LiquiditySide::Maker);
2611 settlement.observe_fill_applied(&fill);
2612 }
2613
2614 if rest_settled {
2615 let mut admitted =
2616 admit_trade_evidence(TradeEvidence::Stream(&trade), &ctx.admission_context())
2617 .unwrap();
2618 admitted.status = PolymarketTradeStatus::Confirmed;
2619 settlement.admit_rest_result(&admitted);
2620 }
2621
2622 let mut rest: PolymarketTradeReport = load("http_trade_report.json");
2623 rest.id = trade.id.clone();
2624 rest.taker_order_id = trade.taker_order_id.clone();
2625 rest.trader_side = trade.trader_side;
2626 rest.side = trade.side;
2627 rest.maker_orders = trade.maker_orders.clone();
2628 for maker in &mut rest.maker_orders {
2629 maker.side = Some(PolymarketOrderSide::Buy);
2630 }
2631
2632 if !rest_owned {
2633 rest.maker_orders.clear();
2634 }
2635
2636 trade.maker_orders.clear();
2637 trade.status = PolymarketTradeStatus::Confirmed;
2638 assert!(settlement.resolution_requested().now_or_never().is_none());
2639
2640 dispatch_user_message(&UserWsMessage::Trade(trade.clone()), &ctx, &mut state);
2641
2642 assert_eq!(
2643 settlement.resolution_requested().now_or_never().is_some(),
2644 known
2645 );
2646 assert!(receiver.try_recv().is_err());
2647 assert_eq!(settlement.record_count(), usize::from(known));
2648
2649 let expected = known.then_some(if rest_settled {
2650 SettlementState::RestConfirmed
2651 } else {
2652 SettlementState::Quarantined
2653 });
2654
2655 assert_eq!(settlement_state(&settlement, &trade.id), expected);
2656 assert_eq!(
2657 settlement.pending_resolutions(),
2658 if known {
2659 vec![trade.id.clone()]
2660 } else {
2661 vec![]
2662 }
2663 );
2664 assert_eq!(trade_hard_fault(&settlement, &trade.id), None);
2665 assert_eq!(
2666 settlement.ensure_resolved(None, "mass status").is_ok(),
2667 !known
2668 );
2669
2670 if known {
2671 apply_rest_trade_evidence(&rest, &ctx, &mut state);
2672
2673 assert!(receiver.try_recv().is_err());
2674 assert_eq!(
2675 settlement_state(&settlement, &trade.id),
2676 Some(if rest_settled || rest_owned {
2677 SettlementState::RestConfirmed
2678 } else {
2679 SettlementState::Quarantined
2680 })
2681 );
2682 assert_eq!(
2683 settlement.pending_resolutions(),
2684 if rest_owned {
2685 vec![]
2686 } else {
2687 vec![trade.id.clone()]
2688 }
2689 );
2690 assert_eq!(trade_hard_fault(&settlement, &trade.id), None);
2691 assert_eq!(
2692 settlement.ensure_resolved(None, "mass status").is_ok(),
2693 rest_owned
2694 );
2695 assert_eq!(
2696 fill_tracker.get_cumulative_filled(&venue_order_id),
2697 Some(Quantity::from("25.000000"))
2698 );
2699 }
2700 }
2701
2702 #[rstest]
2703 fn test_emit_order_rejected_uses_bounded_clean_reason() {
2704 let instrument = test_instrument();
2705 let token_instruments = AtomicMap::new();
2706 let fill_tracker = OrderFillTrackerMap::new();
2707 let pending_submits = PendingSubmitTracker::default();
2708 let order_contexts = OrderContextRegistry::default();
2709 let mut emitter = test_emitter();
2710 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
2711
2712 emitter.set_sender(sender);
2713
2714 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
2715 settlement.mark_live();
2716
2717 let ctx = WsDispatchContext {
2718 signer_type: PolymarketSignerType::Owner,
2719 token_instruments: &token_instruments,
2720 fill_tracker: &fill_tracker,
2721 settlement: &settlement,
2722 pending_submits: &pending_submits,
2723 order_contexts: &order_contexts,
2724 emitter: &emitter,
2725 account_id: AccountId::from("POLY-001"),
2726 clock: nautilus_core::time::get_atomic_clock_realtime(),
2727 user_address: "0xtest",
2728 user_api_key: "test-key",
2729 };
2730 let context = OrderContext {
2731 identity: OrderIdentity {
2732 client_order_id: ClientOrderId::from("O-WS-REJECT"),
2733 strategy_id: StrategyId::from("S-001"),
2734 instrument_id: instrument.id(),
2735 order_side: OrderSide::Buy,
2736 order_type: OrderType::Limit,
2737 },
2738 quantity: Quantity::from("10"),
2739 price: Some(Price::from("0.50")),
2740 trigger_price: None,
2741 trigger_type: None,
2742 time_in_force: TimeInForce::Gtc,
2743 is_post_only: false,
2744 is_reduce_only: false,
2745 is_quote_quantity: false,
2746 };
2747
2748 emit_order_rejected(
2749 &context,
2750 " invalid post-only order:\norder crosses book ",
2751 UnixNanos::from(1_000_000_000),
2752 &ctx,
2753 );
2754
2755 match receiver.try_recv().expect("expected rejected event") {
2756 ExecutionEvent::Order(OrderEventAny::Rejected(event)) => {
2757 assert_eq!(event.reason, "invalid post-only order: order crosses book");
2758 assert!(event.due_post_only);
2759 }
2760 other => panic!("expected rejected event, was {other:?}"),
2761 }
2762 }
2763
2764 #[rstest]
2765 #[case::empty_price("", "1.01", "0")]
2766 #[case::zero_price("0", "1.01", "0")]
2767 #[case::malformed("bad", "100", "0")]
2768 #[case::too_precise("0.50000000000000000000000000001", "100", "0")]
2769 #[case::quantity_overflow("0.5", "79228162514264337593543950335", "0")]
2770 #[case::filled_overflow("0.5", "100", "79228162514264337593543950335")]
2771 fn test_ws_order_report_rejects_invalid_values(
2772 #[case] price: &str,
2773 #[case] quantity: &str,
2774 #[case] filled: &str,
2775 ) {
2776 let mut order: PolymarketUserOrder = load("ws_user_order_placement.json");
2777 order.price = price.into();
2778 order.original_size = quantity.into();
2779 order.size_matched = filled.into();
2780 assert!(
2781 build_ws_order_status_report(
2782 &order,
2783 order.status.as_ref().unwrap(),
2784 order.order_type.unwrap(),
2785 &test_instrument(),
2786 AccountId::from("POLY-001"),
2787 UnixNanos::default(),
2788 UnixNanos::default()
2789 )
2790 .is_err()
2791 );
2792 }
2793
2794 #[rstest]
2795 fn test_ws_fok_order_report_rejects_share_overflow() {
2796 let mut order: PolymarketUserOrder = load("ws_user_order_fok_buy_pusd_size.json");
2797 order.original_size = Decimal::MAX.to_string();
2798 order.price = "0.5".into();
2799 let result = build_ws_order_status_report(
2800 &order,
2801 order.status.as_ref().unwrap(),
2802 PolymarketOrderType::FOK,
2803 &test_instrument(),
2804 AccountId::from("POLY-001"),
2805 UnixNanos::default(),
2806 UnixNanos::default(),
2807 );
2808
2809 assert_eq!(
2810 result.unwrap_err().to_string(),
2811 "order share quantity overflow"
2812 );
2813 }
2814
2815 #[rstest]
2816 fn test_build_ws_order_status_report() {
2817 let order: PolymarketUserOrder = load("ws_user_order_placement.json");
2818 let instrument = test_instrument();
2819 let ts_event = UnixNanos::from(1_000_000_000u64);
2820 let ts_init = UnixNanos::from(2_000_000_000u64);
2821
2822 let report = build_ws_order_status_report(
2823 &order,
2824 order.status.as_ref().unwrap(),
2825 order.order_type.unwrap(),
2826 &instrument,
2827 AccountId::from("POLY-001"),
2828 ts_event,
2829 ts_init,
2830 )
2831 .unwrap();
2832
2833 assert_eq!(report.order_side, Some(OrderSide::Buy));
2834 assert_eq!(report.order_type, OrderType::Limit);
2835 assert_eq!(report.quantity.as_decimal(), dec!(100));
2837 assert_eq!(
2838 report.price.map(|price| price.as_decimal()),
2839 Some(dec!(0.5))
2840 );
2841 assert_eq!(report.ts_accepted, ts_event);
2842 assert_eq!(report.ts_init, ts_init);
2843 }
2844
2845 #[rstest]
2846 fn test_build_ws_order_status_report_venue_cancel_maps_to_canceled() {
2847 let order: PolymarketUserOrder = load("ws_user_order_venue_cancel.json");
2848 let instrument = test_instrument();
2849 let ts_event = UnixNanos::from(1_000_000_000u64);
2850 let ts_init = UnixNanos::from(2_000_000_000u64);
2851
2852 let report = build_ws_order_status_report(
2853 &order,
2854 order.status.as_ref().unwrap(),
2855 order.order_type.unwrap(),
2856 &instrument,
2857 AccountId::from("POLY-001"),
2858 ts_event,
2859 ts_init,
2860 )
2861 .unwrap();
2862
2863 assert_eq!(report.order_status, OrderStatus::Canceled);
2864 }
2865
2866 #[rstest]
2867 #[case::before_window(939_000_000_000, OrderStatus::Canceled)]
2868 #[case::window_start(940_000_000_000, OrderStatus::Expired)]
2869 #[case::security_threshold(999_000_000_000, OrderStatus::Expired)]
2870 #[case::at_expiry(1_000_000_000_000, OrderStatus::Expired)]
2871 #[case::after_expiry(1_000_000_001_000, OrderStatus::Expired)]
2872 fn test_build_ws_order_status_report_gtd_expiry_maps_to_expired(
2873 #[case] ts_event: u64,
2874 #[case] expected: OrderStatus,
2875 ) {
2876 let mut order: PolymarketUserOrder = load("ws_user_order_venue_cancel.json");
2877 order.order_type = Some(PolymarketOrderType::GTD);
2878 order.expiration = Some("1000".to_string());
2879 let instrument = test_instrument();
2880
2881 let report = build_ws_order_status_report(
2882 &order,
2883 order.status.as_ref().unwrap(),
2884 order.order_type.unwrap(),
2885 &instrument,
2886 AccountId::from("POLY-001"),
2887 UnixNanos::from(ts_event),
2888 UnixNanos::from(2_000_000_000u64),
2889 )
2890 .unwrap();
2891
2892 assert_eq!(report.order_status, expected);
2893 assert_eq!(
2894 report.expire_time,
2895 Some(UnixNanos::from(1_000_000_000_000u64))
2896 );
2897 }
2898
2899 #[rstest]
2900 #[case::gtc_in_window(PolymarketOrderType::GTC, Some("1000"))]
2901 #[case::gtd_zero_expiration(PolymarketOrderType::GTD, Some("0"))]
2902 #[case::gtd_missing_expiration(PolymarketOrderType::GTD, None)]
2903 fn test_build_ws_order_status_report_non_expiry_cancel_stays_canceled(
2904 #[case] order_type: PolymarketOrderType,
2905 #[case] expiration: Option<&str>,
2906 ) {
2907 let mut order: PolymarketUserOrder = load("ws_user_order_venue_cancel.json");
2908 order.order_type = Some(order_type);
2909 order.expiration = expiration.map(str::to_string);
2910 let instrument = test_instrument();
2911
2912 let report = build_ws_order_status_report(
2913 &order,
2914 order.status.as_ref().unwrap(),
2915 order.order_type.unwrap(),
2916 &instrument,
2917 AccountId::from("POLY-001"),
2918 UnixNanos::from(999_000_000_000u64),
2919 UnixNanos::from(2_000_000_000u64),
2920 )
2921 .unwrap();
2922
2923 assert_eq!(report.order_status, OrderStatus::Canceled);
2924 }
2925
2926 #[rstest]
2927 fn test_reemit_terminal_expiry_emits_expired() {
2928 let mut order: PolymarketUserOrder = load("ws_user_order_venue_cancel.json");
2929 order.order_type = Some(PolymarketOrderType::GTD);
2930 order.expiration = Some("1000".to_string());
2931 let instrument = test_instrument();
2932 let report = build_ws_order_status_report(
2933 &order,
2934 order.status.as_ref().unwrap(),
2935 PolymarketOrderType::GTD,
2936 &instrument,
2937 AccountId::from("POLY-001"),
2938 UnixNanos::from(1_000_000_000_000u64),
2939 UnixNanos::from(2_000_000_000u64),
2940 )
2941 .unwrap();
2942 let venue_order_id = report.venue_order_id;
2943 let mut state = WsDispatchState::default();
2944 state.record_terminal_cancel_report(report);
2945
2946 let token_instruments = AtomicMap::new();
2947 let fill_tracker = OrderFillTrackerMap::new();
2948 let pending_submits = PendingSubmitTracker::default();
2949 let order_contexts = OrderContextRegistry::default();
2950
2951 let context = OrderContext {
2952 identity: OrderIdentity {
2953 client_order_id: ClientOrderId::from("O-GTD-EXPIRE"),
2954 strategy_id: StrategyId::from("S-001"),
2955 instrument_id: instrument.id(),
2956 order_side: OrderSide::Buy,
2957 order_type: OrderType::Limit,
2958 },
2959 quantity: Quantity::from("10"),
2960 price: Some(Price::from("0.50")),
2961 trigger_price: None,
2962 trigger_type: None,
2963 time_in_force: TimeInForce::Gtd,
2964 is_post_only: false,
2965 is_reduce_only: false,
2966 is_quote_quantity: false,
2967 };
2968
2969 order_contexts.register_context(venue_order_id, context);
2970 let mut emitter = test_emitter();
2971 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
2972 emitter.set_sender(sender);
2973 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
2974
2975 let ctx = WsDispatchContext {
2976 signer_type: PolymarketSignerType::Owner,
2977 token_instruments: &token_instruments,
2978 fill_tracker: &fill_tracker,
2979 settlement: &settlement,
2980 pending_submits: &pending_submits,
2981 order_contexts: &order_contexts,
2982 emitter: &emitter,
2983 account_id: AccountId::from("POLY-001"),
2984 clock: nautilus_core::time::get_atomic_clock_realtime(),
2985 user_address: "0xtest",
2986 user_api_key: "test-key",
2987 };
2988
2989 reemit_terminal_cancel(venue_order_id, &state, &ctx);
2990
2991 match receiver.try_recv().expect("expected expired event") {
2992 ExecutionEvent::Order(OrderEventAny::Expired(event)) => {
2993 assert_eq!(event.client_order_id, ClientOrderId::from("O-GTD-EXPIRE"));
2994 }
2995 other => panic!("expected OrderExpired, was {other:?}"),
2996 }
2997 }
2998
2999 #[rstest]
3002 #[case(
3003 PolymarketOrderSide::Buy,
3004 PolymarketOrderType::FOK,
3005 dec!(1.01),
3006 dec!(0.01),
3007 dec!(101)
3008 )]
3009 #[case(
3010 PolymarketOrderSide::Buy,
3011 PolymarketOrderType::FOK,
3012 dec!(12),
3013 dec!(0.6),
3014 dec!(20)
3015 )]
3016 #[case(
3017 PolymarketOrderSide::Buy,
3018 PolymarketOrderType::FAK,
3019 dec!(1),
3020 dec!(0.01),
3021 dec!(100)
3022 )]
3023 #[case(
3024 PolymarketOrderSide::Buy,
3025 PolymarketOrderType::GTC,
3026 dec!(20),
3027 dec!(0.18),
3028 dec!(20)
3029 )]
3030 #[case(
3031 PolymarketOrderSide::Buy,
3032 PolymarketOrderType::GTD,
3033 dec!(20),
3034 dec!(0.18),
3035 dec!(20)
3036 )]
3037 #[case(
3038 PolymarketOrderSide::Sell,
3039 PolymarketOrderType::FOK,
3040 dec!(20),
3041 dec!(0.6),
3042 dec!(20)
3043 )]
3044 fn test_original_size_to_shares(
3045 #[case] side: PolymarketOrderSide,
3046 #[case] order_type: PolymarketOrderType,
3047 #[case] original_size: Decimal,
3048 #[case] price: Decimal,
3049 #[case] expected: Decimal,
3050 ) {
3051 let shares = original_size_to_shares(original_size, price, side, order_type).unwrap();
3052
3053 assert_eq!(shares, expected);
3054 }
3055
3056 #[rstest]
3058 #[case("1", "0.03", "33.333333", "0.03")]
3059 fn test_build_ws_order_status_report_fok_buy_quantity(
3060 #[case] original_size: &str,
3061 #[case] price: &str,
3062 #[case] expected_quantity: &str,
3063 #[case] expected_price: &str,
3064 ) {
3065 let mut order: PolymarketUserOrder = load("ws_user_order_fok_buy_pusd_size.json");
3066 order.original_size = original_size.to_string();
3067 order.price = price.to_string();
3068 let instrument = test_instrument();
3069
3070 let report = build_ws_order_status_report(
3071 &order,
3072 order.status.as_ref().unwrap(),
3073 order.order_type.unwrap(),
3074 &instrument,
3075 AccountId::from("POLY-001"),
3076 UnixNanos::from(1_000_000_000u64),
3077 UnixNanos::from(2_000_000_000u64),
3078 )
3079 .unwrap();
3080
3081 assert_eq!(
3082 report.quantity.as_decimal(),
3083 Decimal::from_str_exact(expected_quantity).unwrap()
3084 );
3085 assert_eq!(
3086 report.price.map(|price| price.as_decimal()),
3087 Some(Decimal::from_str_exact(expected_price).unwrap())
3088 );
3089 }
3090
3091 #[rstest]
3092 fn test_dispatch_fok_buy_registers_share_quantity_for_in_flight_submit() {
3093 let order: PolymarketUserOrder = load("ws_user_order_fok_buy_pusd_size.json");
3094 let instrument = test_instrument();
3095
3096 let token_instruments = AtomicMap::new();
3097 token_instruments.insert(order.asset_id, instrument.clone());
3098
3099 let fill_tracker = OrderFillTrackerMap::new();
3101 let pending_submits = PendingSubmitTracker::default();
3102 let order_contexts = OrderContextRegistry::default();
3103 let emitter = test_emitter();
3104
3105 let venue_order_id = VenueOrderId::from(order.id.as_str());
3106 let client_order_id = ClientOrderId::from("O-FOK-IN-FLIGHT");
3107 pending_submits.insert(venue_order_id, client_order_id);
3108 register_context(
3109 &order_contexts,
3110 venue_order_id,
3111 instrument.id(),
3112 client_order_id.as_str(),
3113 );
3114
3115 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
3116 settlement.mark_live();
3117 settlement.note_order_submitted(venue_order_id);
3118
3119 let ctx = WsDispatchContext {
3120 signer_type: PolymarketSignerType::Owner,
3121 token_instruments: &token_instruments,
3122 fill_tracker: &fill_tracker,
3123 settlement: &settlement,
3124 pending_submits: &pending_submits,
3125 order_contexts: &order_contexts,
3126 emitter: &emitter,
3127 account_id: AccountId::from("POLY-001"),
3128 clock: nautilus_core::time::get_atomic_clock_realtime(),
3129 user_address: "0xtest",
3130 user_api_key: "test-key",
3131 };
3132 let mut state = WsDispatchState::default();
3133
3134 dispatch_user_message(&UserWsMessage::Order(order), &ctx, &mut state);
3135
3136 assert_eq!(
3138 fill_tracker
3139 .submitted_qty(&venue_order_id)
3140 .map(|qty| qty.as_decimal()),
3141 Some(dec!(101)),
3142 );
3143 }
3144
3145 #[rstest]
3146 fn test_dispatch_fok_buy_report_quantity_is_shares_without_identity() {
3147 let order: PolymarketUserOrder = load("ws_user_order_fok_buy_pusd_size.json");
3148 let instrument = test_instrument();
3149
3150 let token_instruments = AtomicMap::new();
3151 token_instruments.insert(order.asset_id, instrument.clone());
3152
3153 let fill_tracker = OrderFillTrackerMap::new();
3154 let venue_order_id = VenueOrderId::from(order.id.as_str());
3155 fill_tracker.register(
3156 venue_order_id,
3157 Quantity::from("101"),
3158 OrderSide::Buy,
3159 instrument.id(),
3160 instrument.size_precision(),
3161 instrument.price_precision(),
3162 );
3163
3164 let pending_submits = PendingSubmitTracker::default();
3165 let order_contexts = OrderContextRegistry::default();
3167 let mut emitter = test_emitter();
3168 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
3169 emitter.set_sender(sender);
3170
3171 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
3172 settlement.mark_live();
3173 settlement.note_order_submitted(venue_order_id);
3174
3175 let ctx = WsDispatchContext {
3176 signer_type: PolymarketSignerType::Owner,
3177 token_instruments: &token_instruments,
3178 fill_tracker: &fill_tracker,
3179 settlement: &settlement,
3180 pending_submits: &pending_submits,
3181 order_contexts: &order_contexts,
3182 emitter: &emitter,
3183 account_id: AccountId::from("POLY-001"),
3184 clock: nautilus_core::time::get_atomic_clock_realtime(),
3185 user_address: "0xtest",
3186 user_api_key: "test-key",
3187 };
3188 let mut state = WsDispatchState::default();
3189
3190 dispatch_user_message(&UserWsMessage::Order(order), &ctx, &mut state);
3191
3192 let event = receiver.try_recv().expect("expected order report");
3193 let ExecutionEvent::Report(ExecutionReport::Order(report)) = event else {
3194 panic!("expected an order report, was {event:?}");
3195 };
3196
3197 assert_eq!(report.venue_order_id, venue_order_id);
3198 assert_eq!(report.order_side, Some(OrderSide::Buy));
3199 assert_eq!(report.time_in_force, TimeInForce::Fok);
3200 assert_eq!(report.order_status, OrderStatus::Canceled);
3201 assert_eq!(report.quantity.as_decimal(), dec!(101));
3202 assert_eq!(report.filled_qty.as_decimal(), dec!(0));
3203 assert_eq!(
3204 report.price.map(|price| price.as_decimal()),
3205 Some(dec!(0.01))
3206 );
3207 }
3208
3209 #[rstest]
3210 fn test_admit_stream_taker_trade_builds_the_leg() {
3211 let trade: PolymarketUserTrade = load("ws_user_trade.json");
3212 let instrument = instrument_for_trade(&trade);
3213 let instrument_id = instrument.id();
3214 let token_instruments = AtomicMap::new();
3215 token_instruments.insert(trade.asset_id, instrument);
3216 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
3217
3218 let ctx = WsDispatchContext {
3219 signer_type: PolymarketSignerType::Owner,
3220 token_instruments: &token_instruments,
3221 fill_tracker: &OrderFillTrackerMap::new(),
3222 settlement: &settlement,
3223 pending_submits: &PendingSubmitTracker::default(),
3224 order_contexts: &OrderContextRegistry::default(),
3225 emitter: &test_emitter(),
3226 account_id: AccountId::from("POLY-001"),
3227 clock: nautilus_core::time::get_atomic_clock_realtime(),
3228 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
3229 user_api_key: "00000000-0000-0000-0000-000000000001",
3230 };
3231
3232 let admitted =
3233 admit_trade_evidence(TradeEvidence::Stream(&trade), &ctx.admission_context())
3234 .expect("owned taker trade admits");
3235
3236 assert_eq!(admitted.venue_trade_id, trade.id);
3237 assert_eq!(admitted.status, trade.status);
3238 assert!(admitted.has_economics());
3239 assert_eq!(admitted.legs.len(), 1);
3240 let leg = &admitted.legs[0];
3241 assert_eq!(
3242 leg.venue_order_id,
3243 VenueOrderId::from(trade.taker_order_id.as_str())
3244 );
3245 assert_eq!(leg.trade_id.as_str(), trade.id);
3246 assert_eq!(leg.instrument_id, instrument_id);
3247 assert_eq!(leg.order_side, OrderSide::Buy);
3248 assert_eq!(leg.liquidity_side, LiquiditySide::Taker);
3249 assert_eq!(leg.last_qty.as_decimal(), dec!(25));
3250 assert_eq!(leg.last_px.as_decimal(), dec!(0.5));
3251 assert_eq!(leg.ts_event, UnixNanos::from(1_704_067_200_000_000_000u64));
3253 }
3254
3255 fn admission_context_for(
3256 token_instruments: &AtomicMap<Ustr, InstrumentAny>,
3257 ) -> AdmissionContext<'_> {
3258 AdmissionContext {
3259 signer_type: PolymarketSignerType::Owner,
3260 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
3261 api_key: "00000000-0000-0000-0000-000000000001",
3262 pusd: get_pusd_currency(),
3263 instruments: token_instruments,
3264 fee_bases: None,
3265 }
3266 }
3267
3268 #[rstest]
3269 fn test_admit_stream_fill_without_venue_match_time_is_untimestamped() {
3270 let mut trade: PolymarketUserTrade = load("ws_user_trade.json");
3271 trade.match_time = String::new();
3272 trade.timestamp = "not-a-timestamp".to_string();
3273 let token_instruments = AtomicMap::new();
3274 token_instruments.insert(trade.asset_id, instrument_for_trade(&trade));
3275
3276 let result = admit_trade_evidence(
3277 TradeEvidence::Stream(&trade),
3278 &admission_context_for(&token_instruments),
3279 );
3280
3281 let Err(AdmissionError::Untimestamped(e)) = result else {
3282 panic!("expected untimestamped evidence, was {result:?}");
3283 };
3284
3285 assert_eq!(
3286 e.to_string(),
3287 format!(
3288 "trade {} has no valid venue match timestamp (match_time=)",
3289 trade.id
3290 )
3291 );
3292 }
3293
3294 #[rstest]
3295 fn test_admit_stream_failed_trade_without_match_time_carries_no_economics() {
3296 let mut trade: PolymarketUserTrade = load("ws_user_trade.json");
3297 trade.status = PolymarketTradeStatus::Failed;
3298 trade.match_time = String::new();
3299 trade.timestamp = "not-a-timestamp".to_string();
3300 let instrument = instrument_for_trade(&trade);
3301 let instrument_id = instrument.id();
3302 let token_instruments = AtomicMap::new();
3303 token_instruments.insert(trade.asset_id, instrument);
3304
3305 let admitted = admit_trade_evidence(
3306 TradeEvidence::Stream(&trade),
3307 &admission_context_for(&token_instruments),
3308 )
3309 .expect("failed evidence admits owned-leg identity");
3310
3311 assert!(!admitted.has_economics());
3312 assert_eq!(admitted.legs.len(), 1);
3313 let leg = &admitted.legs[0];
3314 assert_eq!(
3315 leg.venue_order_id,
3316 VenueOrderId::from(trade.taker_order_id.as_str())
3317 );
3318 assert_eq!(leg.instrument_id, instrument_id);
3319 assert_eq!(leg.last_qty.as_decimal(), dec!(0));
3320 assert_eq!(leg.last_px.as_decimal(), dec!(0));
3321 assert!(leg.commission.is_zero());
3322 assert_eq!(leg.ts_event, UnixNanos::default());
3323 }
3324
3325 #[rstest]
3326 fn test_admit_price_keeps_instrument_precision_for_stream_and_wire_precision_for_rest() {
3327 let trade: PolymarketUserTrade = load("ws_user_trade.json");
3328 let instrument = instrument_for_trade(&trade);
3329 let price_precision = instrument.price_precision();
3330 let token_instruments = AtomicMap::new();
3331 token_instruments.insert(trade.asset_id, instrument);
3332 let mut rest: PolymarketTradeReport =
3333 serde_json::from_str(include_str!("../../test_data/http_trade_report.json"))
3334 .expect("REST trade fixture");
3335 rest.id = trade.id.clone();
3336 rest.taker_order_id = trade.taker_order_id.clone();
3337 let ctx = admission_context_for(&token_instruments);
3338
3339 let stream = admit_trade_evidence(TradeEvidence::Stream(&trade), &ctx)
3340 .expect("stream evidence admits");
3341 let rest =
3342 admit_trade_evidence(TradeEvidence::Rest(&rest), &ctx).expect("REST evidence admits");
3343
3344 assert_eq!(price_precision, 4);
3345 assert_eq!(stream.legs[0].last_px.as_decimal(), dec!(0.5));
3346 assert_eq!(stream.legs[0].last_px.precision, price_precision);
3347 assert_eq!(rest.legs[0].last_px, stream.legs[0].last_px);
3348 assert_eq!(rest.legs[0].last_px.precision, 1);
3349 }
3350
3351 #[rstest]
3352 #[case::settlement_rounding("0.4200001023529231", "1.758621", Some(dec!(0.42)))]
3353 #[case::beyond_settlement_rounding("0.42001", "5", None)]
3354 fn test_admit_maker_price_snaps_settlement_rounding_on_both_paths(
3355 #[case] wire_price: &str,
3356 #[case] matched_amount: &str,
3357 #[case] snapped: Option<Decimal>,
3358 ) {
3359 let wire_price = Decimal::from_str_exact(wire_price).unwrap();
3360 let matched_amount = Decimal::from_str_exact(matched_amount).unwrap();
3361 let mut trade: PolymarketUserTrade = load("ws_user_trade.json");
3362 trade.trader_side = PolymarketLiquiditySide::Maker;
3363 trade.maker_orders[0].price = wire_price;
3364 trade.maker_orders[0].matched_amount = matched_amount;
3365 let mut rest: PolymarketTradeReport =
3366 serde_json::from_str(include_str!("../../test_data/http_trade_report.json"))
3367 .expect("REST trade fixture");
3368 rest.trader_side = PolymarketLiquiditySide::Maker;
3369 rest.maker_orders[0].price = wire_price;
3370 rest.maker_orders[0].matched_amount = matched_amount;
3371 let token_instruments = AtomicMap::new();
3372 token_instruments.insert(trade.asset_id, instrument_for_trade(&trade));
3373 let maker_address = trade.maker_orders[0].maker_address.clone();
3374 let ctx = AdmissionContext {
3375 signer_type: PolymarketSignerType::Owner,
3376 user_address: &maker_address,
3377 api_key: "ffffffff-ffff-ffff-ffff-ffffffffffff",
3378 pusd: get_pusd_currency(),
3379 instruments: &token_instruments,
3380 fee_bases: None,
3381 };
3382
3383 let stream = admit_trade_evidence(TradeEvidence::Stream(&trade), &ctx);
3384 let rest =
3385 admit_trade_evidence(TradeEvidence::Rest(&rest), &ctx).expect("REST evidence admits");
3386
3387 if let Some(snapped) = snapped {
3388 let stream = stream.expect("stream evidence admits");
3389 assert_eq!(stream.legs[0].last_px.as_decimal(), snapped);
3390 assert_eq!(stream.legs[0].last_px.precision, 4);
3391 assert_eq!(rest.legs[0].last_px, stream.legs[0].last_px);
3392 } else {
3393 let Err(AdmissionError::Invalid(e)) = stream else {
3394 panic!("expected invalid stream evidence, was {stream:?}");
3395 };
3396 assert!(
3397 format!("{e:#}").contains("not exactly representable"),
3398 "unexpected error: {e:#}"
3399 );
3400 assert_eq!(rest.legs[0].last_px.as_decimal(), wire_price);
3401 }
3402 }
3403
3404 #[rstest]
3405 #[case::taker_order_id(false, "invalid venue order ID")]
3406 #[case::maker_trade_id(true, "invalid trade ID source")]
3407 fn test_admit_stream_rejects_invalid_identifiers_without_panicking(
3408 #[case] maker: bool,
3409 #[case] expected_error: &str,
3410 ) {
3411 let mut trade: PolymarketUserTrade = load("ws_user_trade.json");
3412 let token_instruments = AtomicMap::new();
3413 token_instruments.insert(trade.asset_id, instrument_for_trade(&trade));
3414 let maker_address = trade.maker_orders[0].maker_address.clone();
3415
3416 let user_address = if maker {
3417 trade.trader_side = PolymarketLiquiditySide::Maker;
3418 trade.id = "trade-\u{e9}".to_string();
3419 maker_address.as_str()
3420 } else {
3421 trade.taker_order_id = "order-\u{e9}".to_string();
3422 "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266"
3423 };
3424
3425 let ctx = AdmissionContext {
3426 signer_type: PolymarketSignerType::Owner,
3427 user_address,
3428 api_key: "ffffffff-ffff-ffff-ffff-ffffffffffff",
3429 pusd: get_pusd_currency(),
3430 instruments: &token_instruments,
3431 fee_bases: None,
3432 };
3433
3434 let result = admit_trade_evidence(TradeEvidence::Stream(&trade), &ctx);
3435
3436 let Err(AdmissionError::Invalid(e)) = result else {
3437 panic!("expected invalid evidence, was {result:?}");
3438 };
3439
3440 assert!(
3441 format!("{e:#}").contains(expected_error),
3442 "unexpected error: {e:#}"
3443 );
3444 }
3445
3446 #[rstest]
3447 fn test_trade_fill_info_flattens_raw_trade() {
3448 let trade: PolymarketUserTrade = load("ws_user_trade.json");
3449
3450 let info = trade_fill_info(&trade).expect("info should be present");
3451
3452 assert_eq!(info.len(), 21);
3454 assert_eq!(info[&Ustr::from("id")], Ustr::from("trade-0xabcdef1234"));
3455 assert_eq!(info[&Ustr::from("fee_rate_bps")], Ustr::from("0"));
3456 assert_eq!(
3457 info[&Ustr::from("transaction_hash")],
3458 Ustr::from("0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab")
3459 );
3460 assert_eq!(info[&Ustr::from("bucket_index")], Ustr::from("1"));
3462 assert_eq!(info[&Ustr::from("size")], Ustr::from("25.0"));
3463 assert_eq!(
3464 info[&Ustr::from("taker_order_id")],
3465 Ustr::from("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12")
3466 );
3467 assert_eq!(info[&Ustr::from("type")], Ustr::from("TRADE"));
3469 let maker_orders = info[&Ustr::from("maker_orders")].as_str();
3471 assert!(maker_orders.starts_with('['));
3472 assert!(maker_orders.contains("order_id"));
3473
3474 let empty_hash_trade: PolymarketUserTrade = load("ws_user_trade_msg.json");
3475 let empty_hash_info =
3476 trade_fill_info(&empty_hash_trade).expect("empty hash info should be present");
3477 assert!(!empty_hash_info.contains_key(&Ustr::from("transaction_hash")));
3478 }
3479
3480 #[rstest]
3481 fn test_dispatch_order_message_buffers_when_not_accepted() {
3482 let order: PolymarketUserOrder = load("ws_user_order_placement.json");
3483 let instrument = test_instrument();
3484
3485 let token_instruments = AtomicMap::new();
3486 token_instruments.insert(order.asset_id, instrument);
3487
3488 let fill_tracker = OrderFillTrackerMap::new();
3489 let pending_submits = PendingSubmitTracker::default();
3490 let order_contexts = OrderContextRegistry::default();
3491 let emitter = test_emitter();
3492
3493 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
3494 settlement.mark_live();
3495
3496 let ctx = WsDispatchContext {
3497 signer_type: PolymarketSignerType::Owner,
3498 token_instruments: &token_instruments,
3499 fill_tracker: &fill_tracker,
3500 settlement: &settlement,
3501 pending_submits: &pending_submits,
3502 order_contexts: &order_contexts,
3503 emitter: &emitter,
3504 account_id: AccountId::from("POLY-001"),
3505 clock: nautilus_core::time::get_atomic_clock_realtime(),
3506 user_address: "0xtest",
3507 user_api_key: "test-key",
3508 };
3509 let mut state = WsDispatchState::default();
3510
3511 let result = dispatch_user_message(&UserWsMessage::Order(order.clone()), &ctx, &mut state);
3512 assert!(result.is_none());
3513
3514 let venue_order_id = VenueOrderId::from(order.id.as_str());
3516 assert!(fill_tracker.has_pending_report(&venue_order_id));
3517 }
3518
3519 #[rstest]
3520 fn test_dispatch_order_message_ignores_missing_lifecycle_fields() {
3521 let order: PolymarketUserOrder = load("ws_user_order_placement.json");
3522 let instrument = test_instrument();
3523 let token_instruments = AtomicMap::new();
3524 token_instruments.insert(order.asset_id, instrument);
3525 let fill_tracker = OrderFillTrackerMap::new();
3526 let pending_submits = PendingSubmitTracker::default();
3527 let order_contexts = OrderContextRegistry::default();
3528 let emitter = test_emitter();
3529 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
3530 settlement.mark_live();
3531
3532 let ctx = WsDispatchContext {
3533 signer_type: PolymarketSignerType::Owner,
3534 token_instruments: &token_instruments,
3535 fill_tracker: &fill_tracker,
3536 settlement: &settlement,
3537 pending_submits: &pending_submits,
3538 order_contexts: &order_contexts,
3539 emitter: &emitter,
3540 account_id: AccountId::from("POLY-001"),
3541 clock: nautilus_core::time::get_atomic_clock_realtime(),
3542 user_address: "0xtest",
3543 user_api_key: "test-key",
3544 };
3545 let venue_order_id = VenueOrderId::from(order.id.as_str());
3546
3547 let mut missing_status = order.clone();
3548 missing_status.status = None;
3549 dispatch_user_message(
3550 &UserWsMessage::Order(missing_status),
3551 &ctx,
3552 &mut WsDispatchState::default(),
3553 );
3554 assert!(!fill_tracker.has_pending_report(&venue_order_id));
3555
3556 let mut missing_order_type = order;
3557 missing_order_type.order_type = None;
3558 dispatch_user_message(
3559 &UserWsMessage::Order(missing_order_type),
3560 &ctx,
3561 &mut WsDispatchState::default(),
3562 );
3563 assert!(!fill_tracker.has_pending_report(&venue_order_id));
3564 }
3565
3566 #[rstest]
3567 fn test_dispatch_order_message_uses_pending_submit_client_order_id() {
3568 let order: PolymarketUserOrder = load("ws_user_order_placement.json");
3569 let instrument = test_instrument();
3570
3571 let token_instruments = AtomicMap::new();
3572 token_instruments.insert(order.asset_id, instrument);
3573
3574 let fill_tracker = OrderFillTrackerMap::new();
3575 let pending_submits = PendingSubmitTracker::default();
3576 let order_contexts = OrderContextRegistry::default();
3577 let mut emitter = test_emitter();
3578 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
3579 emitter.set_sender(sender);
3580
3581 let venue_order_id = VenueOrderId::from(order.id.as_str());
3582 let client_order_id = ClientOrderId::from("O-UNKNOWN-SUBMIT");
3583 pending_submits.insert(venue_order_id, client_order_id);
3584 register_context(
3585 &order_contexts,
3586 venue_order_id,
3587 test_instrument().id(),
3588 "O-UNKNOWN-SUBMIT",
3589 );
3590
3591 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
3592 settlement.mark_live();
3593 settlement.note_order_submitted(venue_order_id);
3594
3595 let ctx = WsDispatchContext {
3596 signer_type: PolymarketSignerType::Owner,
3597 token_instruments: &token_instruments,
3598 fill_tracker: &fill_tracker,
3599 settlement: &settlement,
3600 pending_submits: &pending_submits,
3601 order_contexts: &order_contexts,
3602 emitter: &emitter,
3603 account_id: AccountId::from("POLY-001"),
3604 clock: nautilus_core::time::get_atomic_clock_realtime(),
3605 user_address: "0xtest",
3606 user_api_key: "test-key",
3607 };
3608 let mut state = WsDispatchState::default();
3609
3610 let _ = dispatch_user_message(&UserWsMessage::Order(order), &ctx, &mut state);
3611
3612 let event = receiver.try_recv().expect("expected accepted event");
3614 match event {
3615 ExecutionEvent::Order(OrderEventAny::Accepted(accepted)) => {
3616 assert_eq!(accepted.client_order_id, client_order_id);
3617 }
3618 other => panic!("Expected accepted event, was {other:?}"),
3619 }
3620
3621 assert!(!fill_tracker.has_pending_report(&venue_order_id));
3622 }
3623
3624 #[rstest]
3625 #[case(PolymarketSignerType::Owner, 1)]
3626 #[case(PolymarketSignerType::Session, 0)]
3627 fn test_dispatch_maker_fill_owned_by_case_variant_address(
3628 #[case] signer_type: PolymarketSignerType,
3629 #[case] expected_fills: usize,
3630 ) {
3631 let mut trade: PolymarketUserTrade = load("ws_user_trade.json");
3632 trade.trader_side = PolymarketLiquiditySide::Maker;
3633 let configured_address = trade.maker_orders[0].maker_address.clone();
3634 let case_variant_address = configured_address
3635 .to_ascii_uppercase()
3636 .replacen("0X", "0x", 1);
3637 assert_ne!(case_variant_address, configured_address);
3638 trade.maker_orders[0].maker_address = case_variant_address;
3639 let foreign_api_key = "ffffffff-ffff-ffff-ffff-ffffffffffff";
3640 assert_ne!(trade.maker_orders[0].owner, foreign_api_key);
3641
3642 let venue_order_id = VenueOrderId::from(trade.maker_orders[0].order_id.as_str());
3643 let token_instruments = AtomicMap::new();
3644 token_instruments.insert(trade.maker_orders[0].asset_id, instrument_for_trade(&trade));
3645 let fill_tracker = OrderFillTrackerMap::new();
3646 let pending_submits = PendingSubmitTracker::default();
3647 let order_contexts = OrderContextRegistry::default();
3648 let emitter = test_emitter();
3649 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
3650 settlement.mark_live();
3651 settlement.note_order_submitted(venue_order_id);
3652
3653 let ctx = WsDispatchContext {
3654 signer_type,
3655 token_instruments: &token_instruments,
3656 fill_tracker: &fill_tracker,
3657 settlement: &settlement,
3658 pending_submits: &pending_submits,
3659 order_contexts: &order_contexts,
3660 emitter: &emitter,
3661 account_id: AccountId::from("POLY-001"),
3662 clock: nautilus_core::time::get_atomic_clock_realtime(),
3663 user_address: &configured_address,
3664 user_api_key: foreign_api_key,
3665 };
3666 let mut state = WsDispatchState::default();
3667
3668 let _ = dispatch_user_message(&UserWsMessage::Trade(trade), &ctx, &mut state);
3669
3670 let fills = fill_tracker.pending_fills_for(&venue_order_id);
3671 assert_eq!(fills.len(), expected_fills);
3672
3673 for fill in fills {
3674 assert_eq!(fill.venue_order_id, venue_order_id);
3675 }
3676 }
3677
3678 #[rstest]
3679 fn test_dispatch_maker_fill_with_settlement_rounded_price_applies_without_quarantine() {
3680 let mut trade: PolymarketUserTrade = load("ws_user_trade.json");
3681 trade.trader_side = PolymarketLiquiditySide::Maker;
3682 trade.status = PolymarketTradeStatus::Matched;
3683 trade.maker_orders[0].price = dec!(0.4200001023529231);
3684 trade.maker_orders[0].matched_amount = dec!(1.758621);
3685 let configured_address = trade.maker_orders[0].maker_address.clone();
3686
3687 let venue_order_id = VenueOrderId::from(trade.maker_orders[0].order_id.as_str());
3688 let token_instruments = AtomicMap::new();
3689 token_instruments.insert(trade.maker_orders[0].asset_id, instrument_for_trade(&trade));
3690 let fill_tracker = OrderFillTrackerMap::new();
3691 let pending_submits = PendingSubmitTracker::default();
3692 let order_contexts = OrderContextRegistry::default();
3693 let emitter = test_emitter();
3694 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
3695 settlement.mark_live();
3696 settlement.note_order_submitted(venue_order_id);
3697
3698 let ctx = WsDispatchContext {
3699 signer_type: PolymarketSignerType::Owner,
3700 token_instruments: &token_instruments,
3701 fill_tracker: &fill_tracker,
3702 settlement: &settlement,
3703 pending_submits: &pending_submits,
3704 order_contexts: &order_contexts,
3705 emitter: &emitter,
3706 account_id: AccountId::from("POLY-001"),
3707 clock: nautilus_core::time::get_atomic_clock_realtime(),
3708 user_address: &configured_address,
3709 user_api_key: "ffffffff-ffff-ffff-ffff-ffffffffffff",
3710 };
3711 let mut state = WsDispatchState::default();
3712 let trade_id = trade.id.clone();
3713
3714 let _ = dispatch_user_message(&UserWsMessage::Trade(trade), &ctx, &mut state);
3715
3716 let fills = fill_tracker.pending_fills_for(&venue_order_id);
3717 assert_eq!(fills.len(), 1);
3718 assert_eq!(fills[0].last_px, Price::from("0.4200"));
3719 assert_eq!(fills[0].last_qty, Quantity::from("1.758621"));
3720 assert_eq!(
3721 settlement_state(&settlement, &trade_id),
3722 Some(SettlementState::Provisional)
3723 );
3724 }
3725
3726 #[rstest]
3727 #[case(dec!(-1))]
3728 #[case(Decimal::MAX)]
3729 fn test_dispatch_maker_numeric_failure_quarantines_trade(#[case] invalid_amount: Decimal) {
3730 let mut trade: PolymarketUserTrade = load("ws_user_trade.json");
3731 trade.status = PolymarketTradeStatus::Confirmed;
3732 trade.trader_side = PolymarketLiquiditySide::Maker;
3733 let configured_address = trade.maker_orders[0].maker_address.clone();
3734 let foreign_api_key = "ffffffff-ffff-ffff-ffff-ffffffffffff";
3735 assert_ne!(trade.maker_orders[0].owner, foreign_api_key);
3736
3737 let venue_order_id = VenueOrderId::from(trade.maker_orders[0].order_id.as_str());
3738 let token_instruments = AtomicMap::new();
3739 token_instruments.insert(trade.maker_orders[0].asset_id, instrument_for_trade(&trade));
3740 let fill_tracker = OrderFillTrackerMap::new();
3741 let pending_submits = PendingSubmitTracker::default();
3742 let order_contexts = OrderContextRegistry::default();
3743 let emitter = test_emitter();
3744 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
3745 settlement.mark_live();
3746 settlement.note_order_submitted(venue_order_id);
3747
3748 let ctx = WsDispatchContext {
3749 signer_type: PolymarketSignerType::Owner,
3750 token_instruments: &token_instruments,
3751 fill_tracker: &fill_tracker,
3752 settlement: &settlement,
3753 pending_submits: &pending_submits,
3754 order_contexts: &order_contexts,
3755 emitter: &emitter,
3756 account_id: AccountId::from("POLY-001"),
3757 clock: nautilus_core::time::get_atomic_clock_realtime(),
3758 user_address: &configured_address,
3759 user_api_key: foreign_api_key,
3760 };
3761
3762 let mut state = WsDispatchState::default();
3763
3764 let mut second_leg = trade.maker_orders[0].clone();
3765 second_leg.order_id = "O-MAKER-SECOND".to_string();
3766 second_leg.matched_amount = dec!(7);
3767 let second_venue_order_id = VenueOrderId::from(second_leg.order_id.as_str());
3768 settlement.note_order_submitted(second_venue_order_id);
3769 trade.maker_orders.push(second_leg);
3770
3771 let mut invalid_trade = trade.clone();
3772 invalid_trade.maker_orders[1].matched_amount = invalid_amount;
3773 dispatch_user_message(&UserWsMessage::Trade(invalid_trade), &ctx, &mut state);
3774 assert_eq!(fill_tracker.pending_fills_for(&venue_order_id).len(), 0);
3775 assert!(
3776 fill_tracker
3777 .pending_fills_for(&second_venue_order_id)
3778 .is_empty()
3779 );
3780 assert_eq!(
3781 settlement_state(&settlement, &trade.id),
3782 Some(SettlementState::Quarantined)
3783 );
3784
3785 assert!(
3786 admit_trade_evidence(TradeEvidence::Stream(&trade), &ctx.admission_context()).is_ok()
3787 );
3788 dispatch_user_message(&UserWsMessage::Trade(trade.clone()), &ctx, &mut state);
3789
3790 assert!(
3791 fill_tracker.pending_fills_for(&venue_order_id).is_empty(),
3792 "invalid stream evidence quarantines; a later valid copy stays REST-gated"
3793 );
3794 assert!(
3795 fill_tracker
3796 .pending_fills_for(&second_venue_order_id)
3797 .is_empty()
3798 );
3799 assert_eq!(
3800 settlement_state(&settlement, &trade.id),
3801 Some(SettlementState::Quarantined)
3802 );
3803 }
3804
3805 #[rstest]
3806 fn test_dispatch_trade_dedup() {
3807 let trade: PolymarketUserTrade = load("ws_user_trade.json");
3808 let instrument = instrument_for_trade(&trade);
3809
3810 let token_instruments = AtomicMap::new();
3811 token_instruments.insert(trade.asset_id, instrument);
3812
3813 let fill_tracker = OrderFillTrackerMap::new();
3814 let pending_submits = PendingSubmitTracker::default();
3815 let order_contexts = OrderContextRegistry::default();
3816 let emitter = test_emitter();
3817
3818 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
3819 settlement.note_order_submitted(VenueOrderId::from(trade.taker_order_id.as_str()));
3820
3821 let ctx = WsDispatchContext {
3822 signer_type: PolymarketSignerType::Owner,
3823 token_instruments: &token_instruments,
3824 fill_tracker: &fill_tracker,
3825 settlement: &settlement,
3826 pending_submits: &pending_submits,
3827 order_contexts: &order_contexts,
3828 emitter: &emitter,
3829 account_id: AccountId::from("POLY-001"),
3830 clock: nautilus_core::time::get_atomic_clock_realtime(),
3831 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
3832 user_api_key: "00000000-0000-0000-0000-000000000001",
3833 };
3834
3835 let mut state = WsDispatchState::default();
3836
3837 let venue_order_id = VenueOrderId::from(trade.taker_order_id.as_str());
3838
3839 let _ = dispatch_user_message(&UserWsMessage::Trade(trade.clone()), &ctx, &mut state);
3841 assert_eq!(fill_tracker.pending_fills_for(&venue_order_id).len(), 1);
3842
3843 let _ = dispatch_user_message(&UserWsMessage::Trade(trade), &ctx, &mut state);
3845 assert_eq!(fill_tracker.pending_fills_for(&venue_order_id).len(), 1);
3846 }
3847
3848 #[rstest]
3849 fn test_dispatch_taker_commission_failure_quarantines_trade() {
3850 let mut trade: PolymarketUserTrade = load("ws_user_trade.json");
3851 trade.status = PolymarketTradeStatus::Confirmed;
3852 let mut valid_instrument = instrument_for_trade(&trade);
3853 set_taker_fee_rate(&mut valid_instrument, dec!(0.03));
3854 let mut invalid_instrument = valid_instrument.clone();
3855 set_taker_fee_rate(
3856 &mut invalid_instrument,
3857 Decimal::from_i128_with_scale(100_000_000_000_000_000_000_000_000i128, 0),
3858 );
3859
3860 let token_instruments = AtomicMap::new();
3861 token_instruments.insert(trade.asset_id, invalid_instrument);
3862 let fill_tracker = OrderFillTrackerMap::new();
3863 let venue_order_id = VenueOrderId::from(trade.taker_order_id.as_str());
3864 fill_tracker.register(
3865 venue_order_id,
3866 Quantity::from("100"),
3867 OrderSide::Buy,
3868 valid_instrument.id(),
3869 valid_instrument.size_precision(),
3870 valid_instrument.price_precision(),
3871 );
3872 let pending_submits = PendingSubmitTracker::default();
3873 let order_contexts = OrderContextRegistry::default();
3874 register_context(
3875 &order_contexts,
3876 venue_order_id,
3877 valid_instrument.id(),
3878 "O-COMMISSION-REPLAY",
3879 );
3880 order_contexts.mark_accepted(venue_order_id);
3881 let mut emitter = test_emitter();
3882 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
3883 emitter.set_sender(sender);
3884 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
3885 settlement.mark_live();
3886 settlement.note_order_submitted(venue_order_id);
3887
3888 let ctx = WsDispatchContext {
3889 signer_type: PolymarketSignerType::Owner,
3890 token_instruments: &token_instruments,
3891 fill_tracker: &fill_tracker,
3892 settlement: &settlement,
3893 pending_submits: &pending_submits,
3894 order_contexts: &order_contexts,
3895 emitter: &emitter,
3896 account_id: AccountId::from("POLY-001"),
3897 clock: nautilus_core::time::get_atomic_clock_realtime(),
3898 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
3899 user_api_key: "00000000-0000-0000-0000-000000000001",
3900 };
3901
3902 let mut state = WsDispatchState::default();
3903
3904 let failed = dispatch_user_message(&UserWsMessage::Trade(trade.clone()), &ctx, &mut state);
3905
3906 assert!(failed.is_none());
3907 assert_eq!(
3908 settlement_state(&settlement, &trade.id),
3909 Some(SettlementState::Quarantined)
3910 );
3911 assert_eq!(
3912 fill_tracker.get_cumulative_filled(&venue_order_id),
3913 Some(Quantity::zero(valid_instrument.size_precision()))
3914 );
3915 assert!(receiver.try_recv().is_err());
3916
3917 token_instruments.insert(trade.asset_id, valid_instrument.clone());
3919 assert!(
3920 admit_trade_evidence(TradeEvidence::Stream(&trade), &ctx.admission_context()).is_ok()
3921 );
3922 let replay = dispatch_user_message(&UserWsMessage::Trade(trade.clone()), &ctx, &mut state);
3923
3924 assert!(replay.is_none());
3925 assert_eq!(
3926 settlement_state(&settlement, &trade.id),
3927 Some(SettlementState::Quarantined)
3928 );
3929 assert!(receiver.try_recv().is_err());
3930
3931 let mut rest_trade: PolymarketTradeReport = load("http_trade_report.json");
3932 rest_trade.size = dec!(17);
3933 rest_trade.price = dec!(0.4);
3934 rest_trade.maker_orders.truncate(1);
3935 rest_trade.maker_orders[0].matched_amount = dec!(17);
3936 rest_trade.maker_orders[0].price = dec!(0.4);
3937 rest_trade.status = PolymarketTradeStatus::Matched;
3938
3939 apply_rest_trade_evidence(&rest_trade, &ctx, &mut state);
3940
3941 assert_eq!(
3942 settlement_state(&settlement, &trade.id),
3943 Some(SettlementState::Quarantined)
3944 );
3945 assert!(receiver.try_recv().is_err());
3946
3947 rest_trade.status = PolymarketTradeStatus::Confirmed;
3948 apply_rest_trade_evidence(&rest_trade, &ctx, &mut state);
3949
3950 let filled = match receiver.try_recv().unwrap() {
3951 ExecutionEvent::Order(OrderEventAny::Filled(event)) => event,
3952 other => panic!("expected recovered fill, was {other:?}"),
3953 };
3954
3955 assert_eq!(filled.account_id, ctx.account_id);
3956 assert_eq!(
3957 filled.client_order_id,
3958 ClientOrderId::from("O-COMMISSION-REPLAY")
3959 );
3960 assert_eq!(filled.venue_order_id, venue_order_id);
3961 assert_eq!(filled.trade_id, TradeId::from(trade.id.as_str()));
3962 assert_eq!(filled.instrument_id, valid_instrument.id());
3963 assert_eq!(filled.order_side, OrderSide::Buy);
3964 assert_eq!(filled.liquidity_side, LiquiditySide::Taker);
3965 assert_eq!(filled.last_qty.as_decimal(), dec!(17));
3966 assert_eq!(filled.last_px.as_decimal(), dec!(0.4));
3967 assert_eq!(filled.commission, Some(Money::from("0.12240 pUSD")));
3968 assert_eq!(
3969 filled.ts_event,
3970 UnixNanos::from(1_704_067_200_000_000_000u64)
3971 );
3972 assert_eq!(
3973 fill_tracker.get_cumulative_filled(&venue_order_id),
3974 Some(Quantity::from("17"))
3975 );
3976 assert_eq!(
3977 settlement_state(&settlement, &trade.id),
3978 Some(SettlementState::RestConfirmed)
3979 );
3980 assert!(receiver.try_recv().is_err());
3981
3982 apply_rest_trade_evidence(&rest_trade, &ctx, &mut state);
3983 settlement.observe_fill_applied(&filled);
3984 apply_rest_trade_evidence(&rest_trade, &ctx, &mut state);
3985
3986 assert!(receiver.try_recv().is_err());
3987 assert_eq!(
3988 fill_tracker.get_cumulative_filled(&venue_order_id),
3989 Some(Quantity::from("17"))
3990 );
3991 assert!(trade_hard_fault(&settlement, &trade.id).is_none());
3992 }
3993
3994 #[rstest]
3995 fn test_dispatch_untimestamped_trade_quarantines_and_requests_resolution() {
3996 let mut trade: PolymarketUserTrade = load("ws_user_trade.json");
3997 trade.status = PolymarketTradeStatus::Matched;
3998 trade.match_time = String::new();
3999 trade.timestamp = "not-a-timestamp".to_string();
4000 let token_instruments = AtomicMap::new();
4001 token_instruments.insert(trade.asset_id, instrument_for_trade(&trade));
4002 let fill_tracker = OrderFillTrackerMap::new();
4003 let pending_submits = PendingSubmitTracker::default();
4004 let order_contexts = OrderContextRegistry::default();
4005 let mut emitter = test_emitter();
4006 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
4007 emitter.set_sender(sender);
4008 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
4009 settlement.mark_live();
4010 settlement.note_order_submitted(VenueOrderId::from(trade.taker_order_id.as_str()));
4011
4012 let ctx = WsDispatchContext {
4013 signer_type: PolymarketSignerType::Owner,
4014 token_instruments: &token_instruments,
4015 fill_tracker: &fill_tracker,
4016 settlement: &settlement,
4017 pending_submits: &pending_submits,
4018 order_contexts: &order_contexts,
4019 emitter: &emitter,
4020 account_id: AccountId::from("POLY-001"),
4021 clock: nautilus_core::time::get_atomic_clock_realtime(),
4022 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
4023 user_api_key: "00000000-0000-0000-0000-000000000001",
4024 };
4025
4026 let mut state = WsDispatchState::default();
4027
4028 let refresh = dispatch_user_message(&UserWsMessage::Trade(trade.clone()), &ctx, &mut state);
4029
4030 assert!(refresh.is_none());
4031 assert_eq!(
4032 settlement_state(&settlement, &trade.id),
4033 Some(SettlementState::Quarantined)
4034 );
4035 assert_eq!(settlement.pending_resolutions(), vec![trade.id]);
4036 assert!(receiver.try_recv().is_err());
4037 }
4038
4039 #[rstest]
4040 fn test_dispatch_trade_replays_after_instrument_becomes_available() {
4041 let trade: PolymarketUserTrade = load("ws_user_trade.json");
4042 let instrument = instrument_for_trade(&trade);
4043 let token_instruments = AtomicMap::new();
4044 let fill_tracker = OrderFillTrackerMap::new();
4045 let pending_submits = PendingSubmitTracker::default();
4046 let order_contexts = OrderContextRegistry::default();
4047 let emitter = test_emitter();
4048 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
4049 settlement.note_order_submitted(VenueOrderId::from(trade.taker_order_id.as_str()));
4050
4051 let ctx = WsDispatchContext {
4052 signer_type: PolymarketSignerType::Owner,
4053 token_instruments: &token_instruments,
4054 fill_tracker: &fill_tracker,
4055 settlement: &settlement,
4056 pending_submits: &pending_submits,
4057 order_contexts: &order_contexts,
4058 emitter: &emitter,
4059 account_id: AccountId::from("POLY-001"),
4060 clock: nautilus_core::time::get_atomic_clock_realtime(),
4061 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
4062 user_api_key: "00000000-0000-0000-0000-000000000001",
4063 };
4064
4065 let mut state = WsDispatchState::default();
4066 let venue_order_id = VenueOrderId::from(trade.taker_order_id.as_str());
4067
4068 let first_result =
4069 dispatch_user_message(&UserWsMessage::Trade(trade.clone()), &ctx, &mut state);
4070 token_instruments.insert(trade.asset_id, instrument);
4071 let replay_result = dispatch_user_message(&UserWsMessage::Trade(trade), &ctx, &mut state);
4072
4073 assert!(first_result.is_none());
4074 assert!(replay_result.is_some());
4075 assert_eq!(fill_tracker.pending_fills_for(&venue_order_id).len(), 1);
4076 }
4077
4078 #[rstest]
4079 #[case(crate::common::enums::PolymarketTradeStatus::Mined)]
4080 #[case(crate::common::enums::PolymarketTradeStatus::Retrying)]
4081 fn test_dispatch_trade_applies_mined_and_retrying_provisionally(
4082 #[case] status: crate::common::enums::PolymarketTradeStatus,
4083 ) {
4084 let mut trade: PolymarketUserTrade = load("ws_user_trade.json");
4085 trade.status = status;
4086 let instrument = instrument_for_trade(&trade);
4087 let token_instruments = AtomicMap::new();
4088 token_instruments.insert(trade.asset_id, instrument);
4089 let fill_tracker = OrderFillTrackerMap::new();
4090 let pending_submits = PendingSubmitTracker::default();
4091 let order_contexts = OrderContextRegistry::default();
4092 let emitter = test_emitter();
4093 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
4094 settlement.note_order_submitted(VenueOrderId::from(trade.taker_order_id.as_str()));
4095
4096 let ctx = WsDispatchContext {
4097 signer_type: PolymarketSignerType::Owner,
4098 token_instruments: &token_instruments,
4099 fill_tracker: &fill_tracker,
4100 settlement: &settlement,
4101 pending_submits: &pending_submits,
4102 order_contexts: &order_contexts,
4103 emitter: &emitter,
4104 account_id: AccountId::from("POLY-001"),
4105 clock: nautilus_core::time::get_atomic_clock_realtime(),
4106 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
4107 user_api_key: "00000000-0000-0000-0000-000000000001",
4108 };
4109
4110 let mut state = WsDispatchState::default();
4111 let venue_order_id = VenueOrderId::from(trade.taker_order_id.as_str());
4112 let trade_id = trade.id.clone();
4113
4114 let result = dispatch_user_message(&UserWsMessage::Trade(trade), &ctx, &mut state);
4115
4116 assert!(result.is_none());
4117 assert_eq!(fill_tracker.pending_fills_for(&venue_order_id).len(), 1);
4118 assert_eq!(
4119 settlement_state(&settlement, &trade_id),
4120 Some(SettlementState::Provisional)
4121 );
4122 }
4123
4124 #[rstest]
4125 #[case::taker(false)]
4126 #[case::maker(true)]
4127 fn test_dispatch_matched_fill_confirmed_by_rest_at_wire_precision_does_not_conflict(
4128 #[case] maker: bool,
4129 ) {
4130 let mut trade: PolymarketUserTrade = load("ws_user_trade.json");
4131 trade.status = PolymarketTradeStatus::Matched;
4132
4133 if maker {
4134 trade.trader_side = PolymarketLiquiditySide::Maker;
4135 trade.side = PolymarketOrderSide::Sell;
4136 trade.maker_orders[0].matched_amount = dec!(20);
4137 }
4138
4139 let (order_id, user_address, api_key) = if maker {
4140 let leg = &trade.maker_orders[0];
4141 (
4142 leg.order_id.as_str(),
4143 leg.maker_address.as_str(),
4144 leg.owner.as_str(),
4145 )
4146 } else {
4147 (
4148 trade.taker_order_id.as_str(),
4149 trade.maker_address.as_str(),
4150 trade.owner.as_str(),
4151 )
4152 };
4153
4154 let instrument = instrument_for_trade(&trade);
4155 let token_instruments = AtomicMap::new();
4156 token_instruments.insert(trade.asset_id, instrument.clone());
4157 let fill_tracker = OrderFillTrackerMap::new();
4158 let venue_order_id = VenueOrderId::from(order_id);
4159 fill_tracker.register(
4160 venue_order_id,
4161 Quantity::from("100"),
4162 OrderSide::Buy,
4163 instrument.id(),
4164 instrument.size_precision(),
4165 instrument.price_precision(),
4166 );
4167 let pending_submits = PendingSubmitTracker::default();
4168 let order_contexts = OrderContextRegistry::default();
4169 register_context(
4170 &order_contexts,
4171 venue_order_id,
4172 instrument.id(),
4173 "O-MATCHED-CONFIRMED",
4174 );
4175 order_contexts.mark_accepted(venue_order_id);
4176 let mut emitter = test_emitter();
4177 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
4178 emitter.set_sender(sender);
4179 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
4180 settlement.mark_live();
4181 settlement.note_order_submitted(venue_order_id);
4182
4183 let ctx = WsDispatchContext {
4184 signer_type: PolymarketSignerType::Owner,
4185 token_instruments: &token_instruments,
4186 fill_tracker: &fill_tracker,
4187 settlement: &settlement,
4188 pending_submits: &pending_submits,
4189 order_contexts: &order_contexts,
4190 emitter: &emitter,
4191 account_id: AccountId::from("POLY-001"),
4192 clock: nautilus_core::time::get_atomic_clock_realtime(),
4193 user_address,
4194 user_api_key: api_key,
4195 };
4196
4197 let mut state = WsDispatchState::default();
4198
4199 let _ = dispatch_user_message(&UserWsMessage::Trade(trade.clone()), &ctx, &mut state);
4200
4201 let filled = match receiver.try_recv().unwrap() {
4202 ExecutionEvent::Order(OrderEventAny::Filled(event)) => event,
4203 other => panic!("expected matched fill, was {other:?}"),
4204 };
4205
4206 settlement.observe_fill_applied(&filled);
4207
4208 let mut rest_confirmed: PolymarketTradeReport =
4209 serde_json::from_str(include_str!("../../test_data/http_trade_report.json"))
4210 .expect("REST trade fixture");
4211 rest_confirmed.id = trade.id.clone();
4212 rest_confirmed.taker_order_id = trade.taker_order_id.clone();
4213 rest_confirmed.trader_side = trade.trader_side;
4214 rest_confirmed.side = trade.side;
4215
4216 if maker {
4217 rest_confirmed.maker_orders[0].side = Some(PolymarketOrderSide::Buy);
4218 }
4219
4220 let rest_evidence = admit_trade_evidence(
4221 TradeEvidence::Rest(&rest_confirmed),
4222 &ctx.admission_context(),
4223 )
4224 .expect("REST CONFIRMED evidence admits");
4225 let rest_actions = settlement.admit_rest_result(&rest_evidence);
4226 execute_settlement_actions(
4227 rest_actions,
4228 rest_trade_info(&rest_confirmed).as_ref(),
4229 &ctx,
4230 &mut state,
4231 );
4232
4233 assert_eq!(rest_evidence.legs[0].last_px, filled.last_px);
4234 assert_eq!(rest_evidence.legs[0].venue_order_id, filled.venue_order_id);
4235 assert_eq!(rest_evidence.legs[0].trade_id, filled.trade_id);
4236 assert_eq!(rest_evidence.legs[0].instrument_id, filled.instrument_id);
4237 assert_eq!(rest_evidence.legs[0].order_side, OrderSide::Buy);
4238 assert_eq!(filled.order_side, OrderSide::Buy);
4239
4240 let liquidity = if maker {
4241 LiquiditySide::Maker
4242 } else {
4243 LiquiditySide::Taker
4244 };
4245
4246 assert_eq!(rest_evidence.legs[0].liquidity_side, liquidity);
4247 assert_eq!(filled.liquidity_side, liquidity);
4248 assert_eq!(rest_evidence.legs[0].last_qty, filled.last_qty);
4249 assert_eq!(rest_evidence.legs[0].commission, filled.commission.unwrap());
4250 assert_eq!(rest_evidence.legs[0].ts_event, filled.ts_event);
4251 assert_ne!(
4252 rest_evidence.legs[0].last_px.precision,
4253 filled.last_px.precision
4254 );
4255 assert!(trade_hard_fault(&settlement, &trade.id).is_none());
4256 assert_eq!(
4257 settlement_state(&settlement, &trade.id),
4258 Some(SettlementState::RestConfirmed)
4259 );
4260 assert!(receiver.try_recv().is_err());
4261 assert_eq!(
4262 fill_tracker.get_cumulative_filled(&venue_order_id),
4263 Some(filled.last_qty)
4264 );
4265 }
4266
4267 #[rstest]
4268 #[case::open_order(PolymarketOrderStatus::Live, None, true, false)]
4269 #[case::canceled_order(PolymarketOrderStatus::Canceled, None, true, false)]
4270 #[case::confirmed_fill(
4271 PolymarketOrderStatus::Matched,
4272 Some(PolymarketTradeStatus::Confirmed),
4273 true,
4274 true
4275 )]
4276 #[case::pending_trade(
4277 PolymarketOrderStatus::Matched,
4278 Some(PolymarketTradeStatus::Matched),
4279 false,
4280 false
4281 )]
4282 #[case::matched_before_trades_listed(PolymarketOrderStatus::Matched, None, false, false)]
4283 fn test_apply_uncertain_order_evidence(
4284 #[case] order_status: PolymarketOrderStatus,
4285 #[case] trade_status: Option<PolymarketTradeStatus>,
4286 #[case] expected_applied: bool,
4287 #[case] expected_fill: bool,
4288 #[values(false, true)] locally_closed: bool,
4289 #[values(false, true)] replacement_promoted: bool,
4290 ) {
4291 let mut order: PolymarketOpenOrder =
4292 serde_json::from_str(include_str!("../../test_data/http_open_order.json"))
4293 .expect("REST open order fixture");
4294 order.status = order_status;
4295 order.original_size = dec!(25);
4296
4297 order.size_matched = if order_status == PolymarketOrderStatus::Matched {
4298 dec!(25)
4299 } else {
4300 Decimal::ZERO
4301 };
4302
4303 let mut trade: PolymarketTradeReport =
4304 serde_json::from_str(include_str!("../../test_data/http_trade_report.json"))
4305 .expect("REST trade fixture");
4306 trade.status = trade_status.unwrap_or(PolymarketTradeStatus::Confirmed);
4307 let trades = trade_status
4308 .map(|_| vec![trade.clone()])
4309 .unwrap_or_default();
4310 let ws_trade: PolymarketUserTrade = load("ws_user_trade.json");
4311 let instrument = instrument_for_trade(&ws_trade);
4312 let token_instruments = AtomicMap::new();
4313 token_instruments.insert(order.asset_id, instrument.clone());
4314 let venue_order_id = VenueOrderId::from(order.id.as_str());
4315 let client_order_id = ClientOrderId::from("O-UNCERTAIN");
4316 let fill_tracker = OrderFillTrackerMap::new();
4317 let pending_submits = PendingSubmitTracker::default();
4318 pending_submits.insert(venue_order_id, client_order_id);
4319 let order_contexts = OrderContextRegistry::default();
4320 register_context(
4321 &order_contexts,
4322 venue_order_id,
4323 instrument.id(),
4324 client_order_id.as_str(),
4325 );
4326 order_contexts.set_closed(client_order_id, locally_closed);
4327 let mut emitter = test_emitter();
4328 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
4329 emitter.set_sender(sender);
4330 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
4331 settlement.begin_session();
4332
4333 let ctx = WsDispatchContext {
4334 signer_type: PolymarketSignerType::Owner,
4335 token_instruments: &token_instruments,
4336 fill_tracker: &fill_tracker,
4337 settlement: &settlement,
4338 pending_submits: &pending_submits,
4339 order_contexts: &order_contexts,
4340 emitter: &emitter,
4341 account_id: AccountId::from("POLY-001"),
4342 clock: nautilus_core::time::get_atomic_clock_realtime(),
4343 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
4344 user_api_key: "00000000-0000-0000-0000-000000000001",
4345 };
4346
4347 let mut state = WsDispatchState::default();
4348
4349 if replacement_promoted {
4350 let replacement_id = VenueOrderId::from("V-REPLACEMENT");
4351 assert!(state.begin_modify(client_order_id, venue_order_id, instrument.id()));
4352 assert!(state.set_modify_replacement(
4353 client_order_id,
4354 replacement_id,
4355 Quantity::from("25"),
4356 Quantity::from("25"),
4357 Quantity::from("0"),
4358 Price::from("0.6"),
4359 ));
4360 assert!(state.claim_modify_replacement(replacement_id).is_some());
4361 order_contexts.mark_accepted(venue_order_id);
4362 }
4363
4364 let applied =
4365 apply_uncertain_order_evidence(venue_order_id, &order, &trades, &ctx, &mut state);
4366
4367 let mut events = Vec::new();
4368
4369 while let Ok(event) = receiver.try_recv() {
4370 events.push(event);
4371 }
4372
4373 let accepted = events.iter().filter(|event| {
4374 matches!(event, ExecutionEvent::Order(OrderEventAny::Accepted(accepted))
4375 if accepted.client_order_id == client_order_id
4376 && accepted.venue_order_id == venue_order_id)
4377 });
4378
4379 let fills: Vec<&OrderFilled> = events
4380 .iter()
4381 .filter_map(|event| match event {
4382 ExecutionEvent::Order(OrderEventAny::Filled(filled)) => Some(filled),
4383 _ => None,
4384 })
4385 .collect();
4386
4387 assert_eq!(applied, expected_applied);
4388 assert_eq!(
4389 accepted.count(),
4390 usize::from(expected_applied && !locally_closed && !replacement_promoted)
4391 );
4392 assert_eq!(fill_tracker.contains(&venue_order_id), expected_applied);
4393
4394 if expected_fill {
4395 assert_eq!(fills.len(), 1);
4396 assert_eq!(fills[0].client_order_id, client_order_id);
4397 assert_eq!(fills[0].venue_order_id, venue_order_id);
4398 assert_eq!(fills[0].trade_id, TradeId::from(trade.id.as_str()));
4399 assert_eq!(fills[0].last_qty.as_decimal(), dec!(25));
4400 assert_eq!(fills[0].last_px.as_decimal(), dec!(0.5));
4401 assert_eq!(
4402 settlement_state(&settlement, &trade.id),
4403 Some(SettlementState::RestConfirmed)
4404 );
4405 } else {
4406 assert!(fills.is_empty());
4407 }
4408
4409 let canceled = events
4410 .iter()
4411 .filter(|event| {
4412 matches!(event, ExecutionEvent::Order(OrderEventAny::Canceled(canceled))
4413 if canceled.client_order_id == client_order_id
4414 && canceled.venue_order_id == Some(venue_order_id))
4415 })
4416 .count();
4417
4418 let expected_cancel = usize::from(
4419 order_status == PolymarketOrderStatus::Canceled
4420 && !locally_closed
4421 && !replacement_promoted,
4422 );
4423
4424 assert_eq!(canceled, expected_cancel);
4425 assert_eq!(
4426 events.len(),
4427 usize::from(expected_applied && !locally_closed && !replacement_promoted)
4428 + fills.len()
4429 + expected_cancel
4430 );
4431 }
4432
4433 #[rstest]
4434 fn test_apply_rest_trade_evidence_closes_ioc_remainder_on_confirmation() {
4435 let trade: PolymarketTradeReport =
4436 serde_json::from_str(include_str!("../../test_data/http_trade_report.json"))
4437 .expect("REST trade fixture");
4438 let ws_trade: PolymarketUserTrade = load("ws_user_trade.json");
4439 let instrument = instrument_for_trade(&ws_trade);
4440 let token_instruments = AtomicMap::new();
4441 token_instruments.insert(trade.asset_id, instrument.clone());
4442 let venue_order_id = VenueOrderId::from(trade.taker_order_id.as_str());
4443 let fill_tracker = OrderFillTrackerMap::new();
4444 fill_tracker.register(
4445 venue_order_id,
4446 Quantity::from("100.0000"),
4447 OrderSide::Buy,
4448 instrument.id(),
4449 instrument.size_precision(),
4450 instrument.price_precision(),
4451 );
4452 let pending_submits = PendingSubmitTracker::default();
4453 let order_contexts = OrderContextRegistry::default();
4454 register_context(&order_contexts, venue_order_id, instrument.id(), "O-IOC");
4455 let mut context = order_contexts
4456 .get(&venue_order_id)
4457 .expect("registered context");
4458 context.time_in_force = TimeInForce::Ioc;
4459 order_contexts.register_context(venue_order_id, context);
4460 order_contexts.mark_accepted(venue_order_id);
4461 let mut emitter = test_emitter();
4462 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
4463 emitter.set_sender(sender);
4464 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
4465 settlement.begin_session();
4466
4467 let ctx = WsDispatchContext {
4468 signer_type: PolymarketSignerType::Owner,
4469 token_instruments: &token_instruments,
4470 fill_tracker: &fill_tracker,
4471 settlement: &settlement,
4472 pending_submits: &pending_submits,
4473 order_contexts: &order_contexts,
4474 emitter: &emitter,
4475 account_id: AccountId::from("POLY-001"),
4476 clock: nautilus_core::time::get_atomic_clock_realtime(),
4477 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
4478 user_api_key: "00000000-0000-0000-0000-000000000001",
4479 };
4480 let mut state = WsDispatchState::default();
4481
4482 apply_rest_trade_evidence(&trade, &ctx, &mut state);
4483
4484 let filled = match receiver.try_recv().unwrap() {
4485 ExecutionEvent::Order(OrderEventAny::Filled(event)) => event,
4486 other => panic!("expected REST-confirmed fill, was {other:?}"),
4487 };
4488
4489 let canceled = match receiver.try_recv().unwrap() {
4490 ExecutionEvent::Order(OrderEventAny::Canceled(event)) => event,
4491 other => panic!("expected IOC remainder cancel, was {other:?}"),
4492 };
4493
4494 assert_eq!(filled.last_qty.as_decimal(), dec!(25));
4495 assert_eq!(filled.trade_id, TradeId::from(trade.id.as_str()));
4496 assert_eq!(canceled.client_order_id, ClientOrderId::from("O-IOC"));
4497 assert_eq!(canceled.venue_order_id, Some(venue_order_id));
4498 assert!(receiver.try_recv().is_err());
4499 assert_eq!(
4500 settlement_state(&settlement, &trade.id),
4501 Some(SettlementState::RestConfirmed)
4502 );
4503
4504 apply_rest_trade_evidence(&trade, &ctx, &mut state);
4505 assert!(receiver.try_recv().is_err());
4506 }
4507
4508 #[rstest]
4509 fn test_apply_rest_trade_evidence_normalizes_fok_dust_once() {
4510 let trade: PolymarketTradeReport =
4511 serde_json::from_str(include_str!("../../test_data/http_trade_report.json"))
4512 .expect("REST trade fixture");
4513 let ws_trade: PolymarketUserTrade = load("ws_user_trade.json");
4514 let instrument = instrument_for_trade(&ws_trade);
4515 let token_instruments = AtomicMap::new();
4516 token_instruments.insert(trade.asset_id, instrument.clone());
4517 let venue_order_id = VenueOrderId::from(trade.taker_order_id.as_str());
4518 let submitted_qty = Quantity::from("25.005000");
4519 let fill_tracker = OrderFillTrackerMap::new();
4520 fill_tracker.register(
4521 venue_order_id,
4522 submitted_qty,
4523 OrderSide::Buy,
4524 instrument.id(),
4525 instrument.size_precision(),
4526 instrument.price_precision(),
4527 );
4528 let pending_submits = PendingSubmitTracker::default();
4529 let order_contexts = OrderContextRegistry::default();
4530 register_context(&order_contexts, venue_order_id, instrument.id(), "O-FOK");
4531 let mut context = order_contexts
4532 .get(&venue_order_id)
4533 .expect("registered context");
4534 context.time_in_force = TimeInForce::Fok;
4535 order_contexts.register_context(venue_order_id, context);
4536 order_contexts.mark_accepted(venue_order_id);
4537 let mut emitter = test_emitter();
4538 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
4539 emitter.set_sender(sender);
4540 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
4541 settlement.begin_session();
4542
4543 let ctx = WsDispatchContext {
4544 signer_type: PolymarketSignerType::Owner,
4545 token_instruments: &token_instruments,
4546 fill_tracker: &fill_tracker,
4547 settlement: &settlement,
4548 pending_submits: &pending_submits,
4549 order_contexts: &order_contexts,
4550 emitter: &emitter,
4551 account_id: AccountId::from("POLY-001"),
4552 clock: nautilus_core::time::get_atomic_clock_realtime(),
4553 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
4554 user_api_key: "00000000-0000-0000-0000-000000000001",
4555 };
4556 let mut state = WsDispatchState::default();
4557
4558 apply_rest_trade_evidence(&trade, &ctx, &mut state);
4559
4560 let filled = match receiver.try_recv().unwrap() {
4561 ExecutionEvent::Order(OrderEventAny::Filled(event)) => event,
4562 other => panic!("expected REST-confirmed fill, was {other:?}"),
4563 };
4564 let updated = match receiver.try_recv().unwrap() {
4565 ExecutionEvent::Order(OrderEventAny::Updated(event)) => event,
4566 other => panic!("expected FOK quantity normalization, was {other:?}"),
4567 };
4568
4569 assert_eq!(filled.last_qty.as_decimal(), dec!(25));
4570 assert_eq!(updated.quantity, filled.last_qty);
4571 assert_eq!(updated.client_order_id, ClientOrderId::from("O-FOK"));
4572 assert!(receiver.try_recv().is_err());
4573
4574 apply_rest_trade_evidence(&trade, &ctx, &mut state);
4575 assert!(receiver.try_recv().is_err());
4576 assert_eq!(
4577 settlement_state(&settlement, &trade.id),
4578 Some(SettlementState::RestConfirmed)
4579 );
4580 assert_eq!(trade_hard_fault(&settlement, &trade.id), None);
4581 }
4582
4583 struct TerminalFaultHarness {
4584 trade: PolymarketUserTrade,
4585 token_instruments: AtomicMap<Ustr, InstrumentAny>,
4586 venue_order_id: VenueOrderId,
4587 fill_tracker: OrderFillTrackerMap,
4588 pending_submits: PendingSubmitTracker,
4589 order_contexts: OrderContextRegistry,
4590 emitter: ExecutionEventEmitter,
4591 receiver: tokio::sync::mpsc::UnboundedReceiver<ExecutionEvent>,
4592 settlement: SettlementRegistry,
4593 fill: OrderFilled,
4594 }
4595
4596 impl TerminalFaultHarness {
4597 fn new(time_in_force: TimeInForce, submitted_qty: &str, client_order_id: &str) -> Self {
4598 let mut trade: PolymarketUserTrade = load("ws_user_trade.json");
4599 trade.status = PolymarketTradeStatus::Matched;
4600 let mut instrument = instrument_for_trade(&trade);
4601 set_taker_fee_rate(&mut instrument, dec!(0.0721));
4602 let token_instruments = AtomicMap::new();
4603 token_instruments.insert(trade.asset_id, instrument.clone());
4604 let venue_order_id = VenueOrderId::from(trade.taker_order_id.as_str());
4605 let fill_tracker = OrderFillTrackerMap::new();
4606 fill_tracker.register(
4607 venue_order_id,
4608 Quantity::from(submitted_qty),
4609 OrderSide::Buy,
4610 instrument.id(),
4611 instrument.size_precision(),
4612 instrument.price_precision(),
4613 );
4614 let pending_submits = PendingSubmitTracker::default();
4615 pending_submits.insert(venue_order_id, ClientOrderId::from(client_order_id));
4616 let order_contexts = OrderContextRegistry::default();
4617 register_context(
4618 &order_contexts,
4619 venue_order_id,
4620 instrument.id(),
4621 client_order_id,
4622 );
4623 let mut context = order_contexts
4624 .get(&venue_order_id)
4625 .expect("registered context");
4626 context.time_in_force = time_in_force;
4627 order_contexts.register_context(venue_order_id, context);
4628 order_contexts.mark_accepted(venue_order_id);
4629 let mut emitter = test_emitter();
4630 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
4631 emitter.set_sender(sender);
4632 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
4633 settlement.begin_session();
4634 settlement.note_order_submitted(venue_order_id);
4635 let ctx = WsDispatchContext {
4636 signer_type: PolymarketSignerType::Owner,
4637 token_instruments: &token_instruments,
4638 fill_tracker: &fill_tracker,
4639 settlement: &settlement,
4640 pending_submits: &pending_submits,
4641 order_contexts: &order_contexts,
4642 emitter: &emitter,
4643 account_id: AccountId::from("POLY-001"),
4644 clock: nautilus_core::time::get_atomic_clock_realtime(),
4645 user_address: trade.maker_address.as_str(),
4646 user_api_key: trade.owner.as_str(),
4647 };
4648 let mut state = WsDispatchState::default();
4649 dispatch_user_message(&UserWsMessage::Trade(trade.clone()), &ctx, &mut state);
4650 let fill = match receiver.try_recv().expect("matched fill") {
4651 ExecutionEvent::Order(OrderEventAny::Filled(fill)) => fill,
4652 other => panic!("expected matched fill, was {other:?}"),
4653 };
4654 settlement.observe_fill_applied(&fill);
4655 assert!(
4656 receiver.try_recv().is_err(),
4657 "provisional match emitted terminal bookkeeping"
4658 );
4659
4660 Self {
4661 trade,
4662 token_instruments,
4663 venue_order_id,
4664 fill_tracker,
4665 pending_submits,
4666 order_contexts,
4667 emitter,
4668 receiver,
4669 settlement,
4670 fill,
4671 }
4672 }
4673
4674 fn ctx(&self) -> WsDispatchContext<'_> {
4675 WsDispatchContext {
4676 signer_type: PolymarketSignerType::Owner,
4677 token_instruments: &self.token_instruments,
4678 fill_tracker: &self.fill_tracker,
4679 settlement: &self.settlement,
4680 pending_submits: &self.pending_submits,
4681 order_contexts: &self.order_contexts,
4682 emitter: &self.emitter,
4683 account_id: AccountId::from("POLY-001"),
4684 clock: nautilus_core::time::get_atomic_clock_realtime(),
4685 user_address: self.trade.maker_address.as_str(),
4686 user_api_key: self.trade.owner.as_str(),
4687 }
4688 }
4689
4690 fn contradicting_rest(&self) -> PolymarketTradeReport {
4691 let mut rest: PolymarketTradeReport = load("http_trade_report.json");
4692 rest.id = self.trade.id.clone();
4693 rest.taker_order_id = self.trade.taker_order_id.clone();
4694 rest.asset_id = self.trade.asset_id;
4695 rest.price = dec!(0.6);
4696 for maker in &mut rest.maker_orders {
4697 maker.price = rest.price;
4698 }
4699
4700 rest.status = PolymarketTradeStatus::Confirmed;
4701 rest
4702 }
4703 }
4704
4705 fn assert_no_fault_bookkeeping(events: &[ExecutionEvent]) {
4706 assert!(
4707 events.iter().all(|event| !matches!(
4708 event,
4709 ExecutionEvent::Order(
4710 OrderEventAny::Updated(_)
4711 | OrderEventAny::Canceled(_)
4712 | OrderEventAny::Filled(_)
4713 | OrderEventAny::FillVoided(_)
4714 )
4715 )),
4716 "faulted trade authorized bookkeeping, was {events:?}"
4717 );
4718 }
4719
4720 fn assert_fault_retained(harness: &TerminalFaultHarness) {
4721 assert_eq!(harness.fill.last_qty, Quantity::from("25.000000"));
4722 assert_eq!(harness.fill.last_px.as_decimal(), dec!(0.5));
4723 assert_eq!(harness.fill.commission.unwrap().as_decimal(), dec!(0.45062));
4724 assert_eq!(
4725 settlement_state(&harness.settlement, &harness.trade.id),
4726 Some(SettlementState::RestConfirmed)
4727 );
4728 assert!(trade_hard_fault(&harness.settlement, &harness.trade.id).is_some());
4729 assert!(harness.settlement.is_trade_confirmed(&harness.trade.id));
4730 assert!(
4731 harness
4732 .settlement
4733 .ensure_resolved(None, "mass status")
4734 .is_err()
4735 );
4736 assert!(harness.fill_tracker.contains(&harness.venue_order_id));
4737 }
4738
4739 #[rstest]
4740 #[case::fok_dust(TimeInForce::Fok, "25.005000", "O-FAULT-FOK")]
4741 #[case::ioc_partial(TimeInForce::Ioc, "100.000000", "O-FAULT-IOC")]
4742 fn test_contradicting_rest_does_not_authorize_terminal_bookkeeping(
4743 #[case] time_in_force: TimeInForce,
4744 #[case] submitted_qty: &str,
4745 #[case] client_order_id: &str,
4746 ) {
4747 let mut harness = TerminalFaultHarness::new(time_in_force, submitted_qty, client_order_id);
4748 let rest = harness.contradicting_rest();
4749 let mut state = WsDispatchState::default();
4750 {
4751 let ctx = harness.ctx();
4752 apply_rest_trade_evidence(&rest, &ctx, &mut state);
4753 apply_rest_trade_evidence(&rest, &ctx, &mut state);
4754 }
4755
4756 let events: Vec<_> = std::iter::from_fn(|| harness.receiver.try_recv().ok()).collect();
4757 assert_no_fault_bookkeeping(&events);
4758 assert_fault_retained(&harness);
4759 }
4760
4761 #[rstest]
4762 #[case::fok_dust(TimeInForce::Fok, "25.005000", "O-UNCERTAIN-FOK", PolymarketOrderType::FOK, dec!(25.005))]
4763 #[case::ioc_partial(TimeInForce::Ioc, "100.000000", "O-UNCERTAIN-IOC", PolymarketOrderType::FAK, dec!(100))]
4764 fn test_uncertain_filled_recovery_does_not_bypass_fault_guard(
4765 #[case] time_in_force: TimeInForce,
4766 #[case] submitted_qty: &str,
4767 #[case] client_order_id: &str,
4768 #[case] order_type: PolymarketOrderType,
4769 #[case] original_size: Decimal,
4770 ) {
4771 let mut harness = TerminalFaultHarness::new(time_in_force, submitted_qty, client_order_id);
4772 let rest = harness.contradicting_rest();
4773 let mut order: PolymarketOpenOrder =
4774 serde_json::from_str(include_str!("../../test_data/http_open_order.json"))
4775 .expect("REST open order fixture");
4776 order.status = PolymarketOrderStatus::Matched;
4777 order.order_type = order_type;
4778 order.original_size = original_size;
4779 order.size_matched = dec!(25);
4780 let mut state = WsDispatchState::default();
4781 let applied = {
4782 let ctx = harness.ctx();
4783 apply_uncertain_order_evidence(
4784 harness.venue_order_id,
4785 &order,
4786 std::slice::from_ref(&rest),
4787 &ctx,
4788 &mut state,
4789 )
4790 };
4791
4792 let events: Vec<_> = std::iter::from_fn(|| harness.receiver.try_recv().ok()).collect();
4793 assert!(applied);
4794 assert_no_fault_bookkeeping(&events);
4795 assert_fault_retained(&harness);
4796 }
4797
4798 #[rstest]
4799 fn test_uncertain_venue_cancel_still_emits_after_hard_fault() {
4800 let mut harness =
4801 TerminalFaultHarness::new(TimeInForce::Fok, "25.005000", "O-VENUE-CANCEL");
4802 let rest = harness.contradicting_rest();
4803 let mut state = WsDispatchState::default();
4804 {
4805 let ctx = harness.ctx();
4806 apply_rest_trade_evidence(&rest, &ctx, &mut state);
4807 }
4808 let fault_events: Vec<_> =
4809 std::iter::from_fn(|| harness.receiver.try_recv().ok()).collect();
4810 assert_no_fault_bookkeeping(&fault_events);
4811
4812 let mut order: PolymarketOpenOrder =
4813 serde_json::from_str(include_str!("../../test_data/http_open_order.json"))
4814 .expect("REST open order fixture");
4815 order.status = PolymarketOrderStatus::Canceled;
4816 order.order_type = PolymarketOrderType::FAK;
4817 order.original_size = dec!(100);
4818 order.size_matched = dec!(25);
4819 let applied = {
4820 let ctx = harness.ctx();
4821 apply_uncertain_order_evidence(
4822 harness.venue_order_id,
4823 &order,
4824 std::slice::from_ref(&rest),
4825 &ctx,
4826 &mut state,
4827 )
4828 };
4829 let canceled = match harness.receiver.try_recv().expect("venue cancel") {
4830 ExecutionEvent::Order(OrderEventAny::Canceled(event)) => event,
4831 other => panic!("expected venue cancel, was {other:?}"),
4832 };
4833
4834 assert!(applied);
4835 assert_eq!(
4836 canceled.client_order_id,
4837 ClientOrderId::from("O-VENUE-CANCEL")
4838 );
4839 assert_eq!(canceled.venue_order_id, Some(harness.venue_order_id));
4840 assert!(harness.receiver.try_recv().is_err());
4841 assert_fault_retained(&harness);
4842 }
4843
4844 #[rstest]
4845 fn test_apply_uncertain_order_evidence_normalizes_filled_fok_dust() {
4846 let mut order: PolymarketOpenOrder =
4847 serde_json::from_str(include_str!("../../test_data/http_open_order.json"))
4848 .expect("REST open order fixture");
4849 order.status = PolymarketOrderStatus::Matched;
4850 order.original_size = dec!(25.005);
4851 order.size_matched = dec!(25);
4852 let trade: PolymarketTradeReport =
4853 serde_json::from_str(include_str!("../../test_data/http_trade_report.json"))
4854 .expect("REST trade fixture");
4855 let ws_trade: PolymarketUserTrade = load("ws_user_trade.json");
4856 let instrument = instrument_for_trade(&ws_trade);
4857 let token_instruments = AtomicMap::new();
4858 token_instruments.insert(order.asset_id, instrument.clone());
4859 let venue_order_id = VenueOrderId::from(order.id.as_str());
4860 let client_order_id = ClientOrderId::from("O-FOK");
4861 let fill_tracker = OrderFillTrackerMap::new();
4862 let pending_submits = PendingSubmitTracker::default();
4863 pending_submits.insert(venue_order_id, client_order_id);
4864 let order_contexts = OrderContextRegistry::default();
4865 register_context(
4866 &order_contexts,
4867 venue_order_id,
4868 instrument.id(),
4869 client_order_id.as_str(),
4870 );
4871 let mut context = order_contexts
4872 .get(&venue_order_id)
4873 .expect("registered context");
4874 context.time_in_force = TimeInForce::Fok;
4875 order_contexts.register_context(venue_order_id, context);
4876 let mut emitter = test_emitter();
4877 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
4878 emitter.set_sender(sender);
4879 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
4880 settlement.begin_session();
4881
4882 let ctx = WsDispatchContext {
4883 signer_type: PolymarketSignerType::Owner,
4884 token_instruments: &token_instruments,
4885 fill_tracker: &fill_tracker,
4886 settlement: &settlement,
4887 pending_submits: &pending_submits,
4888 order_contexts: &order_contexts,
4889 emitter: &emitter,
4890 account_id: AccountId::from("POLY-001"),
4891 clock: nautilus_core::time::get_atomic_clock_realtime(),
4892 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
4893 user_api_key: "00000000-0000-0000-0000-000000000001",
4894 };
4895 let mut state = WsDispatchState::default();
4896
4897 let applied = apply_uncertain_order_evidence(
4898 venue_order_id,
4899 &order,
4900 std::slice::from_ref(&trade),
4901 &ctx,
4902 &mut state,
4903 );
4904
4905 let mut events = Vec::new();
4906
4907 while let Ok(ExecutionEvent::Order(event)) = receiver.try_recv() {
4908 events.push(event);
4909 }
4910
4911 assert!(applied);
4912 assert_eq!(events.len(), 3, "unexpected events: {events:?}");
4913 assert!(matches!(&events[0], OrderEventAny::Accepted(_)));
4914
4915 let OrderEventAny::Filled(filled) = &events[1] else {
4916 panic!("expected fill, was {:?}", events[1]);
4917 };
4918
4919 let OrderEventAny::Updated(updated) = &events[2] else {
4920 panic!("expected terminal quantity update, was {:?}", events[2]);
4921 };
4922
4923 assert_eq!(filled.last_qty.as_decimal(), dec!(25));
4924 assert_eq!(updated.client_order_id, client_order_id);
4925 assert_eq!(updated.quantity.as_decimal(), dec!(25));
4926
4927 let repeated = apply_uncertain_order_evidence(
4928 venue_order_id,
4929 &order,
4930 std::slice::from_ref(&trade),
4931 &ctx,
4932 &mut state,
4933 );
4934 assert!(repeated);
4935 assert!(receiver.try_recv().is_err());
4936 }
4937
4938 #[rstest]
4939 #[case::identical_repeat(dec!(25), dec!(50))]
4940 #[case::contradicting_repeat(dec!(10), dec!(25))]
4941 fn test_apply_uncertain_order_evidence_deduplicates_repeated_rows(
4942 #[case] repeat_size: Decimal,
4943 #[case] size_matched: Decimal,
4944 ) {
4945 let mut order: PolymarketOpenOrder =
4946 serde_json::from_str(include_str!("../../test_data/http_open_order.json"))
4947 .expect("REST open order fixture");
4948 order.status = PolymarketOrderStatus::Matched;
4949 order.original_size = size_matched;
4950 order.size_matched = size_matched;
4951 let trade: PolymarketTradeReport =
4952 serde_json::from_str(include_str!("../../test_data/http_trade_report.json"))
4953 .expect("REST trade fixture");
4954 let mut repeat = trade.clone();
4955 repeat.size = repeat_size;
4956 let ws_trade: PolymarketUserTrade = load("ws_user_trade.json");
4957 let instrument = instrument_for_trade(&ws_trade);
4958 let token_instruments = AtomicMap::new();
4959 token_instruments.insert(order.asset_id, instrument.clone());
4960 let venue_order_id = VenueOrderId::from(order.id.as_str());
4961 let fill_tracker = OrderFillTrackerMap::new();
4962 let pending_submits = PendingSubmitTracker::default();
4963 pending_submits.insert(venue_order_id, ClientOrderId::from("O-UNCERTAIN"));
4964 let order_contexts = OrderContextRegistry::default();
4965 register_context(
4966 &order_contexts,
4967 venue_order_id,
4968 instrument.id(),
4969 "O-UNCERTAIN",
4970 );
4971 let mut emitter = test_emitter();
4972 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
4973 emitter.set_sender(sender);
4974 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
4975 settlement.begin_session();
4976
4977 let ctx = WsDispatchContext {
4978 signer_type: PolymarketSignerType::Owner,
4979 token_instruments: &token_instruments,
4980 fill_tracker: &fill_tracker,
4981 settlement: &settlement,
4982 pending_submits: &pending_submits,
4983 order_contexts: &order_contexts,
4984 emitter: &emitter,
4985 account_id: AccountId::from("POLY-001"),
4986 clock: nautilus_core::time::get_atomic_clock_realtime(),
4987 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
4988 user_api_key: "00000000-0000-0000-0000-000000000001",
4989 };
4990 let mut state = WsDispatchState::default();
4991
4992 let applied = apply_uncertain_order_evidence(
4993 venue_order_id,
4994 &order,
4995 &[trade, repeat],
4996 &ctx,
4997 &mut state,
4998 );
4999
5000 assert!(!applied);
5001 assert!(!fill_tracker.contains(&venue_order_id));
5002 assert!(receiver.try_recv().is_err());
5003 }
5004
5005 #[rstest]
5006 #[case::other_venue_order_id(Some("0xother"), PolymarketOrderSide::Buy)]
5007 #[case::contradicting_side(None, PolymarketOrderSide::Sell)]
5008 fn test_apply_uncertain_order_evidence_rejects_contradicting_row(
5009 #[case] row_id: Option<&str>,
5010 #[case] row_side: PolymarketOrderSide,
5011 ) {
5012 let mut order: PolymarketOpenOrder =
5013 serde_json::from_str(include_str!("../../test_data/http_open_order.json"))
5014 .expect("REST open order fixture");
5015 let venue_order_id = VenueOrderId::from(order.id.as_str());
5016
5017 if let Some(row_id) = row_id {
5018 order.id = row_id.to_string();
5019 }
5020
5021 order.side = row_side;
5022 let ws_trade: PolymarketUserTrade = load("ws_user_trade.json");
5023 let instrument = instrument_for_trade(&ws_trade);
5024 let token_instruments = AtomicMap::new();
5025 token_instruments.insert(order.asset_id, instrument.clone());
5026 let fill_tracker = OrderFillTrackerMap::new();
5027 let pending_submits = PendingSubmitTracker::default();
5028 pending_submits.insert(venue_order_id, ClientOrderId::from("O-UNCERTAIN"));
5029 let order_contexts = OrderContextRegistry::default();
5030 register_context(
5031 &order_contexts,
5032 venue_order_id,
5033 instrument.id(),
5034 "O-UNCERTAIN",
5035 );
5036 let mut emitter = test_emitter();
5037 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
5038 emitter.set_sender(sender);
5039 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
5040
5041 let ctx = WsDispatchContext {
5042 signer_type: PolymarketSignerType::Owner,
5043 token_instruments: &token_instruments,
5044 fill_tracker: &fill_tracker,
5045 settlement: &settlement,
5046 pending_submits: &pending_submits,
5047 order_contexts: &order_contexts,
5048 emitter: &emitter,
5049 account_id: AccountId::from("POLY-001"),
5050 clock: nautilus_core::time::get_atomic_clock_realtime(),
5051 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
5052 user_api_key: "00000000-0000-0000-0000-000000000001",
5053 };
5054 let mut state = WsDispatchState::default();
5055
5056 let applied = apply_uncertain_order_evidence(venue_order_id, &order, &[], &ctx, &mut state);
5057
5058 assert!(!applied);
5059 assert!(!fill_tracker.contains(&venue_order_id));
5060 assert!(receiver.try_recv().is_err());
5061 }
5062
5063 #[rstest]
5064 fn test_apply_stream_gap_order_evidence_applies_only_unseen_trades() {
5065 let mut order: PolymarketOpenOrder =
5066 serde_json::from_str(include_str!("../../test_data/http_open_order.json"))
5067 .expect("REST open order fixture");
5068 order.status = PolymarketOrderStatus::Canceled;
5069 order.original_size = dec!(100);
5070 order.size_matched = dec!(50);
5071 let seen: PolymarketTradeReport =
5072 serde_json::from_str(include_str!("../../test_data/http_trade_report.json"))
5073 .expect("REST trade fixture");
5074 let mut unseen = seen.clone();
5075 unseen.id = "trade-0xunseen".to_string();
5076 let trades = vec![seen.clone(), unseen.clone()];
5077 let ws_trade: PolymarketUserTrade = load("ws_user_trade.json");
5078 let instrument = instrument_for_trade(&ws_trade);
5079 let token_instruments = AtomicMap::new();
5080 token_instruments.insert(order.asset_id, instrument.clone());
5081 let venue_order_id = VenueOrderId::from(order.id.as_str());
5082 let client_order_id = ClientOrderId::from("O-STREAM-GAP");
5083 let fill_tracker = OrderFillTrackerMap::new();
5084 fill_tracker.register(
5085 venue_order_id,
5086 Quantity::from("100.0000"),
5087 OrderSide::Buy,
5088 instrument.id(),
5089 instrument.size_precision(),
5090 instrument.price_precision(),
5091 );
5092 let pending_submits = PendingSubmitTracker::default();
5093 pending_submits.insert(venue_order_id, client_order_id);
5094 let order_contexts = OrderContextRegistry::default();
5095 register_context(
5096 &order_contexts,
5097 venue_order_id,
5098 instrument.id(),
5099 client_order_id.as_str(),
5100 );
5101 order_contexts.mark_accepted(venue_order_id);
5102 let mut emitter = test_emitter();
5103 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
5104 emitter.set_sender(sender);
5105 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
5106 settlement.begin_session();
5107
5108 settlement.hydrate_fill(&OrderFilled::new(
5110 TraderId::from("TESTER-001"),
5111 StrategyId::from("S-001"),
5112 instrument.id(),
5113 client_order_id,
5114 venue_order_id,
5115 AccountId::from("POLY-001"),
5116 TradeId::from(seen.id.as_str()),
5117 OrderSide::Buy,
5118 OrderType::Limit,
5119 Quantity::from("24.9900"),
5120 Price::from("0.5000"),
5121 Currency::pUSD(),
5122 LiquiditySide::Taker,
5123 UUID4::new(),
5124 UnixNanos::from(1_u64),
5125 UnixNanos::from(1_u64),
5126 false,
5127 None,
5128 None,
5129 None,
5130 ));
5131
5132 let ctx = WsDispatchContext {
5133 signer_type: PolymarketSignerType::Owner,
5134 token_instruments: &token_instruments,
5135 fill_tracker: &fill_tracker,
5136 settlement: &settlement,
5137 pending_submits: &pending_submits,
5138 order_contexts: &order_contexts,
5139 emitter: &emitter,
5140 account_id: AccountId::from("POLY-001"),
5141 clock: nautilus_core::time::get_atomic_clock_realtime(),
5142 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
5143 user_api_key: "00000000-0000-0000-0000-000000000001",
5144 };
5145
5146 let mut state = WsDispatchState::default();
5147
5148 let applied =
5149 apply_stream_gap_order_evidence(venue_order_id, &order, &trades, &ctx, &mut state);
5150
5151 let mut events = Vec::new();
5152
5153 while let Ok(event) = receiver.try_recv() {
5154 events.push(event);
5155 }
5156
5157 let ExecutionEvent::Order(OrderEventAny::Filled(filled)) = &events[0] else {
5158 panic!("expected the unseen trade's fill, was {:?}", events[0]);
5159 };
5160
5161 assert!(applied);
5162 assert_eq!(events.len(), 1);
5163 assert_eq!(filled.client_order_id, client_order_id);
5164 assert_eq!(filled.venue_order_id, venue_order_id);
5165 assert_eq!(filled.trade_id, TradeId::from(unseen.id.as_str()));
5166 assert_eq!(filled.last_qty.as_decimal(), dec!(25));
5167 assert_eq!(filled.last_px.as_decimal(), dec!(0.5));
5168 assert_eq!(
5169 settlement_state(&settlement, &unseen.id),
5170 Some(SettlementState::RestConfirmed)
5171 );
5172 assert_eq!(
5173 settlement_state(&settlement, &seen.id),
5174 Some(SettlementState::Provisional)
5175 );
5176 assert!(trade_hard_fault(&settlement, &seen.id).is_none());
5177 }
5178
5179 #[rstest]
5180 fn test_dispatch_matched_trade_emits_fill_and_failed_trade_quarantines_then_rest_voids_it() {
5181 let mut trade: PolymarketUserTrade = load("ws_user_trade.json");
5182 trade.status = crate::common::enums::PolymarketTradeStatus::Matched;
5183 let instrument = instrument_for_trade(&trade);
5184 let token_instruments = AtomicMap::new();
5185 token_instruments.insert(trade.asset_id, instrument.clone());
5186 let fill_tracker = OrderFillTrackerMap::new();
5187 let venue_order_id = VenueOrderId::from(trade.taker_order_id.as_str());
5188 fill_tracker.register(
5189 venue_order_id,
5190 Quantity::from("100"),
5191 OrderSide::Buy,
5192 instrument.id(),
5193 instrument.size_precision(),
5194 instrument.price_precision(),
5195 );
5196 let pending_submits = PendingSubmitTracker::default();
5197 let order_contexts = OrderContextRegistry::default();
5198 register_context(
5199 &order_contexts,
5200 venue_order_id,
5201 instrument.id(),
5202 "O-MATCHED-FAILED",
5203 );
5204 order_contexts.mark_accepted(venue_order_id);
5205 let mut emitter = test_emitter();
5206 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
5207 emitter.set_sender(sender);
5208 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
5209 settlement.mark_live();
5210 settlement.note_order_submitted(venue_order_id);
5211
5212 let ctx = WsDispatchContext {
5213 signer_type: PolymarketSignerType::Owner,
5214 token_instruments: &token_instruments,
5215 fill_tracker: &fill_tracker,
5216 settlement: &settlement,
5217 pending_submits: &pending_submits,
5218 order_contexts: &order_contexts,
5219 emitter: &emitter,
5220 account_id: AccountId::from("POLY-001"),
5221 clock: nautilus_core::time::get_atomic_clock_realtime(),
5222 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
5223 user_api_key: "00000000-0000-0000-0000-000000000001",
5224 };
5225 let mut state = WsDispatchState::default();
5226
5227 let matched = dispatch_user_message(&UserWsMessage::Trade(trade.clone()), &ctx, &mut state);
5229 let filled = match receiver.try_recv().unwrap() {
5230 ExecutionEvent::Order(OrderEventAny::Filled(event)) => event,
5231 other => panic!("expected matched fill, was {other:?}"),
5232 };
5233
5234 assert!(matched.is_none());
5235 assert_eq!(
5236 settlement_state(&settlement, &trade.id),
5237 Some(SettlementState::Provisional)
5238 );
5239
5240 settlement.observe_fill_applied(&filled);
5242
5243 trade.status = crate::common::enums::PolymarketTradeStatus::Failed;
5245 let failed = dispatch_user_message(&UserWsMessage::Trade(trade.clone()), &ctx, &mut state);
5246 assert!(failed.is_none());
5247 assert_eq!(
5248 settlement_state(&settlement, &trade.id),
5249 Some(SettlementState::Quarantined)
5250 );
5251 assert!(receiver.try_recv().is_err());
5252 assert_eq!(
5253 fill_tracker.get_cumulative_filled(&venue_order_id),
5254 Some(filled.last_qty)
5255 );
5256
5257 let mut rest_failed: PolymarketTradeReport =
5259 serde_json::from_str(include_str!("../../test_data/http_trade_report.json"))
5260 .expect("REST trade fixture");
5261 rest_failed.id = trade.id.clone();
5262 rest_failed.taker_order_id = trade.taker_order_id.clone();
5263 rest_failed.status = PolymarketTradeStatus::Failed;
5264 rest_failed.trader_side = PolymarketLiquiditySide::Taker;
5265 let rest_evidence =
5266 admit_trade_evidence(TradeEvidence::Rest(&rest_failed), &ctx.admission_context())
5267 .expect("REST FAILED evidence admits");
5268 let rest_actions = settlement.admit_rest_result(&rest_evidence);
5269 execute_settlement_actions(
5270 rest_actions,
5271 rest_trade_info(&rest_failed).as_ref(),
5272 &ctx,
5273 &mut state,
5274 );
5275
5276 let voided = match receiver.try_recv().unwrap() {
5277 ExecutionEvent::Order(OrderEventAny::FillVoided(event)) => event,
5278 other => panic!("expected failed fill correction, was {other:?}"),
5279 };
5280
5281 assert!(settlement.take_account_refresh());
5282 assert!(trade_hard_fault(&settlement, &trade.id).is_none());
5283 assert_eq!(
5284 settlement_state(&settlement, &trade.id),
5285 Some(SettlementState::RestFailed)
5286 );
5287 assert_eq!(voided.trade_id, filled.trade_id);
5288 assert_eq!(voided.voided_qty, filled.last_qty);
5289 assert_eq!(voided.commission_voided, filled.commission);
5290 assert_eq!(voided.last_px, filled.last_px);
5291 assert!(!voided.is_reopened);
5292 assert_eq!(voided.causation_id, Some(filled.event_id));
5293 assert_eq!(
5294 fill_tracker.get_cumulative_filled(&venue_order_id),
5295 Some(Quantity::zero(instrument.size_precision()))
5296 );
5297
5298 trade.status = PolymarketTradeStatus::Matched;
5300 let stale_trade_id = trade.id.clone();
5301 let matched_after_failure =
5302 dispatch_user_message(&UserWsMessage::Trade(trade), &ctx, &mut state);
5303 assert!(matched_after_failure.is_none());
5304 assert_eq!(
5305 settlement_state(&settlement, &stale_trade_id),
5306 Some(SettlementState::RestFailed)
5307 );
5308 assert!(receiver.try_recv().is_err());
5309 }
5310
5311 #[rstest]
5312 fn test_dispatch_trade_uses_pending_submit_client_order_id() {
5313 let trade: PolymarketUserTrade = load("ws_user_trade.json");
5314 let instrument = instrument_for_trade(&trade);
5315
5316 let token_instruments = AtomicMap::new();
5317 token_instruments.insert(trade.asset_id, instrument);
5318
5319 let fill_tracker = OrderFillTrackerMap::new();
5320 let pending_submits = PendingSubmitTracker::default();
5321 let order_contexts = OrderContextRegistry::default();
5322 let emitter = test_emitter();
5323
5324 let venue_order_id = VenueOrderId::from(trade.taker_order_id.as_str());
5325 let client_order_id = ClientOrderId::from("O-UNKNOWN-FILL");
5326 pending_submits.insert(venue_order_id, client_order_id);
5327
5328 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
5329 settlement.mark_live();
5330 settlement.note_order_submitted(venue_order_id);
5331
5332 let ctx = WsDispatchContext {
5333 signer_type: PolymarketSignerType::Owner,
5334 token_instruments: &token_instruments,
5335 fill_tracker: &fill_tracker,
5336 settlement: &settlement,
5337 pending_submits: &pending_submits,
5338 order_contexts: &order_contexts,
5339 emitter: &emitter,
5340 account_id: AccountId::from("POLY-001"),
5341 clock: nautilus_core::time::get_atomic_clock_realtime(),
5342 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
5343 user_api_key: "00000000-0000-0000-0000-000000000001",
5344 };
5345 let mut state = WsDispatchState::default();
5346
5347 let _ = dispatch_user_message(&UserWsMessage::Trade(trade), &ctx, &mut state);
5348
5349 let fills = fill_tracker.pending_fills_for(&venue_order_id);
5350 assert_eq!(fills[0].client_order_id, Some(client_order_id));
5351 }
5352
5353 #[rstest]
5354 fn test_dispatch_late_fill_stays_tracked_after_later_registrations() {
5355 let trade: PolymarketUserTrade = load("ws_user_trade.json");
5356 let market: GammaMarket = load("gamma_market_sports_market_money_line.json");
5357 let defs = parse_gamma_market(&market).unwrap();
5358 let instrument = bind_instrument(
5359 create_instrument_from_def(&defs[0], UnixNanos::from(1_000_000_000u64)).unwrap(),
5360 trade.market.as_str(),
5361 trade.outcome,
5362 );
5363 let venue_order_id = VenueOrderId::from(trade.taker_order_id.as_str());
5364
5365 let token_instruments = AtomicMap::new();
5366 token_instruments.insert(trade.asset_id, instrument.clone());
5367
5368 let fill_tracker = OrderFillTrackerMap::new();
5369 fill_tracker.register(
5370 venue_order_id,
5371 Quantity::from("100"),
5372 OrderSide::Buy,
5373 instrument.id(),
5374 instrument.size_precision(),
5375 instrument.price_precision(),
5376 );
5377
5378 let pending_submits = PendingSubmitTracker::default();
5379 let order_contexts = OrderContextRegistry::default();
5380 register_context(
5381 &order_contexts,
5382 venue_order_id,
5383 instrument.id(),
5384 "O-LATE-FILL",
5385 );
5386 order_contexts.mark_accepted(venue_order_id);
5387 assert!(order_contexts.get(&venue_order_id).is_some());
5388
5389 for index in 0..10_000 {
5390 let later_venue_order_id = VenueOrderId::from(format!("V-LATER-{index}").as_str());
5391 let later_client_order_id = format!("O-LATER-{index}");
5392 register_context(
5393 &order_contexts,
5394 later_venue_order_id,
5395 instrument.id(),
5396 &later_client_order_id,
5397 );
5398 order_contexts.mark_accepted(later_venue_order_id);
5399 fill_tracker.register(
5400 later_venue_order_id,
5401 Quantity::from("1"),
5402 OrderSide::Sell,
5403 instrument.id(),
5404 instrument.size_precision(),
5405 instrument.price_precision(),
5406 );
5407 }
5408 assert!(order_contexts.get(&venue_order_id).is_some());
5409 assert!(fill_tracker.contains(&venue_order_id));
5410
5411 let mut emitter = test_emitter();
5412 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
5413 emitter.set_sender(sender);
5414
5415 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
5416 settlement.mark_live();
5417 settlement.note_order_submitted(venue_order_id);
5418
5419 let ctx = WsDispatchContext {
5420 signer_type: PolymarketSignerType::Owner,
5421 token_instruments: &token_instruments,
5422 fill_tracker: &fill_tracker,
5423 settlement: &settlement,
5424 pending_submits: &pending_submits,
5425 order_contexts: &order_contexts,
5426 emitter: &emitter,
5427 account_id: AccountId::from("POLY-001"),
5428 clock: nautilus_core::time::get_atomic_clock_realtime(),
5429 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
5430 user_api_key: "00000000-0000-0000-0000-000000000001",
5431 };
5432 let mut state = WsDispatchState::default();
5433
5434 dispatch_user_message(&UserWsMessage::Trade(trade.clone()), &ctx, &mut state);
5435
5436 let event = receiver.try_recv().expect("expected tracked late fill");
5437 let ExecutionEvent::Order(OrderEventAny::Filled(filled)) = event else {
5438 panic!("expected tracked OrderFilled after later registrations, was {event:?}");
5439 };
5440
5441 assert_eq!(filled.client_order_id, ClientOrderId::from("O-LATE-FILL"));
5442 assert_eq!(filled.venue_order_id, venue_order_id);
5443 assert_eq!(filled.trade_id, TradeId::from(trade.id.as_str()));
5444 assert_eq!(filled.instrument_id, instrument.id());
5445 assert_eq!(
5446 filled.last_qty.as_decimal(),
5447 Decimal::from_str_exact(&trade.size).unwrap()
5448 );
5449 assert_eq!(
5450 filled.last_px.as_decimal(),
5451 Decimal::from_str_exact(&trade.price).unwrap()
5452 );
5453 assert_eq!(filled.order_side, OrderSide::Buy);
5454 assert_eq!(filled.liquidity_side, LiquiditySide::Taker);
5455 let commission = filled.commission.expect("tracked fill has commission");
5456 assert_eq!(commission.as_decimal(), dec!(0.1875));
5457 assert_eq!(commission.currency, Currency::pUSD());
5458 assert!(receiver.try_recv().is_err());
5459 }
5460
5461 #[rstest]
5462 fn test_dispatch_order_matched_caps_filled_qty_when_no_trades_tracked() {
5463 let order: PolymarketUserOrder = load("ws_user_order_matched.json");
5464 let instrument = test_instrument();
5465
5466 let token_instruments = AtomicMap::new();
5467 token_instruments.insert(order.asset_id, instrument.clone());
5468
5469 let fill_tracker = OrderFillTrackerMap::new();
5470 let venue_order_id = VenueOrderId::from(order.id.as_str());
5471
5472 fill_tracker.register(
5474 venue_order_id,
5475 Quantity::from("100"),
5476 OrderSide::Buy,
5477 instrument.id(),
5478 instrument.size_precision(),
5479 instrument.price_precision(),
5480 );
5481
5482 let pending_submits = PendingSubmitTracker::default();
5483 let order_contexts = OrderContextRegistry::default();
5486 let mut emitter = test_emitter();
5487 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
5488 emitter.set_sender(sender);
5489
5490 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
5491 settlement.mark_live();
5492 settlement.note_order_submitted(venue_order_id);
5493
5494 let ctx = WsDispatchContext {
5495 signer_type: PolymarketSignerType::Owner,
5496 token_instruments: &token_instruments,
5497 fill_tracker: &fill_tracker,
5498 settlement: &settlement,
5499 pending_submits: &pending_submits,
5500 order_contexts: &order_contexts,
5501 emitter: &emitter,
5502 account_id: AccountId::from("POLY-001"),
5503 clock: nautilus_core::time::get_atomic_clock_realtime(),
5504 user_address: "0xtest",
5505 user_api_key: "test-key",
5506 };
5507 let mut state = WsDispatchState::default();
5508
5509 dispatch_user_message(&UserWsMessage::Order(order), &ctx, &mut state);
5510
5511 let event = receiver.try_recv().expect("Expected report");
5512 match event {
5513 ExecutionEvent::Report(report) => match report {
5514 ExecutionReport::Order(order_report) => {
5515 assert_eq!(order_report.filled_qty, Quantity::from("0"));
5516 }
5517 other => panic!("Expected order report, was {other:?}"),
5518 },
5519 other => panic!("Expected report event, was {other:?}"),
5520 }
5521 }
5522
5523 #[rstest]
5524 fn test_dispatch_order_matched_uses_tracked_fills_for_filled_qty() {
5525 let order: PolymarketUserOrder = load("ws_user_order_matched.json");
5526 let instrument = test_instrument();
5527
5528 let token_instruments = AtomicMap::new();
5529 token_instruments.insert(order.asset_id, instrument.clone());
5530
5531 let fill_tracker = OrderFillTrackerMap::new();
5532 let venue_order_id = VenueOrderId::from(order.id.as_str());
5533
5534 fill_tracker.register(
5536 venue_order_id,
5537 Quantity::from("100"),
5538 OrderSide::Buy,
5539 instrument.id(),
5540 instrument.size_precision(),
5541 instrument.price_precision(),
5542 );
5543 fill_tracker.record_fill(&venue_order_id, Quantity::new(50.0, 6));
5544
5545 let pending_submits = PendingSubmitTracker::default();
5546 let order_contexts = OrderContextRegistry::default();
5549 let mut emitter = test_emitter();
5550 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
5551 emitter.set_sender(sender);
5552
5553 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
5554 settlement.mark_live();
5555 settlement.note_order_submitted(venue_order_id);
5556
5557 let ctx = WsDispatchContext {
5558 signer_type: PolymarketSignerType::Owner,
5559 token_instruments: &token_instruments,
5560 fill_tracker: &fill_tracker,
5561 settlement: &settlement,
5562 pending_submits: &pending_submits,
5563 order_contexts: &order_contexts,
5564 emitter: &emitter,
5565 account_id: AccountId::from("POLY-001"),
5566 clock: nautilus_core::time::get_atomic_clock_realtime(),
5567 user_address: "0xtest",
5568 user_api_key: "test-key",
5569 };
5570 let mut state = WsDispatchState::default();
5571
5572 dispatch_user_message(&UserWsMessage::Order(order), &ctx, &mut state);
5573
5574 let event = receiver.try_recv().expect("Expected report");
5575 match event {
5576 ExecutionEvent::Report(report) => match report {
5577 ExecutionReport::Order(order_report) => {
5578 assert_eq!(order_report.filled_qty, Quantity::from("50"));
5579 }
5580 other => panic!("Expected order report, was {other:?}"),
5581 },
5582 other => panic!("Expected report event, was {other:?}"),
5583 }
5584 }
5585
5586 #[rstest]
5587 fn test_dispatch_order_matched_normalizes_quantity_without_fill() {
5588 let order: PolymarketUserOrder = load("ws_user_order_matched.json");
5589 let instrument = test_instrument();
5590
5591 let token_instruments = AtomicMap::new();
5592 token_instruments.insert(order.asset_id, instrument.clone());
5593
5594 let fill_tracker = OrderFillTrackerMap::new();
5595 let venue_order_id = VenueOrderId::from(order.id.as_str());
5596 fill_tracker.register(
5597 venue_order_id,
5598 Quantity::from("100"),
5599 OrderSide::Buy,
5600 instrument.id(),
5601 instrument.size_precision(),
5602 instrument.price_precision(),
5603 );
5604 fill_tracker.record_fill(&venue_order_id, Quantity::new(99.995, 6));
5605
5606 let pending_submits = PendingSubmitTracker::default();
5607 let order_contexts = OrderContextRegistry::default();
5608 register_context(
5609 &order_contexts,
5610 venue_order_id,
5611 instrument.id(),
5612 "O-MATCHED",
5613 );
5614 order_contexts.mark_accepted(venue_order_id);
5615 let mut emitter = test_emitter();
5616 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
5617 emitter.set_sender(sender);
5618
5619 let clock = Box::leak(Box::new(AtomicTime::new(
5620 false,
5621 UnixNanos::from(2_000_000_000u64),
5622 )));
5623
5624 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
5625 settlement.mark_live();
5626 settlement.note_order_submitted(venue_order_id);
5627
5628 let ctx = WsDispatchContext {
5629 signer_type: PolymarketSignerType::Owner,
5630 token_instruments: &token_instruments,
5631 fill_tracker: &fill_tracker,
5632 settlement: &settlement,
5633 pending_submits: &pending_submits,
5634 order_contexts: &order_contexts,
5635 emitter: &emitter,
5636 account_id: AccountId::from("POLY-001"),
5637 clock,
5638 user_address: "0xtest",
5639 user_api_key: "test-key",
5640 };
5641 let mut state = WsDispatchState::default();
5642
5643 let _ = settlement.admit_rest_result(&AdmittedTrade {
5644 venue_trade_id: "trade-0xfill1".to_string(),
5645 status: PolymarketTradeStatus::Confirmed,
5646 legs: Vec::new(),
5647 });
5648
5649 dispatch_user_message(&UserWsMessage::Order(order), &ctx, &mut state);
5650
5651 let event = receiver.try_recv().expect("expected quantity update");
5652 match event {
5653 ExecutionEvent::Order(OrderEventAny::Updated(updated)) => {
5654 assert_eq!(
5655 updated.ts_event,
5656 UnixNanos::from(1_703_875_201_000_000_000u64)
5657 );
5658 assert_eq!(updated.ts_init, UnixNanos::from(2_000_000_000u64));
5659 assert_eq!(updated.quantity, Quantity::new(99.995, 6));
5660 assert!(updated.reconciliation);
5661 }
5662 other => panic!("expected updated event, was {other:?}"),
5663 }
5664 assert!(receiver.try_recv().is_err());
5665 }
5666
5667 #[rstest]
5668 fn test_confirmed_trade_normalizes_pending_matched_quantity() {
5669 let mut order: PolymarketUserOrder = load("ws_user_order_matched.json");
5670 let mut trade: PolymarketUserTrade = load("ws_user_trade.json");
5671 let instrument = instrument_for_trade(&trade);
5672 order.associate_trades = Some(vec![trade.id.clone()]);
5673 trade.size = "99.995".to_string();
5674 trade.price = order.price.clone();
5675 trade.maker_orders[0].matched_amount = dec!(99.995);
5676 trade.maker_orders[0].price = trade.price.parse().unwrap();
5677
5678 let token_instruments = AtomicMap::new();
5679 token_instruments.insert(order.asset_id, instrument.clone());
5680 let fill_tracker = OrderFillTrackerMap::new();
5681 let venue_order_id = VenueOrderId::from(order.id.as_str());
5682 fill_tracker.register(
5683 venue_order_id,
5684 Quantity::from("100"),
5685 OrderSide::Buy,
5686 instrument.id(),
5687 instrument.size_precision(),
5688 instrument.price_precision(),
5689 );
5690 let pending_submits = PendingSubmitTracker::default();
5691 let order_contexts = OrderContextRegistry::default();
5692 register_context(
5693 &order_contexts,
5694 venue_order_id,
5695 instrument.id(),
5696 "O-CONFIRMED-DUST",
5697 );
5698 order_contexts.mark_accepted(venue_order_id);
5699 let mut emitter = test_emitter();
5700 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
5701 emitter.set_sender(sender);
5702 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
5703 settlement.mark_live();
5704 settlement.note_order_submitted(venue_order_id);
5705
5706 let ctx = WsDispatchContext {
5707 signer_type: PolymarketSignerType::Owner,
5708 token_instruments: &token_instruments,
5709 fill_tracker: &fill_tracker,
5710 settlement: &settlement,
5711 pending_submits: &pending_submits,
5712 order_contexts: &order_contexts,
5713 emitter: &emitter,
5714 account_id: AccountId::from("POLY-001"),
5715 clock: nautilus_core::time::get_atomic_clock_realtime(),
5716 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
5717 user_api_key: "00000000-0000-0000-0000-000000000001",
5718 };
5719 let mut state = WsDispatchState::default();
5720
5721 dispatch_user_message(&UserWsMessage::Order(order), &ctx, &mut state);
5722 assert!(receiver.try_recv().is_err());
5723
5724 dispatch_user_message(&UserWsMessage::Trade(trade), &ctx, &mut state);
5725
5726 let real_fill = receiver.try_recv().expect("expected confirmed venue fill");
5727 let normalized = receiver
5728 .try_recv()
5729 .expect("expected quantity normalization");
5730
5731 match (real_fill, normalized) {
5732 (
5733 ExecutionEvent::Order(OrderEventAny::Filled(real)),
5734 ExecutionEvent::Order(OrderEventAny::Updated(updated)),
5735 ) => {
5736 assert_eq!(real.last_qty, Quantity::from("99.995"));
5737 assert_eq!(updated.quantity, Quantity::from("99.995"));
5738 assert!(updated.reconciliation);
5739 }
5740 other => panic!("expected fill then quantity update, was {other:?}"),
5741 }
5742 assert!(receiver.try_recv().is_err());
5743 }
5744
5745 #[rstest]
5746 fn test_cancel_reemitted_after_fill_for_canceled_order() {
5747 let cancel_order: PolymarketUserOrder = load("ws_user_order_cancellation.json");
5748 let trade: PolymarketUserTrade = load("ws_user_trade.json");
5749 let instrument = instrument_for_trade(&trade);
5750
5751 let token_instruments = AtomicMap::new();
5752 token_instruments.insert(cancel_order.asset_id, instrument.clone());
5753
5754 let fill_tracker = OrderFillTrackerMap::new();
5755 let venue_order_id = VenueOrderId::from(cancel_order.id.as_str());
5756
5757 fill_tracker.register(
5759 venue_order_id,
5760 Quantity::from("100"),
5761 OrderSide::Buy,
5762 instrument.id(),
5763 instrument.size_precision(),
5764 instrument.price_precision(),
5765 );
5766
5767 let pending_submits = PendingSubmitTracker::default();
5768 let order_contexts = OrderContextRegistry::default();
5769 register_context(&order_contexts, venue_order_id, instrument.id(), "O-CANCEL");
5770 order_contexts.mark_accepted(venue_order_id);
5771 let mut emitter = test_emitter();
5772 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
5773 emitter.set_sender(sender);
5774
5775 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
5776 settlement.mark_live();
5777 settlement.note_order_submitted(venue_order_id);
5778
5779 let ctx = WsDispatchContext {
5780 signer_type: PolymarketSignerType::Owner,
5781 token_instruments: &token_instruments,
5782 fill_tracker: &fill_tracker,
5783 settlement: &settlement,
5784 pending_submits: &pending_submits,
5785 order_contexts: &order_contexts,
5786 emitter: &emitter,
5787 account_id: AccountId::from("POLY-001"),
5788 clock: nautilus_core::time::get_atomic_clock_realtime(),
5789 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
5790 user_api_key: "00000000-0000-0000-0000-000000000001",
5791 };
5792 let mut state = WsDispatchState::default();
5793
5794 dispatch_user_message(&UserWsMessage::Order(cancel_order), &ctx, &mut state);
5796 let cancel_event = receiver.try_recv().expect("Expected canceled event");
5797 match &cancel_event {
5798 ExecutionEvent::Order(OrderEventAny::Canceled(c)) => {
5799 assert_eq!(c.venue_order_id, Some(venue_order_id));
5800 }
5801 other => panic!("Expected canceled event, was {other:?}"),
5802 }
5803
5804 dispatch_user_message(&UserWsMessage::Trade(trade), &ctx, &mut state);
5806
5807 let fill_event = receiver.try_recv().expect("Expected filled event");
5809 match &fill_event {
5810 ExecutionEvent::Order(OrderEventAny::Filled(f)) => {
5811 assert_eq!(f.venue_order_id, venue_order_id);
5812 }
5813 other => panic!("Expected filled event, was {other:?}"),
5814 }
5815
5816 let reemitted_cancel = receiver
5817 .try_recv()
5818 .expect("Expected re-emitted canceled event");
5819
5820 match &reemitted_cancel {
5821 ExecutionEvent::Order(OrderEventAny::Canceled(c)) => {
5822 assert_eq!(c.venue_order_id, Some(venue_order_id));
5823 }
5824 other => panic!("Expected canceled event, was {other:?}"),
5825 }
5826 }
5827
5828 #[rstest]
5829 fn test_gtd_expiry_reemitted_after_late_fill() {
5830 let mut cancel_order: PolymarketUserOrder = load("ws_user_order_cancellation.json");
5831 cancel_order.order_type = Some(PolymarketOrderType::GTD);
5832 cancel_order.expiration = Some("1703875265".to_string());
5833 let trade: PolymarketUserTrade = load("ws_user_trade.json");
5834 let instrument = instrument_for_trade(&trade);
5835 let token_instruments = AtomicMap::new();
5836 token_instruments.insert(cancel_order.asset_id, instrument.clone());
5837 let fill_tracker = OrderFillTrackerMap::new();
5838 let venue_order_id = VenueOrderId::from(cancel_order.id.as_str());
5839 fill_tracker.register(
5840 venue_order_id,
5841 Quantity::from("100"),
5842 OrderSide::Buy,
5843 instrument.id(),
5844 instrument.size_precision(),
5845 instrument.price_precision(),
5846 );
5847 let pending_submits = PendingSubmitTracker::default();
5848 let order_contexts = OrderContextRegistry::default();
5849 register_context(
5850 &order_contexts,
5851 venue_order_id,
5852 instrument.id(),
5853 "O-GTD-EXPIRE",
5854 );
5855 order_contexts.mark_accepted(venue_order_id);
5856 let mut emitter = test_emitter();
5857 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
5858 emitter.set_sender(sender);
5859 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
5860 settlement.mark_live();
5861 settlement.note_order_submitted(venue_order_id);
5862
5863 let ctx = WsDispatchContext {
5864 signer_type: PolymarketSignerType::Owner,
5865 token_instruments: &token_instruments,
5866 fill_tracker: &fill_tracker,
5867 settlement: &settlement,
5868 pending_submits: &pending_submits,
5869 order_contexts: &order_contexts,
5870 emitter: &emitter,
5871 account_id: AccountId::from("POLY-001"),
5872 clock: nautilus_core::time::get_atomic_clock_realtime(),
5873 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
5874 user_api_key: "00000000-0000-0000-0000-000000000001",
5875 };
5876
5877 let mut state = WsDispatchState::default();
5878
5879 dispatch_user_message(&UserWsMessage::Order(cancel_order), &ctx, &mut state);
5880
5881 match receiver.try_recv().expect("expected expired event") {
5882 ExecutionEvent::Order(OrderEventAny::Expired(event)) => {
5883 assert_eq!(event.venue_order_id, Some(venue_order_id));
5884 }
5885 other => panic!("expected OrderExpired, was {other:?}"),
5886 }
5887
5888 dispatch_user_message(&UserWsMessage::Trade(trade), &ctx, &mut state);
5889
5890 match receiver.try_recv().expect("expected fill") {
5891 ExecutionEvent::Order(OrderEventAny::Filled(event)) => {
5892 assert_eq!(event.venue_order_id, venue_order_id);
5893 }
5894 other => panic!("expected fill, was {other:?}"),
5895 }
5896
5897 match receiver.try_recv().expect("expected re-emitted expiry") {
5898 ExecutionEvent::Order(OrderEventAny::Expired(event)) => {
5899 assert_eq!(event.venue_order_id, Some(venue_order_id));
5900 }
5901 other => panic!("expected re-emitted OrderExpired, was {other:?}"),
5902 }
5903 }
5904
5905 #[rstest]
5906 fn test_modified_old_leg_suppresses_gtd_expiry() {
5907 let mut cancel_order: PolymarketUserOrder = load("ws_user_order_cancellation.json");
5908 cancel_order.order_type = Some(PolymarketOrderType::GTD);
5909 cancel_order.expiration = Some("1703875265".to_string());
5910 let trade: PolymarketUserTrade = load("ws_user_trade.json");
5911 let instrument = instrument_for_trade(&trade);
5912 let token_instruments = AtomicMap::new();
5913 token_instruments.insert(cancel_order.asset_id, instrument.clone());
5914 let fill_tracker = OrderFillTrackerMap::new();
5915 let old_venue_order_id = VenueOrderId::from(cancel_order.id.as_str());
5916 fill_tracker.register(
5917 old_venue_order_id,
5918 Quantity::from("100"),
5919 OrderSide::Buy,
5920 instrument.id(),
5921 instrument.size_precision(),
5922 instrument.price_precision(),
5923 );
5924 let client_order_id = ClientOrderId::from("O-MODIFIED-GTD-LEG");
5925 let pending_submits = PendingSubmitTracker::default();
5926 let order_contexts = OrderContextRegistry::default();
5927 register_context(
5928 &order_contexts,
5929 old_venue_order_id,
5930 instrument.id(),
5931 client_order_id.as_str(),
5932 );
5933 order_contexts.mark_accepted(old_venue_order_id);
5934 let mut emitter = test_emitter();
5935 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
5936 emitter.set_sender(sender);
5937 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
5938 settlement.note_order_submitted(old_venue_order_id);
5939
5940 let ctx = WsDispatchContext {
5941 signer_type: PolymarketSignerType::Owner,
5942 token_instruments: &token_instruments,
5943 fill_tracker: &fill_tracker,
5944 settlement: &settlement,
5945 pending_submits: &pending_submits,
5946 order_contexts: &order_contexts,
5947 emitter: &emitter,
5948 account_id: AccountId::from("POLY-001"),
5949 clock: nautilus_core::time::get_atomic_clock_realtime(),
5950 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
5951 user_api_key: "00000000-0000-0000-0000-000000000001",
5952 };
5953
5954 let mut state = WsDispatchState::default();
5955 assert!(state.begin_modify(client_order_id, old_venue_order_id, instrument.id()));
5956 assert!(state.set_modify_replacement(
5957 client_order_id,
5958 VenueOrderId::from("0xreplacement"),
5959 Quantity::from("100"),
5960 Quantity::from("100"),
5961 Quantity::from("0"),
5962 Price::from("0.5"),
5963 ));
5964 assert!(
5965 state
5966 .claim_modify_replacement(VenueOrderId::from("0xreplacement"))
5967 .is_some()
5968 );
5969
5970 dispatch_user_message(&UserWsMessage::Order(cancel_order), &ctx, &mut state);
5971
5972 assert!(receiver.try_recv().is_err());
5973 }
5974
5975 #[rstest]
5976 fn test_modified_old_leg_suppresses_cancel_but_still_emits_late_fill() {
5977 let cancel_order: PolymarketUserOrder = load("ws_user_order_cancellation.json");
5978 let trade: PolymarketUserTrade = load("ws_user_trade.json");
5979 let instrument = instrument_for_trade(&trade);
5980 let token_instruments = AtomicMap::new();
5981 token_instruments.insert(cancel_order.asset_id, instrument.clone());
5982
5983 let fill_tracker = OrderFillTrackerMap::new();
5984 let old_venue_order_id = VenueOrderId::from(cancel_order.id.as_str());
5985 fill_tracker.register(
5986 old_venue_order_id,
5987 Quantity::from("100"),
5988 OrderSide::Buy,
5989 instrument.id(),
5990 instrument.size_precision(),
5991 instrument.price_precision(),
5992 );
5993
5994 let client_order_id = ClientOrderId::from("O-MODIFIED-OLD-LEG");
5995 let pending_submits = PendingSubmitTracker::default();
5996 let order_contexts = OrderContextRegistry::default();
5997 register_context(
5998 &order_contexts,
5999 old_venue_order_id,
6000 instrument.id(),
6001 client_order_id.as_str(),
6002 );
6003 order_contexts.mark_accepted(old_venue_order_id);
6004 let mut emitter = test_emitter();
6005 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
6006 emitter.set_sender(sender);
6007 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
6008 settlement.note_order_submitted(old_venue_order_id);
6009
6010 let ctx = WsDispatchContext {
6011 signer_type: PolymarketSignerType::Owner,
6012 token_instruments: &token_instruments,
6013 fill_tracker: &fill_tracker,
6014 settlement: &settlement,
6015 pending_submits: &pending_submits,
6016 order_contexts: &order_contexts,
6017 emitter: &emitter,
6018 account_id: AccountId::from("POLY-001"),
6019 clock: nautilus_core::time::get_atomic_clock_realtime(),
6020 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
6021 user_api_key: "00000000-0000-0000-0000-000000000001",
6022 };
6023
6024 let mut state = WsDispatchState::default();
6025 let replacement_venue_order_id = VenueOrderId::from("0xreplacement");
6026 assert!(state.begin_modify(client_order_id, old_venue_order_id, instrument.id()));
6027 assert!(state.set_modify_replacement(
6028 client_order_id,
6029 replacement_venue_order_id,
6030 Quantity::from("100"),
6031 Quantity::from("100"),
6032 Quantity::from("0"),
6033 Price::from("0.5"),
6034 ));
6035 assert!(
6036 state
6037 .claim_modify_replacement(replacement_venue_order_id)
6038 .is_some()
6039 );
6040
6041 dispatch_user_message(&UserWsMessage::Order(cancel_order), &ctx, &mut state);
6042 assert!(receiver.try_recv().is_err());
6043
6044 dispatch_user_message(&UserWsMessage::Trade(trade), &ctx, &mut state);
6045
6046 match receiver.try_recv().expect("expected late old-leg fill") {
6047 ExecutionEvent::Order(OrderEventAny::Filled(fill)) => {
6048 assert_eq!(fill.client_order_id, client_order_id);
6049 assert_eq!(fill.venue_order_id, old_venue_order_id);
6050 }
6051 other => panic!("expected late old-leg fill, was {other:?}"),
6052 }
6053
6054 assert!(receiver.try_recv().is_err());
6055 }
6056
6057 #[rstest]
6058 fn test_pending_modify_replacement_ws_activity_emits_updated_without_accepted() {
6059 let mut replacement: PolymarketUserOrder = load("ws_user_order_placement.json");
6060 let instrument = test_instrument();
6061 let old_venue_order_id = VenueOrderId::from("0xold-modify-leg");
6062 let replacement_venue_order_id = VenueOrderId::from("0xreplacement-modify-leg");
6063 replacement.id = replacement_venue_order_id.to_string();
6064
6065 let token_instruments = AtomicMap::new();
6066 token_instruments.insert(replacement.asset_id, instrument.clone());
6067 let fill_tracker = OrderFillTrackerMap::new();
6068 fill_tracker.register(
6069 old_venue_order_id,
6070 Quantity::from("100"),
6071 OrderSide::Buy,
6072 instrument.id(),
6073 instrument.size_precision(),
6074 instrument.price_precision(),
6075 );
6076 let client_order_id = ClientOrderId::from("O-PENDING-MODIFY");
6077 let pending_submits = PendingSubmitTracker::default();
6078 let order_contexts = OrderContextRegistry::default();
6079 register_context(
6080 &order_contexts,
6081 old_venue_order_id,
6082 instrument.id(),
6083 client_order_id.as_str(),
6084 );
6085 order_contexts.mark_accepted(old_venue_order_id);
6086 let mut emitter = test_emitter();
6087 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
6088 emitter.set_sender(sender);
6089 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
6090 settlement.mark_live();
6091
6092 let ctx = WsDispatchContext {
6093 signer_type: PolymarketSignerType::Owner,
6094 token_instruments: &token_instruments,
6095 fill_tracker: &fill_tracker,
6096 settlement: &settlement,
6097 pending_submits: &pending_submits,
6098 order_contexts: &order_contexts,
6099 emitter: &emitter,
6100 account_id: AccountId::from("POLY-001"),
6101 clock: nautilus_core::time::get_atomic_clock_realtime(),
6102 user_address: "0xtest",
6103 user_api_key: "test-key",
6104 };
6105
6106 let mut state = WsDispatchState::default();
6107 assert!(state.begin_modify(client_order_id, old_venue_order_id, instrument.id()));
6108 assert!(state.set_modify_replacement(
6109 client_order_id,
6110 replacement_venue_order_id,
6111 Quantity::from("120"),
6112 Quantity::from("100"),
6113 Quantity::from("20"),
6114 Price::from("0.5"),
6115 ));
6116
6117 let original_context = order_contexts.get(&old_venue_order_id).unwrap();
6118
6119 dispatch_user_message(&UserWsMessage::Order(replacement), &ctx, &mut state);
6120
6121 match receiver.try_recv().expect("expected replacement update") {
6122 ExecutionEvent::Order(OrderEventAny::Updated(updated)) => {
6123 assert_eq!(updated.client_order_id, client_order_id);
6124 assert_eq!(updated.venue_order_id, Some(replacement_venue_order_id));
6125 assert_eq!(updated.quantity, Quantity::from("120"));
6126 assert_eq!(updated.price, Some(Price::from("0.5")));
6127 }
6128 other => panic!("expected replacement update, was {other:?}"),
6129 }
6130
6131 assert_eq!(
6132 order_contexts.venue_order_id(&client_order_id),
6133 Some(replacement_venue_order_id)
6134 );
6135 assert_eq!(
6136 order_contexts.get(&old_venue_order_id),
6137 Some(original_context)
6138 );
6139 assert_eq!(
6140 order_contexts.get(&replacement_venue_order_id),
6141 Some(OrderContext {
6142 quantity: Quantity::from("120"),
6143 price: Some(Price::from("0.5")),
6144 ..original_context
6145 })
6146 );
6147 assert!(receiver.try_recv().is_err());
6148 }
6149
6150 #[rstest]
6151 fn test_modify_replacement_missing_context_consumes_claim_without_publication() {
6152 let instrument_id = InstrumentId::from("TEST.POLYMARKET");
6153 let client_order_id = ClientOrderId::from("O-MISSING-CONTEXT");
6154 let old_id = VenueOrderId::from("V-MISSING-CONTEXT-OLD");
6155 let new_id = VenueOrderId::from("V-MISSING-CONTEXT-NEW");
6156 let mut state = WsDispatchState::default();
6157 assert!(state.begin_modify(client_order_id, old_id, instrument_id));
6158 assert!(state.set_modify_replacement(
6159 client_order_id,
6160 new_id,
6161 Quantity::from("12.34"),
6162 Quantity::from("9.87"),
6163 Quantity::from("2.47"),
6164 Price::from("0.6789"),
6165 ));
6166 let order_contexts = OrderContextRegistry::default();
6167 let fill_tracker = OrderFillTrackerMap::new();
6168 let clock = nautilus_core::time::get_atomic_clock_realtime();
6169 let mut emitter = test_emitter();
6170 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
6171 emitter.set_sender(sender);
6172
6173 let activity = state.promote_modify_replacement(
6174 new_id,
6175 &order_contexts,
6176 &fill_tracker,
6177 &emitter,
6178 clock,
6179 |old_venue_order_id| {
6180 assert_eq!(old_venue_order_id, old_id);
6181 order_contexts
6182 .get(&old_venue_order_id)
6183 .map(|context| (context, emitter.account_id()))
6184 },
6185 || panic!("missing context must stop before resolving event time"),
6186 );
6187
6188 let duplicate = state.promote_modify_replacement(
6189 new_id,
6190 &order_contexts,
6191 &fill_tracker,
6192 &emitter,
6193 clock,
6194 |_| panic!("a consumed claim must stop before resolving context"),
6195 || panic!("a consumed claim must stop before resolving event time"),
6196 );
6197
6198 assert!(activity.is_none());
6199 assert!(duplicate.is_none());
6200 assert!(!state.is_modifying(&client_order_id));
6201 assert!(state.pending_modify_promotion(new_id).is_none());
6202 assert!(state.suppress_modify_cancel(old_id));
6203 assert_eq!(order_contexts.get(&new_id), None);
6204 assert_eq!(order_contexts.venue_order_id(&client_order_id), None);
6205 assert!(!fill_tracker.contains(&new_id));
6206 assert!(receiver.try_recv().is_err());
6207 assert!(order_contexts.mark_accepted(new_id));
6208 }
6209
6210 #[rstest]
6211 fn test_pending_modify_replacement_ws_rejection_emits_once_and_closes_old_leg() {
6212 let mut replacement: PolymarketUserOrder = load("ws_user_order_placement.json");
6213 let instrument = test_instrument();
6214 let old_venue_order_id = VenueOrderId::from("0xold-rejected-modify-leg");
6215 let replacement_venue_order_id = VenueOrderId::from("0xrejected-replacement-modify-leg");
6216 replacement.id = replacement_venue_order_id.to_string();
6217 replacement.status = Some(PolymarketUserOrderStatus::new(
6218 PolymarketOrderStatus::Unmatched,
6219 Some("replacement rejected"),
6220 ));
6221
6222 let token_instruments = AtomicMap::new();
6223 token_instruments.insert(replacement.asset_id, instrument.clone());
6224 let fill_tracker = OrderFillTrackerMap::new();
6225 fill_tracker.register(
6226 old_venue_order_id,
6227 Quantity::from("100"),
6228 OrderSide::Buy,
6229 instrument.id(),
6230 instrument.size_precision(),
6231 instrument.price_precision(),
6232 );
6233 let client_order_id = ClientOrderId::from("O-REJECTED-MODIFY");
6234 let pending_submits = PendingSubmitTracker::default();
6235 let order_contexts = OrderContextRegistry::default();
6236 register_context(
6237 &order_contexts,
6238 old_venue_order_id,
6239 instrument.id(),
6240 client_order_id.as_str(),
6241 );
6242 order_contexts.mark_accepted(old_venue_order_id);
6243 let mut emitter = test_emitter();
6244 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
6245 emitter.set_sender(sender);
6246 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
6247 settlement.mark_live();
6248
6249 let ctx = WsDispatchContext {
6250 signer_type: PolymarketSignerType::Owner,
6251 token_instruments: &token_instruments,
6252 fill_tracker: &fill_tracker,
6253 settlement: &settlement,
6254 pending_submits: &pending_submits,
6255 order_contexts: &order_contexts,
6256 emitter: &emitter,
6257 account_id: AccountId::from("POLY-001"),
6258 clock: nautilus_core::time::get_atomic_clock_realtime(),
6259 user_address: "0xtest",
6260 user_api_key: "test-key",
6261 };
6262
6263 let mut state = WsDispatchState::default();
6264 assert!(state.begin_modify(client_order_id, old_venue_order_id, instrument.id()));
6265 let cancel_ts = UnixNanos::from(123);
6266 assert!(state.confirm_modify_cancel(client_order_id, old_venue_order_id, cancel_ts,));
6267 assert!(state.set_modify_replacement(
6268 client_order_id,
6269 replacement_venue_order_id,
6270 Quantity::from("120"),
6271 Quantity::from("100"),
6272 Quantity::from("20"),
6273 Price::from("0.5"),
6274 ));
6275
6276 dispatch_user_message(&UserWsMessage::Order(replacement), &ctx, &mut state);
6277
6278 match receiver.try_recv().expect("expected modify rejection") {
6279 ExecutionEvent::Order(OrderEventAny::ModifyRejected(rejected)) => {
6280 assert_eq!(rejected.client_order_id, client_order_id);
6281 assert_eq!(rejected.venue_order_id, Some(old_venue_order_id));
6282 assert_eq!(rejected.reason, "replacement rejected");
6283 }
6284 other => panic!("expected modify rejection, was {other:?}"),
6285 }
6286
6287 match receiver.try_recv().expect("expected old-leg cancellation") {
6288 ExecutionEvent::Order(OrderEventAny::Canceled(canceled)) => {
6289 assert_eq!(canceled.client_order_id, client_order_id);
6290 assert_eq!(canceled.venue_order_id, Some(old_venue_order_id));
6291 assert_eq!(canceled.ts_event, cancel_ts);
6292 }
6293 other => panic!("expected old-leg cancellation, was {other:?}"),
6294 }
6295
6296 assert!(
6297 state
6298 .pending_modify_promotion(replacement_venue_order_id)
6299 .is_none()
6300 );
6301 assert!(receiver.try_recv().is_err());
6302 }
6303
6304 #[rstest]
6305 fn test_pending_modify_replacement_fill_promotes_before_fill() {
6306 let trade: PolymarketUserTrade = load("ws_user_trade.json");
6307 let instrument = instrument_for_trade(&trade);
6308 let old_venue_order_id = VenueOrderId::from("0xold-fill-leg");
6309 let replacement_venue_order_id = VenueOrderId::from(trade.taker_order_id.as_str());
6310 let token_instruments = AtomicMap::new();
6311 token_instruments.insert(trade.asset_id, instrument.clone());
6312 let fill_tracker = OrderFillTrackerMap::new();
6313 fill_tracker.register(
6314 old_venue_order_id,
6315 Quantity::from("100"),
6316 OrderSide::Buy,
6317 instrument.id(),
6318 instrument.size_precision(),
6319 instrument.price_precision(),
6320 );
6321 let client_order_id = ClientOrderId::from("O-PENDING-MODIFY-FILL");
6322 let pending_submits = PendingSubmitTracker::default();
6323 let order_contexts = OrderContextRegistry::default();
6324 register_context(
6325 &order_contexts,
6326 old_venue_order_id,
6327 instrument.id(),
6328 client_order_id.as_str(),
6329 );
6330 order_contexts.mark_accepted(old_venue_order_id);
6331 let mut emitter = test_emitter();
6332 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
6333 emitter.set_sender(sender);
6334 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
6335 settlement.note_order_submitted(replacement_venue_order_id);
6336
6337 let ctx = WsDispatchContext {
6338 signer_type: PolymarketSignerType::Owner,
6339 token_instruments: &token_instruments,
6340 fill_tracker: &fill_tracker,
6341 settlement: &settlement,
6342 pending_submits: &pending_submits,
6343 order_contexts: &order_contexts,
6344 emitter: &emitter,
6345 account_id: AccountId::from("POLY-001"),
6346 clock: nautilus_core::time::get_atomic_clock_realtime(),
6347 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
6348 user_api_key: "00000000-0000-0000-0000-000000000001",
6349 };
6350
6351 let mut state = WsDispatchState::default();
6352 assert!(state.begin_modify(client_order_id, old_venue_order_id, instrument.id()));
6353 assert!(state.set_modify_replacement(
6354 client_order_id,
6355 replacement_venue_order_id,
6356 Quantity::from("120"),
6357 Quantity::from("100"),
6358 Quantity::from("20"),
6359 Price::from("0.5"),
6360 ));
6361 let mut cancellation: PolymarketUserOrder = load("ws_user_order_cancellation.json");
6362 cancellation.id = replacement_venue_order_id.to_string();
6363 let cancellation_report = build_ws_order_status_report(
6364 &cancellation,
6365 cancellation.status.as_ref().unwrap(),
6366 cancellation.order_type.unwrap(),
6367 &instrument,
6368 ctx.account_id,
6369 UnixNanos::from(2_000_000_000),
6370 UnixNanos::from(3_000_000_000),
6371 )
6372 .unwrap();
6373 assert!(
6374 fill_tracker
6375 .accept_or_buffer_report(replacement_venue_order_id, cancellation_report)
6376 .is_none()
6377 );
6378
6379 dispatch_user_message(&UserWsMessage::Trade(trade), &ctx, &mut state);
6380
6381 match receiver.try_recv().expect("expected replacement update") {
6382 ExecutionEvent::Order(OrderEventAny::Updated(updated)) => {
6383 assert_eq!(updated.client_order_id, client_order_id);
6384 assert_eq!(updated.venue_order_id, Some(replacement_venue_order_id));
6385 assert_eq!(updated.quantity, Quantity::from("120"));
6386 }
6387 other => panic!("expected replacement update, was {other:?}"),
6388 }
6389
6390 match receiver.try_recv().expect("expected replacement fill") {
6391 ExecutionEvent::Order(OrderEventAny::Filled(fill)) => {
6392 assert_eq!(fill.client_order_id, client_order_id);
6393 assert_eq!(fill.venue_order_id, replacement_venue_order_id);
6394 }
6395 other => panic!("expected replacement fill, was {other:?}"),
6396 }
6397
6398 match receiver.try_recv().expect("expected replacement cancel") {
6399 ExecutionEvent::Order(OrderEventAny::Canceled(cancel)) => {
6400 assert_eq!(cancel.client_order_id, client_order_id);
6401 assert_eq!(cancel.venue_order_id, Some(replacement_venue_order_id));
6402 }
6403 other => panic!("expected replacement cancel, was {other:?}"),
6404 }
6405
6406 assert!(receiver.try_recv().is_err());
6407 }
6408
6409 #[rstest]
6410 fn test_modified_buy_overfill_raises_order_qty_with_prior_legs() {
6411 let mut trade: PolymarketUserTrade = load("ws_user_trade.json");
6412 trade.size = "100.000058".to_string();
6413 trade.maker_orders[0].matched_amount = dec!(100.000058);
6414 let instrument = instrument_for_trade(&trade);
6415 let old_venue_order_id = VenueOrderId::from("0xold-overfill-leg");
6416 let replacement_venue_order_id = VenueOrderId::from(trade.taker_order_id.as_str());
6417 let token_instruments = AtomicMap::new();
6418 token_instruments.insert(trade.asset_id, instrument.clone());
6419 let fill_tracker = OrderFillTrackerMap::new();
6420 fill_tracker.register(
6421 old_venue_order_id,
6422 Quantity::from("120"),
6423 OrderSide::Buy,
6424 instrument.id(),
6425 instrument.size_precision(),
6426 instrument.price_precision(),
6427 );
6428 let client_order_id = ClientOrderId::from("O-MODIFIED-OVERFILL");
6429 let pending_submits = PendingSubmitTracker::default();
6430 let order_contexts = OrderContextRegistry::default();
6431 register_context(
6432 &order_contexts,
6433 old_venue_order_id,
6434 instrument.id(),
6435 client_order_id.as_str(),
6436 );
6437 order_contexts.mark_accepted(old_venue_order_id);
6438 let mut emitter = test_emitter();
6439 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
6440 emitter.set_sender(sender);
6441 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
6442 settlement.note_order_submitted(replacement_venue_order_id);
6443
6444 let ctx = WsDispatchContext {
6445 signer_type: PolymarketSignerType::Owner,
6446 token_instruments: &token_instruments,
6447 fill_tracker: &fill_tracker,
6448 settlement: &settlement,
6449 pending_submits: &pending_submits,
6450 order_contexts: &order_contexts,
6451 emitter: &emitter,
6452 account_id: AccountId::from("POLY-001"),
6453 clock: nautilus_core::time::get_atomic_clock_realtime(),
6454 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
6455 user_api_key: "00000000-0000-0000-0000-000000000001",
6456 };
6457
6458 let mut state = WsDispatchState::default();
6460 assert!(state.begin_modify(client_order_id, old_venue_order_id, instrument.id()));
6461 assert!(state.set_modify_replacement(
6462 client_order_id,
6463 replacement_venue_order_id,
6464 Quantity::from("120"),
6465 Quantity::from("100"),
6466 Quantity::from("20"),
6467 Price::from("0.5"),
6468 ));
6469
6470 dispatch_user_message(&UserWsMessage::Trade(trade), &ctx, &mut state);
6471
6472 let promoted = receiver.try_recv().expect("expected replacement update");
6473 let bumped = receiver.try_recv().expect("expected overfill update");
6474 let filled = receiver.try_recv().expect("expected replacement fill");
6475
6476 match promoted {
6477 ExecutionEvent::Order(OrderEventAny::Updated(updated)) => {
6478 assert_eq!(updated.quantity, Quantity::from("120"));
6479 }
6480 other => panic!("expected replacement update, was {other:?}"),
6481 }
6482
6483 match bumped {
6484 ExecutionEvent::Order(OrderEventAny::Updated(updated)) => {
6485 assert_eq!(updated.client_order_id, client_order_id);
6486 assert_eq!(updated.venue_order_id, Some(replacement_venue_order_id));
6487 assert_eq!(updated.quantity.as_decimal(), dec!(120.000058));
6488 }
6489 other => panic!("expected overfill update, was {other:?}"),
6490 }
6491
6492 match filled {
6493 ExecutionEvent::Order(OrderEventAny::Filled(fill)) => {
6494 assert_eq!(fill.venue_order_id, replacement_venue_order_id);
6495 assert_eq!(fill.last_qty.as_decimal(), dec!(100.000058));
6496 }
6497 other => panic!("expected replacement fill, was {other:?}"),
6498 }
6499
6500 assert!(receiver.try_recv().is_err());
6501 }
6502
6503 #[rstest]
6504 fn test_late_modify_completion_does_not_finish_newer_modify() {
6505 let instrument_id = InstrumentId::from("TEST.POLYMARKET");
6506 let client_order_id = ClientOrderId::from("O-MODIFY-GENERATION");
6507 let old_venue_order_id = VenueOrderId::from("0xmodify-generation-old");
6508 let first_replacement_venue_order_id = VenueOrderId::from("0xmodify-generation-first");
6509 let second_replacement_venue_order_id = VenueOrderId::from("0xmodify-generation-second");
6510 let mut state = WsDispatchState::default();
6511
6512 assert!(state.begin_modify(client_order_id, old_venue_order_id, instrument_id));
6513 assert!(state.set_modify_replacement(
6514 client_order_id,
6515 first_replacement_venue_order_id,
6516 Quantity::from("12"),
6517 Quantity::from("12"),
6518 Quantity::from("0"),
6519 Price::from("0.5"),
6520 ));
6521 assert!(
6522 state
6523 .claim_modify_replacement(first_replacement_venue_order_id)
6524 .is_some()
6525 );
6526 assert!(state.begin_modify(
6527 client_order_id,
6528 first_replacement_venue_order_id,
6529 instrument_id,
6530 ));
6531 assert!(state.set_modify_replacement(
6532 client_order_id,
6533 second_replacement_venue_order_id,
6534 Quantity::from("15"),
6535 Quantity::from("15"),
6536 Quantity::from("0"),
6537 Price::from("0.6"),
6538 ));
6539
6540 assert!(
6541 state
6542 .finish_modify_without_replacement(
6543 client_order_id,
6544 old_venue_order_id,
6545 true,
6546 UnixNanos::from(123),
6547 )
6548 .is_none()
6549 );
6550 let promotion = state
6551 .pending_modify_promotion(second_replacement_venue_order_id)
6552 .expect("newer modification must remain pending");
6553 assert_eq!(promotion.client_order_id, client_order_id);
6554 assert_eq!(
6555 promotion.old_venue_order_id,
6556 first_replacement_venue_order_id
6557 );
6558 assert_eq!(promotion.venue_order_id, second_replacement_venue_order_id);
6559 assert_eq!(promotion.quantity, Quantity::from("15"));
6560 assert_eq!(promotion.leg_quantity, Quantity::from("15"));
6561 assert_eq!(promotion.price, Price::from("0.6"));
6562 }
6563
6564 #[rstest]
6565 fn test_pending_modify_lookup_selects_matching_replacement() {
6566 let instrument_id = InstrumentId::from("TEST.POLYMARKET");
6567 let first_client_order_id = ClientOrderId::from("O-MODIFY-LOOKUP-1");
6568 let second_client_order_id = ClientOrderId::from("O-MODIFY-LOOKUP-2");
6569 let first_old_venue_order_id = VenueOrderId::from("0xmodify-lookup-old-1");
6570 let second_old_venue_order_id = VenueOrderId::from("0xmodify-lookup-old-2");
6571 let first_replacement_venue_order_id = VenueOrderId::from("0xmodify-lookup-new-1");
6572 let second_replacement_venue_order_id = VenueOrderId::from("0xmodify-lookup-new-2");
6573 let mut state = WsDispatchState::default();
6574
6575 assert!(state.begin_modify(
6576 first_client_order_id,
6577 first_old_venue_order_id,
6578 instrument_id,
6579 ));
6580 assert!(state.set_modify_replacement(
6581 first_client_order_id,
6582 first_replacement_venue_order_id,
6583 Quantity::from("11"),
6584 Quantity::from("10"),
6585 Quantity::from("1"),
6586 Price::from("0.4"),
6587 ));
6588 assert!(state.begin_modify(
6589 second_client_order_id,
6590 second_old_venue_order_id,
6591 instrument_id,
6592 ));
6593 assert!(state.set_modify_replacement(
6594 second_client_order_id,
6595 second_replacement_venue_order_id,
6596 Quantity::from("22"),
6597 Quantity::from("20"),
6598 Quantity::from("2"),
6599 Price::from("0.6"),
6600 ));
6601
6602 let second = state
6603 .claim_modify_replacement(second_replacement_venue_order_id)
6604 .expect("second replacement must be selected");
6605 assert_eq!(second.client_order_id, second_client_order_id);
6606 assert_eq!(second.old_venue_order_id, second_old_venue_order_id);
6607 assert_eq!(second.venue_order_id, second_replacement_venue_order_id);
6608 assert_eq!(second.quantity, Quantity::from("22"));
6609 assert_eq!(second.leg_quantity, Quantity::from("20"));
6610 assert_eq!(second.price, Price::from("0.6"));
6611
6612 let first = state
6613 .pending_modify_promotion(first_replacement_venue_order_id)
6614 .expect("first replacement must remain pending");
6615 assert_eq!(first.client_order_id, first_client_order_id);
6616 assert_eq!(first.old_venue_order_id, first_old_venue_order_id);
6617 assert_eq!(first.venue_order_id, first_replacement_venue_order_id);
6618 assert_eq!(first.quantity, Quantity::from("11"));
6619 assert_eq!(first.leg_quantity, Quantity::from("10"));
6620 assert_eq!(first.price, Price::from("0.4"));
6621 }
6622
6623 #[rstest]
6624 fn test_begin_cancels_is_atomic_when_one_order_conflicts() {
6625 let instrument_id = InstrumentId::from("TEST.POLYMARKET");
6626 let available_client_order_id = ClientOrderId::from("O-CANCEL-AVAILABLE");
6627 let conflicting_client_order_id = ClientOrderId::from("O-CANCEL-CONFLICT");
6628 let mut state = WsDispatchState::default();
6629
6630 assert!(state.begin_modify(
6631 conflicting_client_order_id,
6632 VenueOrderId::from("0xmodify-conflict"),
6633 instrument_id,
6634 ));
6635 assert!(!state.begin_cancels(&[
6636 (available_client_order_id, instrument_id),
6637 (conflicting_client_order_id, instrument_id),
6638 ]));
6639 assert!(state.begin_modify(
6640 available_client_order_id,
6641 VenueOrderId::from("0xmodify-available"),
6642 instrument_id,
6643 ));
6644 }
6645
6646 #[rstest]
6647 fn test_begin_available_cancels_skips_existing_cancel() {
6648 let instrument_id = InstrumentId::from("TEST.POLYMARKET");
6649 let pending_client_order_id = ClientOrderId::from("O-CANCEL-PENDING");
6650 let available_client_order_id = ClientOrderId::from("O-CANCEL-AVAILABLE");
6651 let modifying_client_order_id = ClientOrderId::from("O-MODIFY-PENDING");
6652 let unreserved_client_order_id = ClientOrderId::from("O-CANCEL-UNRESERVED");
6653 let mut state = WsDispatchState::default();
6654
6655 assert!(state.begin_cancels(&[(pending_client_order_id, instrument_id)]));
6656 assert_eq!(
6657 state
6658 .begin_available_cancels(&[
6659 (pending_client_order_id, instrument_id),
6660 (available_client_order_id, instrument_id),
6661 ])
6662 .unwrap(),
6663 vec![available_client_order_id],
6664 );
6665 assert!(!state.begin_modify(
6666 pending_client_order_id,
6667 VenueOrderId::from("0xcancel-pending"),
6668 instrument_id,
6669 ));
6670 assert!(!state.begin_modify(
6671 available_client_order_id,
6672 VenueOrderId::from("0xcancel-available"),
6673 instrument_id,
6674 ));
6675
6676 state.finish_cancels(&[pending_client_order_id, available_client_order_id]);
6677 assert!(state.begin_modify(
6678 modifying_client_order_id,
6679 VenueOrderId::from("0xmodify-pending"),
6680 instrument_id,
6681 ));
6682 assert!(
6683 state
6684 .begin_available_cancels(&[
6685 (unreserved_client_order_id, instrument_id),
6686 (modifying_client_order_id, instrument_id),
6687 ])
6688 .is_none()
6689 );
6690 assert!(state.begin_modify(
6691 unreserved_client_order_id,
6692 VenueOrderId::from("0xcancel-unreserved"),
6693 instrument_id,
6694 ));
6695 }
6696
6697 #[rstest]
6698 fn test_cancel_and_modify_are_mutually_exclusive() {
6699 let instrument_id = InstrumentId::from("TEST.POLYMARKET");
6700 let client_order_id = ClientOrderId::from("O-MODIFY-CANCEL-ALL");
6701 let other_client_order_id = ClientOrderId::from("O-MODIFY-MARKET-CANCEL");
6702 let venue_order_id = VenueOrderId::from("0xmodify-cancel-all");
6703 let mut state = WsDispatchState::default();
6704
6705 assert!(state.begin_cancels(&[(client_order_id, instrument_id)]));
6706 assert!(!state.begin_modify(client_order_id, venue_order_id, instrument_id));
6707 assert!(state.begin_market_cancel(instrument_id));
6708 assert!(!state.begin_modify(
6709 other_client_order_id,
6710 VenueOrderId::from("0xmodify-market-cancel"),
6711 instrument_id,
6712 ));
6713 state.finish_market_cancel(instrument_id);
6714 state.finish_cancels(&[client_order_id]);
6715 assert!(state.begin_modify(client_order_id, venue_order_id, instrument_id));
6716 assert!(!state.begin_cancels(&[(client_order_id, instrument_id)]));
6717 assert!(!state.begin_market_cancel(instrument_id));
6718 assert!(state.set_modify_replacement(
6719 client_order_id,
6720 VenueOrderId::from("0xmodify-cancel-all-new"),
6721 Quantity::from("12"),
6722 Quantity::from("12"),
6723 Quantity::from("0"),
6724 Price::from("0.5"),
6725 ));
6726 assert!(!state.begin_cancels(&[(client_order_id, instrument_id)]));
6727 assert!(!state.begin_market_cancel(instrument_id));
6728 assert!(
6729 state
6730 .finish_modify_without_replacement(
6731 client_order_id,
6732 venue_order_id,
6733 false,
6734 UnixNanos::default(),
6735 )
6736 .is_some()
6737 );
6738 assert!(state.begin_market_cancel(instrument_id));
6739 assert!(!state.begin_modify(client_order_id, venue_order_id, instrument_id));
6740 state.finish_market_cancel(instrument_id);
6741 }
6742
6743 #[rstest]
6744 fn test_reset_preserves_modify_recovery_and_stale_leg_safety() {
6745 let instrument_id = InstrumentId::from("TEST.POLYMARKET");
6746 let pending_client_order_id = ClientOrderId::from("O-PENDING-RESET");
6747 let pending_old_venue_order_id = VenueOrderId::from("0xpending-old-reset");
6748 let pending_new_venue_order_id = VenueOrderId::from("0xpending-new-reset");
6749 let replaced_client_order_id = ClientOrderId::from("O-REPLACED-RESET");
6750 let replaced_old_venue_order_id = VenueOrderId::from("0xreplaced-old-reset");
6751 let replaced_new_venue_order_id = VenueOrderId::from("0xreplaced-new-reset");
6752 let closed_client_order_id = ClientOrderId::from("O-CLOSED-RESET");
6753 let closed_venue_order_id = VenueOrderId::from("0xclosed-reset");
6754 let cancel_client_order_id = ClientOrderId::from("O-CANCEL-RESET");
6755 let trade_id = TradeId::from("T-RESET");
6756 let mut state = WsDispatchState::default();
6757
6758 assert!(state.begin_modify(
6759 pending_client_order_id,
6760 pending_old_venue_order_id,
6761 instrument_id,
6762 ));
6763 assert!(state.set_modify_replacement(
6764 pending_client_order_id,
6765 pending_new_venue_order_id,
6766 Quantity::from("12"),
6767 Quantity::from("10"),
6768 Quantity::from("2"),
6769 Price::from("0.5"),
6770 ));
6771 assert!(state.begin_modify(
6772 replaced_client_order_id,
6773 replaced_old_venue_order_id,
6774 instrument_id,
6775 ));
6776 assert!(state.set_modify_replacement(
6777 replaced_client_order_id,
6778 replaced_new_venue_order_id,
6779 Quantity::from("12"),
6780 Quantity::from("10"),
6781 Quantity::from("2"),
6782 Price::from("0.5"),
6783 ));
6784 assert!(
6785 state
6786 .claim_modify_replacement(replaced_new_venue_order_id)
6787 .is_some()
6788 );
6789 assert!(state.begin_modify(closed_client_order_id, closed_venue_order_id, instrument_id,));
6790 assert!(
6791 state
6792 .finish_modify_without_replacement(
6793 closed_client_order_id,
6794 closed_venue_order_id,
6795 true,
6796 UnixNanos::from(1),
6797 )
6798 .is_some()
6799 );
6800 state.record_reconciled_fill(trade_id, pending_old_venue_order_id);
6801 assert!(state.begin_cancels(&[(cancel_client_order_id, instrument_id)]));
6802
6803 state.reset_session();
6804
6805 assert_eq!(
6806 state
6807 .pending_modify_promotion(pending_new_venue_order_id)
6808 .unwrap()
6809 .client_order_id,
6810 pending_client_order_id,
6811 );
6812 assert!(state.suppress_modify_cancel(replaced_old_venue_order_id));
6813 assert!(state.suppress_modify_cancel(closed_venue_order_id));
6814 assert!(state.suppress_modify_cancel_reemit(pending_old_venue_order_id));
6815 assert!(state.suppress_modify_cancel_reemit(replaced_old_venue_order_id));
6816 assert!(!state.suppress_modify_cancel_reemit(closed_venue_order_id));
6817 assert!(
6818 state
6819 .reconciled_fills
6820 .contains(&(trade_id, pending_old_venue_order_id))
6821 );
6822 assert!(state.begin_cancels(&[(cancel_client_order_id, instrument_id)]));
6823 }
6824
6825 #[rstest]
6826 fn test_reconciled_fill_is_not_reapplied_from_websocket() {
6827 let trade: PolymarketUserTrade = load("ws_user_trade.json");
6828 let instrument = instrument_for_trade(&trade);
6829 let venue_order_id = VenueOrderId::from(trade.taker_order_id.as_str());
6830 let token_instruments = AtomicMap::new();
6831 token_instruments.insert(trade.asset_id, instrument.clone());
6832 let fill_tracker = OrderFillTrackerMap::new();
6833 fill_tracker.restore_order(
6834 venue_order_id,
6835 Quantity::from("100"),
6836 Quantity::from("0"),
6837 Quantity::from("0"),
6838 Quantity::from("25"),
6839 OrderSide::Buy,
6840 );
6841 let pending_submits = PendingSubmitTracker::default();
6842 let order_contexts = OrderContextRegistry::default();
6843 register_context(
6844 &order_contexts,
6845 venue_order_id,
6846 instrument.id(),
6847 "O-RECONCILED-FILL",
6848 );
6849 let mut emitter = test_emitter();
6850 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
6851 emitter.set_sender(sender);
6852 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
6853 settlement.mark_live();
6854 settlement.note_order_submitted(venue_order_id);
6855
6856 let ctx = WsDispatchContext {
6857 signer_type: PolymarketSignerType::Owner,
6858 token_instruments: &token_instruments,
6859 fill_tracker: &fill_tracker,
6860 settlement: &settlement,
6861 pending_submits: &pending_submits,
6862 order_contexts: &order_contexts,
6863 emitter: &emitter,
6864 account_id: AccountId::from("POLY-001"),
6865 clock: nautilus_core::time::get_atomic_clock_realtime(),
6866 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
6867 user_api_key: "00000000-0000-0000-0000-000000000001",
6868 };
6869
6870 let mut state = WsDispatchState::default();
6871 state.record_reconciled_fill(TradeId::from(trade.id.as_str()), venue_order_id);
6872
6873 dispatch_user_message(&UserWsMessage::Trade(trade), &ctx, &mut state);
6874
6875 assert_eq!(
6876 fill_tracker.get_cumulative_filled(&venue_order_id),
6877 Some(Quantity::from("25")),
6878 );
6879 assert!(receiver.try_recv().is_err());
6880 assert!(settlement.ensure_resolved(None, "mass status").is_ok());
6882 }
6883
6884 #[rstest]
6885 fn test_cancel_not_reemitted_when_fill_completes_order() {
6886 let cancel_order: PolymarketUserOrder = load("ws_user_order_cancellation.json");
6887 let trade: PolymarketUserTrade = load("ws_user_trade.json");
6888 let instrument = instrument_for_trade(&trade);
6889
6890 let token_instruments = AtomicMap::new();
6891 token_instruments.insert(cancel_order.asset_id, instrument.clone());
6892
6893 let fill_tracker = OrderFillTrackerMap::new();
6894 let venue_order_id = VenueOrderId::from(cancel_order.id.as_str());
6895
6896 fill_tracker.register(
6898 venue_order_id,
6899 Quantity::from("25"),
6900 OrderSide::Buy,
6901 instrument.id(),
6902 instrument.size_precision(),
6903 instrument.price_precision(),
6904 );
6905
6906 let pending_submits = PendingSubmitTracker::default();
6907 let order_contexts = OrderContextRegistry::default();
6908 register_context(
6909 &order_contexts,
6910 venue_order_id,
6911 instrument.id(),
6912 "O-CANCEL-FULL",
6913 );
6914 order_contexts.mark_accepted(venue_order_id);
6915 let mut emitter = test_emitter();
6916 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
6917 emitter.set_sender(sender);
6918
6919 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
6920 settlement.mark_live();
6921 settlement.note_order_submitted(venue_order_id);
6922
6923 let ctx = WsDispatchContext {
6924 signer_type: PolymarketSignerType::Owner,
6925 token_instruments: &token_instruments,
6926 fill_tracker: &fill_tracker,
6927 settlement: &settlement,
6928 pending_submits: &pending_submits,
6929 order_contexts: &order_contexts,
6930 emitter: &emitter,
6931 account_id: AccountId::from("POLY-001"),
6932 clock: nautilus_core::time::get_atomic_clock_realtime(),
6933 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
6934 user_api_key: "00000000-0000-0000-0000-000000000001",
6935 };
6936 let mut state = WsDispatchState::default();
6937
6938 dispatch_user_message(&UserWsMessage::Order(cancel_order), &ctx, &mut state);
6940 let _cancel = receiver.try_recv().expect("Expected canceled event");
6941
6942 dispatch_user_message(&UserWsMessage::Trade(trade), &ctx, &mut state);
6943 let _fill = receiver.try_recv().expect("Expected filled event");
6944
6945 assert!(
6947 receiver.try_recv().is_err(),
6948 "Should not re-emit cancel when fill completes the order"
6949 );
6950 }
6951
6952 #[rstest]
6953 fn test_cancel_saved_before_acceptance_and_retained_for_modify_reset() {
6954 let cancel_order: PolymarketUserOrder = load("ws_user_order_cancellation.json");
6955 let instrument = test_instrument();
6956 let instrument_id = instrument.id();
6957
6958 let token_instruments = AtomicMap::new();
6959 token_instruments.insert(cancel_order.asset_id, instrument);
6960
6961 let fill_tracker = OrderFillTrackerMap::new();
6963 let venue_order_id = VenueOrderId::from(cancel_order.id.as_str());
6964
6965 let pending_submits = PendingSubmitTracker::default();
6966 let order_contexts = OrderContextRegistry::default();
6967 let emitter = test_emitter();
6968
6969 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
6970 settlement.mark_live();
6971 settlement.note_order_submitted(venue_order_id);
6972
6973 let ctx = WsDispatchContext {
6974 signer_type: PolymarketSignerType::Owner,
6975 token_instruments: &token_instruments,
6976 fill_tracker: &fill_tracker,
6977 settlement: &settlement,
6978 pending_submits: &pending_submits,
6979 order_contexts: &order_contexts,
6980 emitter: &emitter,
6981 account_id: AccountId::from("POLY-001"),
6982 clock: nautilus_core::time::get_atomic_clock_realtime(),
6983 user_address: "0xtest",
6984 user_api_key: "test-key",
6985 };
6986 let mut state = WsDispatchState::default();
6987
6988 dispatch_user_message(&UserWsMessage::Order(cancel_order), &ctx, &mut state);
6990
6991 assert!(fill_tracker.has_pending_report(&venue_order_id));
6993 assert!(state.terminal_cancel_reports.get(&venue_order_id).is_some());
6994
6995 let client_order_id = ClientOrderId::from("O-CANCEL-RESET");
6996 assert!(state.begin_modify(client_order_id, venue_order_id, instrument_id));
6997 state.reset_session();
6998 assert!(
6999 state
7000 .finish_modify_without_replacement(
7001 client_order_id,
7002 venue_order_id,
7003 false,
7004 UnixNanos::default(),
7005 )
7006 .unwrap()
7007 .1
7008 .is_some()
7009 );
7010 }
7011
7012 #[rstest]
7015 #[case(PolymarketOrderStatus::Canceled, "Canceled", OrderStatus::Canceled)]
7016 #[case(
7017 PolymarketOrderStatus::CanceledMarketResolved,
7018 "Expired",
7019 OrderStatus::Expired
7020 )]
7021 fn test_buffered_fill_emitted_before_terminal_status(
7022 #[case] status: PolymarketOrderStatus,
7023 #[case] expected_terminal: &str,
7024 #[case] expected_order_status: OrderStatus,
7025 #[values(false, true)] context_before_update: bool,
7026 ) {
7027 let mut terminal_order: PolymarketUserOrder = load("ws_user_order_cancellation.json");
7028 terminal_order.status = Some(status.into());
7029 let trade: PolymarketUserTrade = load("ws_user_trade.json");
7030 let instrument = instrument_for_trade(&trade);
7031
7032 let token_instruments = AtomicMap::new();
7033 token_instruments.insert(terminal_order.asset_id, instrument.clone());
7034
7035 let fill_tracker = OrderFillTrackerMap::new();
7037 let venue_order_id = VenueOrderId::from(terminal_order.id.as_str());
7038 let client_order_id = ClientOrderId::from("O-BUFFERED");
7039
7040 let pending_submits = PendingSubmitTracker::default();
7041 pending_submits.insert(venue_order_id, client_order_id);
7042 let order_contexts = OrderContextRegistry::default();
7043
7044 if context_before_update {
7045 register_context(
7046 &order_contexts,
7047 venue_order_id,
7048 instrument.id(),
7049 client_order_id.as_str(),
7050 );
7051 }
7052
7053 let mut emitter = test_emitter();
7054 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
7055 emitter.set_sender(sender);
7056
7057 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
7058 settlement.mark_live();
7059 settlement.note_order_submitted(venue_order_id);
7060
7061 let ctx = WsDispatchContext {
7062 signer_type: PolymarketSignerType::Owner,
7063 token_instruments: &token_instruments,
7064 fill_tracker: &fill_tracker,
7065 settlement: &settlement,
7066 pending_submits: &pending_submits,
7067 order_contexts: &order_contexts,
7068 emitter: &emitter,
7069 account_id: AccountId::from("POLY-001"),
7070 clock: nautilus_core::time::get_atomic_clock_realtime(),
7071 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
7072 user_api_key: "00000000-0000-0000-0000-000000000001",
7073 };
7074 let mut state = WsDispatchState::default();
7075
7076 dispatch_user_message(&UserWsMessage::Trade(trade), &ctx, &mut state);
7078 assert!(
7079 receiver.try_recv().is_err(),
7080 "a buffered fill must emit no event before the order is registered",
7081 );
7082
7083 if !context_before_update {
7084 dispatch_user_message(
7085 &UserWsMessage::Order(terminal_order.clone()),
7086 &ctx,
7087 &mut state,
7088 );
7089 assert!(receiver.try_recv().is_err());
7090 assert!(!fill_tracker.contains(&venue_order_id));
7091 register_context(
7092 &order_contexts,
7093 venue_order_id,
7094 instrument.id(),
7095 client_order_id.as_str(),
7096 );
7097 }
7098
7099 dispatch_user_message(&UserWsMessage::Order(terminal_order), &ctx, &mut state);
7101
7102 let mut emitted = Vec::new();
7103
7104 while let Ok(event) = receiver.try_recv() {
7105 match event {
7106 ExecutionEvent::Order(order_event) => emitted.push(order_event),
7107 other => panic!("expected only order events, was {other:?}"),
7108 }
7109 }
7110
7111 assert_eq!(emitted.len(), 3, "emitted sequence was {emitted:?}");
7112 match &emitted[0] {
7113 OrderEventAny::Accepted(accepted) => {
7114 assert_eq!(accepted.client_order_id, client_order_id);
7115 assert_eq!(accepted.venue_order_id, venue_order_id);
7116 }
7117 other => panic!("expected accepted event first, was {other:?}"),
7118 }
7119
7120 match &emitted[1] {
7121 OrderEventAny::Filled(filled) => {
7122 assert_eq!(filled.client_order_id, client_order_id);
7123 assert_eq!(filled.venue_order_id, venue_order_id);
7124 assert_eq!(filled.last_qty.as_decimal(), dec!(25));
7125 }
7126 other => panic!("expected filled event before the terminal status, was {other:?}"),
7127 }
7128
7129 let terminal = match &emitted[2] {
7130 OrderEventAny::Canceled(canceled) => {
7131 assert_eq!(canceled.client_order_id, client_order_id);
7132 assert_eq!(canceled.venue_order_id, Some(venue_order_id));
7133 "Canceled"
7134 }
7135 OrderEventAny::Expired(expired) => {
7136 assert_eq!(expired.client_order_id, client_order_id);
7137 assert_eq!(expired.venue_order_id, Some(venue_order_id));
7138 "Expired"
7139 }
7140 other => panic!("expected a terminal order event last, was {other:?}"),
7141 };
7142 assert_eq!(terminal, expected_terminal);
7143
7144 let mut order = OrderTestBuilder::new(OrderType::Limit)
7146 .instrument_id(instrument.id())
7147 .client_order_id(client_order_id)
7148 .strategy_id(StrategyId::from("S-001"))
7149 .side(OrderSide::Buy)
7150 .price(Price::from("0.5"))
7151 .quantity(Quantity::from("100"))
7152 .build();
7153
7154 for event in emitted {
7155 order.apply(event).expect("emitted sequence must be valid");
7156 }
7157
7158 assert_eq!(order.status(), expected_order_status);
7159 assert_eq!(order.filled_qty().as_decimal(), dec!(25));
7160 }
7161
7162 #[rstest]
7174 fn test_issue_3797_interleaved_cancel_fill_sequence() {
7175 use crate::common::{
7176 enums::{
7177 PolymarketEventType, PolymarketLiquiditySide, PolymarketOrderSide,
7178 PolymarketOrderStatus, PolymarketOrderType, PolymarketOutcome,
7179 PolymarketTradeStatus,
7180 },
7181 models::PolymarketMakerOrder,
7182 };
7183
7184 let instrument = bind_instrument(test_instrument(), "0x4134", PolymarketOutcome::yes());
7185 let asset_id = instrument.id().symbol.inner();
7186
7187 let order_id =
7188 "0xe743f6c823ecdfa9ddaaf08673b2441d15a38d89e14dcb25b3b70c284be4f6ad".to_string();
7189 let venue_order_id = VenueOrderId::from(order_id.as_str());
7190
7191 let token_instruments = AtomicMap::new();
7192 token_instruments.insert(asset_id, instrument.clone());
7193
7194 let fill_tracker = OrderFillTrackerMap::new();
7195 fill_tracker.register(
7196 venue_order_id,
7197 Quantity::from("20"),
7198 OrderSide::Buy,
7199 instrument.id(),
7200 instrument.size_precision(),
7201 instrument.price_precision(),
7202 );
7203
7204 let pending_submits = PendingSubmitTracker::default();
7205 let order_contexts = OrderContextRegistry::default();
7206 register_context(&order_contexts, venue_order_id, instrument.id(), "O-3797");
7207 order_contexts.mark_accepted(venue_order_id);
7208 let mut emitter = test_emitter();
7209 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
7210 emitter.set_sender(sender);
7211
7212 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
7213 settlement.mark_live();
7214 settlement.note_order_submitted(venue_order_id);
7215
7216 let ctx = WsDispatchContext {
7217 signer_type: PolymarketSignerType::Owner,
7218 token_instruments: &token_instruments,
7219 fill_tracker: &fill_tracker,
7220 settlement: &settlement,
7221 pending_submits: &pending_submits,
7222 order_contexts: &order_contexts,
7223 emitter: &emitter,
7224 account_id: AccountId::from("POLY-001"),
7225 clock: nautilus_core::time::get_atomic_clock_realtime(),
7226 user_address: "0xabc",
7227 user_api_key: "xxx",
7228 };
7229 let mut state = WsDispatchState::default();
7230
7231 let make_order =
7232 |size_matched: &str, ts: &str, event_type: PolymarketEventType| PolymarketUserOrder {
7233 asset_id,
7234 associate_trades: None,
7235 created_at: Some("1775074735".to_string()),
7236 expiration: Some("0".to_string()),
7237 id: order_id.clone(),
7238 maker_address: Some(Ustr::from("0xabc")),
7239 market: Ustr::from("0x4134"),
7240 order_owner: Some(Ustr::from("xxx")),
7241 order_type: Some(PolymarketOrderType::GTC),
7242 original_size: "20".to_string(),
7243 outcome: Some(PolymarketOutcome::yes()),
7244 owner: Ustr::from("xxx"),
7245 price: "0.18".to_string(),
7246 side: PolymarketOrderSide::Buy,
7247 size_matched: size_matched.to_string(),
7248 status: Some(PolymarketOrderStatus::Canceled.into()),
7249 timestamp: ts.to_string(),
7250 event_type,
7251 };
7252
7253 let make_trade = |trade_id: &str, matched_amount: f64, ts: &str| PolymarketUserTrade {
7254 asset_id,
7255 bucket_index: 0,
7256 fee_rate_bps: "1000".to_string(),
7257 id: trade_id.to_string(),
7258 last_update: "1775074738".to_string(),
7259 maker_address: Ustr::from("0xother"),
7260 maker_orders: vec![PolymarketMakerOrder {
7261 asset_id,
7262 maker_address: "0xabc".to_string(),
7263 matched_amount: Decimal::from_str_exact(&matched_amount.to_string())
7264 .unwrap_or(Decimal::ZERO),
7265 order_id: order_id.clone(),
7266 outcome: PolymarketOutcome::yes(),
7267 owner: "xxx".to_string(),
7268 price: Decimal::from_str_exact("0.18").unwrap_or(Decimal::ZERO),
7269 side: None,
7270 }],
7271 market: Ustr::from("0x4134"),
7272 match_time: "1775074735".to_string(),
7273 outcome: PolymarketOutcome::yes(),
7274 owner: Ustr::from("other-owner"),
7275 price: "0.82".to_string(),
7276 side: PolymarketOrderSide::Buy,
7277 size: "1.219511".to_string(),
7278 status: PolymarketTradeStatus::Confirmed,
7279 taker_order_id: "0xtaker01".to_string(),
7280 timestamp: ts.to_string(),
7281 trade_owner: Ustr::from("other-owner"),
7282 transaction_hash: None,
7283 trader_side: PolymarketLiquiditySide::Maker,
7284 event_type: PolymarketEventType::Trade,
7285 };
7286
7287 let msg_a = make_order("0", "1775074738031", PolymarketEventType::Cancellation);
7289 dispatch_user_message(&UserWsMessage::Order(msg_a), &ctx, &mut state);
7290
7291 let evt = receiver.try_recv().expect("(A) canceled event");
7292 match &evt {
7293 ExecutionEvent::Order(OrderEventAny::Canceled(c)) => {
7294 assert_eq!(c.venue_order_id, Some(venue_order_id));
7295 }
7296 other => panic!("(A) expected canceled event, was {other:?}"),
7297 }
7298
7299 let msg_b = make_trade("trade-b", 1.219511, "1775074738032");
7301 dispatch_user_message(&UserWsMessage::Trade(msg_b), &ctx, &mut state);
7302
7303 let evt = receiver.try_recv().expect("(B) filled event");
7304 match &evt {
7305 ExecutionEvent::Order(OrderEventAny::Filled(f)) => {
7306 assert_eq!(f.venue_order_id, venue_order_id);
7307 }
7308 other => panic!("(B) expected filled event, was {other:?}"),
7309 }
7310 let evt = receiver.try_recv().expect("(B) re-emitted cancel");
7312 match &evt {
7313 ExecutionEvent::Order(OrderEventAny::Canceled(c)) => {
7314 assert_eq!(c.venue_order_id, Some(venue_order_id));
7315 }
7316 other => panic!("(B) expected re-emitted cancel, was {other:?}"),
7317 }
7318
7319 let msg_c = make_order("1.219511", "1775074738034", PolymarketEventType::Update);
7321 dispatch_user_message(&UserWsMessage::Order(msg_c), &ctx, &mut state);
7322
7323 let evt = receiver.try_recv().expect("(C) canceled event");
7324 match &evt {
7325 ExecutionEvent::Order(OrderEventAny::Canceled(c)) => {
7326 assert_eq!(c.venue_order_id, Some(venue_order_id));
7327 }
7328 other => panic!("(C) expected canceled event, was {other:?}"),
7329 }
7330
7331 let msg_d = make_order("2.560972", "1775074738038", PolymarketEventType::Update);
7333 dispatch_user_message(&UserWsMessage::Order(msg_d), &ctx, &mut state);
7334
7335 let evt = receiver.try_recv().expect("(D) canceled event");
7336 match &evt {
7337 ExecutionEvent::Order(OrderEventAny::Canceled(c)) => {
7338 assert_eq!(c.venue_order_id, Some(venue_order_id));
7339 }
7340 other => panic!("(D) expected canceled event, was {other:?}"),
7341 }
7342
7343 let msg_e = make_trade("trade-e", 1.341461, "1775074738036");
7345 dispatch_user_message(&UserWsMessage::Trade(msg_e), &ctx, &mut state);
7346
7347 let evt = receiver.try_recv().expect("(E) filled event");
7348 match &evt {
7349 ExecutionEvent::Order(OrderEventAny::Filled(f)) => {
7350 assert_eq!(f.venue_order_id, venue_order_id);
7351 }
7352 other => panic!("(E) expected filled event, was {other:?}"),
7353 }
7354
7355 let evt = receiver.try_recv().expect("(E) re-emitted cancel");
7357 match &evt {
7358 ExecutionEvent::Order(OrderEventAny::Canceled(c)) => {
7359 assert_eq!(c.venue_order_id, Some(venue_order_id));
7360 }
7361 other => panic!("(E) expected re-emitted cancel, was {other:?}"),
7362 }
7363
7364 assert!(
7366 receiver.try_recv().is_err(),
7367 "No further events expected after the sequence"
7368 );
7369 }
7370
7371 #[rstest]
7372 fn test_dispatch_taker_fill_bumps_qty_for_dust_overfill() {
7373 use crate::common::enums::{
7377 PolymarketEventType, PolymarketOrderSide, PolymarketOutcome, PolymarketTradeStatus,
7378 };
7379
7380 let instrument = bind_instrument(test_instrument(), "0xmarket", PolymarketOutcome::yes());
7381 let asset_id = instrument.id().symbol.inner();
7382 let token_instruments = AtomicMap::new();
7383 token_instruments.insert(asset_id, instrument.clone());
7384
7385 let fill_tracker = OrderFillTrackerMap::new();
7386 let venue_order_id = VenueOrderId::from("0xtaker-overfill");
7387 let submitted =
7388 Quantity::from_decimal_dp(dec!(7.5757), instrument.size_precision()).unwrap();
7389 fill_tracker.register(
7390 venue_order_id,
7391 submitted,
7392 OrderSide::Buy,
7393 instrument.id(),
7394 instrument.size_precision(),
7395 instrument.price_precision(),
7396 );
7397
7398 let pending_submits = PendingSubmitTracker::default();
7399 let order_contexts = OrderContextRegistry::default();
7400 register_context(
7401 &order_contexts,
7402 venue_order_id,
7403 instrument.id(),
7404 "O-OVERFILL",
7405 );
7406 order_contexts.mark_accepted(venue_order_id);
7407 let mut emitter = test_emitter();
7408 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
7409 emitter.set_sender(sender);
7410
7411 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
7412 settlement.mark_live();
7413 settlement.note_order_submitted(venue_order_id);
7414
7415 let ctx = WsDispatchContext {
7416 signer_type: PolymarketSignerType::Owner,
7417 token_instruments: &token_instruments,
7418 fill_tracker: &fill_tracker,
7419 settlement: &settlement,
7420 pending_submits: &pending_submits,
7421 order_contexts: &order_contexts,
7422 emitter: &emitter,
7423 account_id: AccountId::from("POLY-001"),
7424 clock: nautilus_core::time::get_atomic_clock_realtime(),
7425 user_address: "0xtest",
7426 user_api_key: "00000000-0000-0000-0000-000000000001",
7427 };
7428 let mut state = WsDispatchState::default();
7429
7430 let trade = PolymarketUserTrade {
7431 asset_id,
7432 bucket_index: 0,
7433 fee_rate_bps: "0".to_string(),
7434 id: "trade-overfill".to_string(),
7435 last_update: "1700000001".to_string(),
7436 maker_address: Ustr::from("0xmaker"),
7437 maker_orders: vec![],
7438 market: Ustr::from("0xmarket"),
7439 match_time: "1700000000".to_string(),
7440 outcome: PolymarketOutcome::yes(),
7441 owner: Ustr::from("00000000-0000-0000-0000-000000000001"),
7442 price: "0.66".to_string(),
7443 side: PolymarketOrderSide::Buy,
7444 size: "7.575758".to_string(),
7445 status: PolymarketTradeStatus::Confirmed,
7446 taker_order_id: venue_order_id.as_str().to_string(),
7447 timestamp: "1700000000000".to_string(),
7448 trade_owner: Ustr::from("00000000-0000-0000-0000-000000000001"),
7449 transaction_hash: None,
7450 trader_side: PolymarketLiquiditySide::Taker,
7451 event_type: PolymarketEventType::Trade,
7452 };
7453
7454 dispatch_user_message(&UserWsMessage::Trade(trade), &ctx, &mut state);
7455
7456 let venue_qty =
7457 Quantity::from_decimal_dp(dec!(7.575758), instrument.size_precision()).unwrap();
7458 let cumulative = fill_tracker.get_cumulative_filled(&venue_order_id);
7459 let updated = receiver.try_recv().expect("expected an updated event");
7460 let filled = receiver.try_recv().expect("expected a filled event");
7461
7462 assert_eq!(cumulative, Some(venue_qty));
7463 assert_eq!(fill_tracker.submitted_qty(&venue_order_id), Some(venue_qty));
7464
7465 match updated {
7466 ExecutionEvent::Order(OrderEventAny::Updated(updated)) => {
7467 assert_eq!(updated.quantity, venue_qty);
7468 assert_eq!(updated.venue_order_id, Some(venue_order_id));
7469 }
7470 other => panic!("expected updated event, was {other:?}"),
7471 }
7472
7473 match filled {
7474 ExecutionEvent::Order(OrderEventAny::Filled(filled)) => {
7475 assert_eq!(filled.last_qty, venue_qty);
7476 assert_eq!(filled.venue_order_id, venue_order_id);
7477 }
7478 other => panic!("expected filled event, was {other:?}"),
7479 }
7480
7481 assert!(receiver.try_recv().is_err());
7482 }
7483
7484 #[rstest]
7485 #[case(
7486 TimeInForce::Ioc,
7487 OrderType::Market,
7488 OrderSide::Buy,
7489 "5.202910",
7490 "5.202897",
7491 false,
7492 true
7493 )]
7494 #[case(
7495 TimeInForce::Fok,
7496 OrderType::Limit,
7497 OrderSide::Buy,
7498 "5.202910",
7499 "5.202897",
7500 true,
7501 false
7502 )]
7503 #[case(
7504 TimeInForce::Ioc,
7505 OrderType::Limit,
7506 OrderSide::Buy,
7507 "30",
7508 "20",
7509 false,
7510 true
7511 )]
7512 #[case(
7513 TimeInForce::Ioc,
7514 OrderType::Market,
7515 OrderSide::Sell,
7516 "5.202910",
7517 "5.202897",
7518 false,
7519 true
7520 )]
7521 #[case(
7522 TimeInForce::Gtc,
7523 OrderType::Limit,
7524 OrderSide::Buy,
7525 "5.202910",
7526 "5.202897",
7527 false,
7528 false
7529 )]
7530 fn test_taker_terminal_status_on_trade_confirm(
7531 #[case] time_in_force: TimeInForce,
7532 #[case] order_type: OrderType,
7533 #[case] order_side: OrderSide,
7534 #[case] submitted_qty: &str,
7535 #[case] fill_qty: &str,
7536 #[case] expect_normalization: bool,
7537 #[case] expect_cancel: bool,
7538 ) {
7539 use crate::common::enums::{
7543 PolymarketEventType, PolymarketOrderSide, PolymarketOutcome, PolymarketTradeStatus,
7544 };
7545
7546 let instrument = bind_instrument(test_instrument(), "0xmarket", PolymarketOutcome::yes());
7547 let asset_id = instrument.id().symbol.inner();
7548 let token_instruments = AtomicMap::new();
7549 token_instruments.insert(asset_id, instrument.clone());
7550
7551 let fill_tracker = OrderFillTrackerMap::new();
7552 let venue_order_id = VenueOrderId::from("0xtaker-one-shot-dust");
7553 let submitted = Quantity::from_decimal_dp(
7554 Decimal::from_str_exact(submitted_qty).unwrap(),
7555 instrument.size_precision(),
7556 )
7557 .unwrap();
7558 fill_tracker.register(
7559 venue_order_id,
7560 submitted,
7561 order_side,
7562 instrument.id(),
7563 instrument.size_precision(),
7564 instrument.price_precision(),
7565 );
7566
7567 let pending_submits = PendingSubmitTracker::default();
7568 let order_contexts = OrderContextRegistry::default();
7569 order_contexts.register_context(
7570 venue_order_id,
7571 OrderContext {
7572 identity: OrderIdentity {
7573 client_order_id: ClientOrderId::from("O-ONE-SHOT"),
7574 strategy_id: StrategyId::from("S-001"),
7575 instrument_id: instrument.id(),
7576 order_side,
7577 order_type,
7578 },
7579 quantity: submitted,
7580 price: Some(Price::from("0.50")),
7581 trigger_price: None,
7582 trigger_type: None,
7583 time_in_force,
7584 is_post_only: false,
7585 is_reduce_only: false,
7586 is_quote_quantity: false,
7587 },
7588 );
7589 order_contexts.mark_accepted(venue_order_id);
7590 let mut emitter = test_emitter();
7591 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
7592 emitter.set_sender(sender);
7593
7594 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
7595 settlement.mark_live();
7596 settlement.note_order_submitted(venue_order_id);
7597
7598 let ctx = WsDispatchContext {
7599 signer_type: PolymarketSignerType::Owner,
7600 token_instruments: &token_instruments,
7601 fill_tracker: &fill_tracker,
7602 settlement: &settlement,
7603 pending_submits: &pending_submits,
7604 order_contexts: &order_contexts,
7605 emitter: &emitter,
7606 account_id: AccountId::from("POLY-001"),
7607 clock: nautilus_core::time::get_atomic_clock_realtime(),
7608 user_address: "0xtest",
7609 user_api_key: "00000000-0000-0000-0000-000000000001",
7610 };
7611 let mut state = WsDispatchState::default();
7612
7613 let trade = PolymarketUserTrade {
7614 asset_id,
7615 bucket_index: 0,
7616 fee_rate_bps: "0".to_string(),
7617 id: "trade-one-shot-dust".to_string(),
7618 last_update: "1700000001".to_string(),
7619 maker_address: Ustr::from("0xmaker"),
7620 maker_orders: vec![],
7621 market: Ustr::from("0xmarket"),
7622 match_time: "1700000000".to_string(),
7623 outcome: PolymarketOutcome::yes(),
7624 owner: Ustr::from("00000000-0000-0000-0000-000000000001"),
7625 price: "0.963".to_string(),
7626 side: if order_side == OrderSide::Buy {
7627 PolymarketOrderSide::Buy
7628 } else {
7629 PolymarketOrderSide::Sell
7630 },
7631 size: fill_qty.to_string(),
7632 status: PolymarketTradeStatus::Confirmed,
7633 taker_order_id: venue_order_id.as_str().to_string(),
7634 timestamp: "1700000000000".to_string(),
7635 trade_owner: Ustr::from("00000000-0000-0000-0000-000000000001"),
7636 transaction_hash: None,
7637 trader_side: PolymarketLiquiditySide::Taker,
7638 event_type: PolymarketEventType::Trade,
7639 };
7640
7641 dispatch_user_message(&UserWsMessage::Trade(trade), &ctx, &mut state);
7642
7643 let event = receiver.try_recv().expect("expected the venue fill event");
7644 match event {
7645 ExecutionEvent::Order(OrderEventAny::Filled(filled)) => {
7646 assert_eq!(
7647 filled.last_qty,
7648 Quantity::from_decimal_dp(
7649 Decimal::from_str_exact(fill_qty).unwrap(),
7650 instrument.size_precision(),
7651 )
7652 .unwrap(),
7653 );
7654 }
7655 other => panic!("expected filled event, was {other:?}"),
7656 }
7657
7658 if expect_normalization {
7659 let event = receiver.try_recv().expect("expected quantity update");
7660 match event {
7661 ExecutionEvent::Order(OrderEventAny::Updated(updated)) => {
7662 assert_eq!(
7663 updated.quantity,
7664 Quantity::new(5.202897, instrument.size_precision()),
7665 );
7666 assert_eq!(updated.venue_order_id, Some(venue_order_id));
7667 assert!(updated.reconciliation);
7668 }
7669 other => panic!("expected updated event, was {other:?}"),
7670 }
7671 assert!(
7672 fill_tracker
7673 .get_cumulative_filled(&venue_order_id)
7674 .is_none(),
7675 "order must be settled and removed from the tracker",
7676 );
7677 } else if expect_cancel {
7678 let event = receiver.try_recv().expect("expected IOC cancellation");
7679 match event {
7680 ExecutionEvent::Order(OrderEventAny::Canceled(canceled)) => {
7681 assert_eq!(canceled.venue_order_id, Some(venue_order_id));
7682 }
7683 other => panic!("expected canceled event, was {other:?}"),
7684 }
7685 assert!(
7686 fill_tracker
7687 .get_cumulative_filled(&venue_order_id)
7688 .is_none(),
7689 "canceled IOC must be settled and removed from the tracker",
7690 );
7691 } else {
7692 assert!(
7693 receiver.try_recv().is_err(),
7694 "resting order must not receive a terminal event",
7695 );
7696 assert!(
7697 fill_tracker
7698 .get_cumulative_filled(&venue_order_id)
7699 .is_some(),
7700 "ineligible order must stay tracked with open leaves",
7701 );
7702 }
7703 }
7704
7705 #[rstest]
7706 fn test_dispatch_taker_fill_gross_overfill_raises_qty_then_fills() {
7707 use crate::common::enums::{
7711 PolymarketEventType, PolymarketOrderSide, PolymarketOutcome, PolymarketTradeStatus,
7712 };
7713
7714 let instrument = bind_instrument(test_instrument(), "0xmarket", PolymarketOutcome::yes());
7715 let asset_id = instrument.id().symbol.inner();
7716 let size_precision = instrument.size_precision();
7717 let token_instruments = AtomicMap::new();
7718 token_instruments.insert(asset_id, instrument.clone());
7719
7720 let fill_tracker = OrderFillTrackerMap::new();
7721 let venue_order_id = VenueOrderId::from("0xtaker-gross-overfill");
7722 let submitted = Quantity::new(30.0, size_precision);
7723 fill_tracker.register(
7724 venue_order_id,
7725 submitted,
7726 OrderSide::Buy,
7727 instrument.id(),
7728 size_precision,
7729 instrument.price_precision(),
7730 );
7731
7732 let pending_submits = PendingSubmitTracker::default();
7733 let order_contexts = OrderContextRegistry::default();
7734 register_context(
7735 &order_contexts,
7736 venue_order_id,
7737 instrument.id(),
7738 "O-GROSS-OVERFILL",
7739 );
7740 order_contexts.mark_accepted(venue_order_id);
7741 let mut emitter = test_emitter();
7742 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
7743 emitter.set_sender(sender);
7744
7745 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
7746 settlement.mark_live();
7747 settlement.note_order_submitted(venue_order_id);
7748
7749 let ctx = WsDispatchContext {
7750 signer_type: PolymarketSignerType::Owner,
7751 token_instruments: &token_instruments,
7752 fill_tracker: &fill_tracker,
7753 settlement: &settlement,
7754 pending_submits: &pending_submits,
7755 order_contexts: &order_contexts,
7756 emitter: &emitter,
7757 account_id: AccountId::from("POLY-001"),
7758 clock: nautilus_core::time::get_atomic_clock_realtime(),
7759 user_address: "0xtest",
7760 user_api_key: "00000000-0000-0000-0000-000000000001",
7761 };
7762 let mut state = WsDispatchState::default();
7763
7764 let trade = PolymarketUserTrade {
7766 asset_id,
7767 bucket_index: 0,
7768 fee_rate_bps: "0".to_string(),
7769 id: "trade-gross-overfill".to_string(),
7770 last_update: "1700000001".to_string(),
7771 maker_address: Ustr::from("0xmaker"),
7772 maker_orders: vec![],
7773 market: Ustr::from("0xmarket"),
7774 match_time: "1700000000".to_string(),
7775 outcome: PolymarketOutcome::yes(),
7776 owner: Ustr::from("00000000-0000-0000-0000-000000000001"),
7777 price: "0.014".to_string(),
7778 side: PolymarketOrderSide::Buy,
7779 size: "33.846152".to_string(),
7780 status: PolymarketTradeStatus::Confirmed,
7781 taker_order_id: venue_order_id.as_str().to_string(),
7782 timestamp: "1700000000000".to_string(),
7783 trade_owner: Ustr::from("00000000-0000-0000-0000-000000000001"),
7784 transaction_hash: None,
7785 trader_side: PolymarketLiquiditySide::Taker,
7786 event_type: PolymarketEventType::Trade,
7787 };
7788
7789 dispatch_user_message(&UserWsMessage::Trade(trade), &ctx, &mut state);
7790
7791 let expected_qty = Quantity::new(33.846152, size_precision);
7792
7793 match receiver.try_recv().expect("expected an updated event") {
7795 ExecutionEvent::Order(OrderEventAny::Updated(updated)) => {
7796 assert_eq!(updated.quantity, expected_qty);
7797 assert_eq!(updated.venue_order_id, Some(venue_order_id));
7798 }
7799 other => panic!("expected updated event raising qty to the fill, was {other:?}"),
7800 }
7801
7802 match receiver.try_recv().expect("expected a filled event") {
7803 ExecutionEvent::Order(OrderEventAny::Filled(filled)) => {
7804 assert_eq!(filled.last_qty, expected_qty);
7805 assert_eq!(filled.venue_order_id, venue_order_id);
7806 }
7807 other => panic!("expected filled event, was {other:?}"),
7808 }
7809 }
7810
7811 #[rstest]
7814 #[case(
7815 crate::common::enums::PolymarketOrderStatus::Unmatched,
7816 Some("invalid post-only order: order crosses book"),
7817 "Rejected"
7818 )]
7819 #[case(
7820 crate::common::enums::PolymarketOrderStatus::CanceledMarketResolved,
7821 None,
7822 "Expired"
7823 )]
7824 fn test_dispatch_order_terminal_status_emits_event(
7825 #[case] status: crate::common::enums::PolymarketOrderStatus,
7826 #[case] reason: Option<&str>,
7827 #[case] expected: &str,
7828 ) {
7829 use crate::common::enums::{
7830 PolymarketEventType, PolymarketOrderSide, PolymarketOrderType, PolymarketOutcome,
7831 };
7832
7833 let instrument = test_instrument();
7834 let asset_id = instrument.id().symbol.inner();
7835 let order_id = "0xterminal-order".to_string();
7836 let venue_order_id = VenueOrderId::from(order_id.as_str());
7837
7838 let token_instruments = AtomicMap::new();
7839 token_instruments.insert(asset_id, instrument.clone());
7840
7841 let fill_tracker = OrderFillTrackerMap::new();
7842 fill_tracker.register(
7843 venue_order_id,
7844 Quantity::from("10"),
7845 OrderSide::Buy,
7846 instrument.id(),
7847 instrument.size_precision(),
7848 instrument.price_precision(),
7849 );
7850
7851 let pending_submits = PendingSubmitTracker::default();
7852 let order_contexts = OrderContextRegistry::default();
7853 register_context(
7854 &order_contexts,
7855 venue_order_id,
7856 instrument.id(),
7857 "O-TERMINAL",
7858 );
7859 order_contexts.mark_accepted(venue_order_id);
7860 let mut emitter = test_emitter();
7861 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
7862 emitter.set_sender(sender);
7863
7864 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
7865 settlement.mark_live();
7866 settlement.note_order_submitted(venue_order_id);
7867
7868 let ctx = WsDispatchContext {
7869 signer_type: PolymarketSignerType::Owner,
7870 token_instruments: &token_instruments,
7871 fill_tracker: &fill_tracker,
7872 settlement: &settlement,
7873 pending_submits: &pending_submits,
7874 order_contexts: &order_contexts,
7875 emitter: &emitter,
7876 account_id: AccountId::from("POLY-001"),
7877 clock: nautilus_core::time::get_atomic_clock_realtime(),
7878 user_address: "0xabc",
7879 user_api_key: "xxx",
7880 };
7881 let mut state = WsDispatchState::default();
7882
7883 let order = PolymarketUserOrder {
7884 asset_id,
7885 associate_trades: None,
7886 created_at: Some("1775074735".to_string()),
7887 expiration: Some("0".to_string()),
7888 id: order_id,
7889 maker_address: Some(Ustr::from("0xabc")),
7890 market: Ustr::from("0x4134"),
7891 order_owner: Some(Ustr::from("xxx")),
7892 order_type: Some(PolymarketOrderType::FOK),
7893 original_size: "10".to_string(),
7894 outcome: Some(PolymarketOutcome::yes()),
7895 owner: Ustr::from("xxx"),
7896 price: "0.50".to_string(),
7897 side: PolymarketOrderSide::Buy,
7898 size_matched: "0".to_string(),
7899 status: Some(PolymarketUserOrderStatus::new(status, reason)),
7900 timestamp: "1775074738031".to_string(),
7901 event_type: PolymarketEventType::Placement,
7902 };
7903
7904 dispatch_user_message(&UserWsMessage::Order(order), &ctx, &mut state);
7905
7906 let event = receiver.try_recv().expect("expected terminal order event");
7907 match event {
7908 ExecutionEvent::Order(order_event) => {
7909 assert!(
7910 format!("{order_event:?}").starts_with(expected),
7911 "expected {expected}, was {order_event:?}"
7912 );
7913 assert_eq!(
7914 order_event.client_order_id(),
7915 ClientOrderId::from("O-TERMINAL")
7916 );
7917
7918 if let OrderEventAny::Rejected(rejected) = order_event {
7919 assert_eq!(
7920 rejected.reason,
7921 "invalid post-only order: order crosses book"
7922 );
7923 assert!(rejected.due_post_only);
7924 }
7925 }
7926 other => panic!("expected order event, was {other:?}"),
7927 }
7928 }
7929}