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::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.take().unwrap_or_default();
2139 info.insert(
2140 "condition_id".to_string(),
2141 serde_json::Value::String(market.to_string()),
2142 );
2143 binary.info = Some(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 let mut info = binary.info.take().unwrap_or_default();
2160 info.insert(
2161 "fee_schedule".into(),
2162 serde_json::json!({
2163 "exponent": exponent.to_string(),
2164 "rate": rate.to_string(),
2165 "takerOnly": true,
2166 "rebateRate": "0",
2167 }),
2168 );
2169 binary.info = Some(info);
2170 }
2171
2172 fn test_emitter() -> ExecutionEventEmitter {
2173 ExecutionEventEmitter::new(
2174 nautilus_core::time::get_atomic_clock_realtime(),
2175 TraderId::from("TESTER-001"),
2176 AccountId::from("POLY-001"),
2177 AccountType::Cash,
2178 Some(Currency::pUSD()),
2179 )
2180 }
2181
2182 #[rstest]
2183 #[case::matching(|_: &mut PolymarketUserTrade, _: &mut InstrumentAny| {}, false, dec!(0))]
2184 #[case::quantity(|trade: &mut PolymarketUserTrade, _: &mut InstrumentAny| trade.size = "26.0".to_string(), true, dec!(0))]
2185 #[case::price(|trade: &mut PolymarketUserTrade, _: &mut InstrumentAny| trade.price = "0.6".to_string(), true, dec!(0))]
2186 #[case::side(|trade: &mut PolymarketUserTrade, _: &mut InstrumentAny| trade.side = PolymarketOrderSide::Sell, true, dec!(0))]
2187 #[case::fee_schedule(|_: &mut PolymarketUserTrade, instrument: &mut InstrumentAny| set_taker_fee_rate(instrument, dec!(0.01)), false, dec!(0))]
2188 fn test_dispatch_conflicting_trade_after_applied_fill(
2189 #[case] change: fn(&mut PolymarketUserTrade, &mut InstrumentAny),
2190 #[case] conflicts: bool,
2191 #[case] commission: Decimal,
2192 ) {
2193 let mut trade: PolymarketUserTrade = load("ws_user_trade.json");
2194 trade.status = PolymarketTradeStatus::Matched;
2195 let instrument = instrument_for_trade(&trade);
2196 let token_instruments = AtomicMap::new();
2197 token_instruments.insert(trade.asset_id, instrument.clone());
2198 let venue_order_id = VenueOrderId::from(trade.taker_order_id.as_str());
2199 let fill_tracker = OrderFillTrackerMap::new();
2200 fill_tracker.register(
2201 venue_order_id,
2202 Quantity::from("100.000000"),
2203 OrderSide::Buy,
2204 instrument.id(),
2205 instrument.size_precision(),
2206 instrument.price_precision(),
2207 );
2208 let pending_submits = PendingSubmitTracker::default();
2209 let order_contexts = OrderContextRegistry::default();
2210 register_context(
2211 &order_contexts,
2212 venue_order_id,
2213 instrument.id(),
2214 "O-CONFLICT",
2215 );
2216 order_contexts.mark_accepted(venue_order_id);
2217 let mut emitter = test_emitter();
2218 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
2219 emitter.set_sender(sender);
2220 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
2221 settlement.begin_session();
2222 settlement.note_order_submitted(venue_order_id);
2223
2224 let ctx = WsDispatchContext {
2225 signer_type: PolymarketSignerType::Owner,
2226 token_instruments: &token_instruments,
2227 fill_tracker: &fill_tracker,
2228 settlement: &settlement,
2229 pending_submits: &pending_submits,
2230 order_contexts: &order_contexts,
2231 emitter: &emitter,
2232 account_id: AccountId::from("POLY-001"),
2233 clock: nautilus_core::time::get_atomic_clock_realtime(),
2234 user_address: trade.maker_address.as_str(),
2235 user_api_key: trade.owner.as_str(),
2236 };
2237
2238 let mut state = WsDispatchState::default();
2239 dispatch_user_message(&UserWsMessage::Trade(trade.clone()), &ctx, &mut state);
2240
2241 let ExecutionEvent::Order(OrderEventAny::Filled(fill)) = receiver.try_recv().unwrap()
2242 else {
2243 panic!("expected original fill");
2244 };
2245
2246 settlement.observe_fill_applied(&fill);
2247 let mut incoming = trade.clone();
2248 incoming.status = PolymarketTradeStatus::Confirmed;
2249 let mut updated_instrument = instrument;
2250 change(&mut incoming, &mut updated_instrument);
2251 token_instruments.insert(trade.asset_id, updated_instrument);
2252 let admitted =
2253 admit_trade_evidence(TradeEvidence::Stream(&incoming), &ctx.admission_context())
2254 .unwrap();
2255
2256 dispatch_user_message(&UserWsMessage::Trade(incoming), &ctx, &mut state);
2257
2258 assert_eq!(fill.trade_id, TradeId::from(trade.id.as_str()));
2259 assert_eq!(fill.last_qty, Quantity::from("25.000000"));
2260 assert_eq!(fill.last_px.as_decimal(), dec!(0.5));
2261 assert_eq!(fill.order_side, OrderSide::Buy);
2262 assert_eq!(fill.commission, Some(Money::zero(Currency::pUSD())));
2263 assert_eq!(admitted.legs[0].commission.as_decimal(), commission);
2264 assert!(receiver.try_recv().is_err());
2265 assert_eq!(
2266 settlement_state(&settlement, &trade.id),
2267 Some(if conflicts {
2268 SettlementState::Quarantined
2269 } else {
2270 SettlementState::StreamConfirmed
2271 })
2272 );
2273 assert_eq!(
2274 settlement.pending_resolutions(),
2275 if conflicts {
2276 vec![trade.id.clone()]
2277 } else {
2278 vec![]
2279 }
2280 );
2281 assert_eq!(trade_hard_fault(&settlement, &trade.id), None);
2282 assert_eq!(
2283 settlement.ensure_resolved(None, "mass status").is_ok(),
2284 !conflicts
2285 );
2286 }
2287
2288 #[rstest]
2289 #[case::refresh_before_stream_confirm(true, false)]
2290 #[case::refresh_after_stream_confirm(false, false)]
2291 #[case::refresh_exponent(true, true)]
2292 fn test_fee_schedule_refresh_preserves_admitted_taker_commission(
2293 #[case] refresh_before_stream: bool,
2294 #[case] refresh_exponent: bool,
2295 ) {
2296 let mut trade: PolymarketUserTrade = load("ws_user_trade.json");
2297 trade.status = PolymarketTradeStatus::Matched;
2298 let mut instrument = instrument_for_trade(&trade);
2299 set_taker_fee_rate(&mut instrument, dec!(0.0721));
2300 let token_instruments = AtomicMap::new();
2301 token_instruments.insert(trade.asset_id, instrument.clone());
2302 let venue_order_id = VenueOrderId::from(trade.taker_order_id.as_str());
2303 let fill_tracker = OrderFillTrackerMap::new();
2304 fill_tracker.register(
2305 venue_order_id,
2306 Quantity::from("100.000000"),
2307 OrderSide::Buy,
2308 instrument.id(),
2309 instrument.size_precision(),
2310 instrument.price_precision(),
2311 );
2312 let pending_submits = PendingSubmitTracker::default();
2313 let order_contexts = OrderContextRegistry::default();
2314 register_context(
2315 &order_contexts,
2316 venue_order_id,
2317 instrument.id(),
2318 "O-FEE-BASIS",
2319 );
2320 order_contexts.mark_accepted(venue_order_id);
2321 let mut emitter = test_emitter();
2322 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
2323 emitter.set_sender(sender);
2324 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
2325 settlement.begin_session();
2326 settlement.note_order_submitted(venue_order_id);
2327
2328 let ctx = WsDispatchContext {
2329 signer_type: PolymarketSignerType::Owner,
2330 token_instruments: &token_instruments,
2331 fill_tracker: &fill_tracker,
2332 settlement: &settlement,
2333 pending_submits: &pending_submits,
2334 order_contexts: &order_contexts,
2335 emitter: &emitter,
2336 account_id: AccountId::from("POLY-001"),
2337 clock: nautilus_core::time::get_atomic_clock_realtime(),
2338 user_address: trade.maker_address.as_str(),
2339 user_api_key: trade.owner.as_str(),
2340 };
2341
2342 let mut state = WsDispatchState::default();
2343
2344 dispatch_user_message(&UserWsMessage::Trade(trade.clone()), &ctx, &mut state);
2345
2346 let ExecutionEvent::Order(OrderEventAny::Filled(fill)) = receiver.try_recv().unwrap()
2347 else {
2348 panic!("expected original fill");
2349 };
2350
2351 settlement.observe_fill_applied(&fill);
2352
2353 let refresh = |rate| {
2354 let mut updated = instrument.clone();
2355 set_taker_fee_rate(&mut updated, rate);
2356 token_instruments.insert(trade.asset_id, updated);
2357 };
2358
2359 if refresh_before_stream {
2360 if refresh_exponent {
2361 let mut updated = instrument.clone();
2362 set_taker_fee_schedule(&mut updated, dec!(0.0721), Decimal::TWO);
2363 token_instruments.insert(trade.asset_id, updated);
2364 } else {
2365 refresh(dec!(0.01));
2366 }
2367 }
2368
2369 let mut confirmed = trade.clone();
2370 confirmed.status = PolymarketTradeStatus::Confirmed;
2371 dispatch_user_message(&UserWsMessage::Trade(confirmed.clone()), &ctx, &mut state);
2372
2373 if !refresh_before_stream {
2374 refresh(dec!(0.01));
2375 }
2376
2377 let admitted_stream =
2378 admit_trade_evidence(TradeEvidence::Stream(&confirmed), &ctx.admission_context())
2379 .unwrap();
2380 assert_eq!(
2381 admitted_stream.legs[0].commission.as_decimal(),
2382 dec!(0.45062)
2383 );
2384
2385 let mut rest: PolymarketTradeReport = load("http_trade_report.json");
2386 rest.id = trade.id.clone();
2387 rest.taker_order_id = trade.taker_order_id.clone();
2388 apply_rest_trade_evidence(&rest, &ctx, &mut state);
2389 apply_rest_trade_evidence(&rest, &ctx, &mut state);
2390 assert_eq!(
2391 settlement_state(&settlement, &trade.id),
2392 Some(SettlementState::RestConfirmed)
2393 );
2394 assert_eq!(trade_hard_fault(&settlement, &trade.id), None);
2395 assert!(settlement.pending_resolutions().is_empty());
2396 assert!(settlement.ensure_resolved(None, "mass status").is_ok());
2397 let admitted =
2398 admit_trade_evidence(TradeEvidence::Rest(&rest), &ctx.admission_context()).unwrap();
2399 let report = settlement
2400 .build_fill_report(&trade.id, &admitted.legs[0], UnixNanos::from(1))
2401 .unwrap();
2402 assert_eq!(report.commission.as_decimal(), dec!(0.45062));
2403 assert_eq!(report.last_qty, fill.last_qty);
2404 assert_eq!(report.last_px, fill.last_px);
2405
2406 let mut fresh = trade.clone();
2407 fresh.id = format!("{}-fresh", trade.id);
2408 fresh.taker_order_id = format!("{}-fresh", trade.taker_order_id);
2409 fresh.status = PolymarketTradeStatus::Matched;
2410 let fresh_order_id = VenueOrderId::from(fresh.taker_order_id.as_str());
2411 fill_tracker.register(
2412 fresh_order_id,
2413 Quantity::from("100.000000"),
2414 OrderSide::Buy,
2415 instrument.id(),
2416 instrument.size_precision(),
2417 instrument.price_precision(),
2418 );
2419 register_context(
2420 &order_contexts,
2421 fresh_order_id,
2422 instrument.id(),
2423 "O-FEE-FRESH",
2424 );
2425 order_contexts.mark_accepted(fresh_order_id);
2426 settlement.note_order_submitted(fresh_order_id);
2427 dispatch_user_message(&UserWsMessage::Trade(fresh), &ctx, &mut state);
2428
2429 let ExecutionEvent::Order(OrderEventAny::Filled(fresh_fill)) = receiver.try_recv().unwrap()
2430 else {
2431 panic!("expected fresh fill");
2432 };
2433
2434 assert_eq!(fill.trade_id, TradeId::from(trade.id.as_str()));
2435 assert_eq!(fill.last_qty, Quantity::from("25.000000"));
2436 assert_eq!(fill.last_px.as_decimal(), dec!(0.5));
2437 assert_eq!(fill.commission.unwrap().as_decimal(), dec!(0.45062));
2438 assert_ne!(dec!(0.450625), fill.commission.unwrap().as_decimal());
2439
2440 let fresh_commission = if refresh_exponent {
2441 dec!(0.11265)
2442 } else {
2443 dec!(0.0625)
2444 };
2445
2446 assert_eq!(
2447 fresh_fill.commission.unwrap().as_decimal(),
2448 fresh_commission
2449 );
2450 assert!(receiver.try_recv().is_err());
2451 assert_eq!(
2452 fill_tracker.get_cumulative_filled(&venue_order_id),
2453 Some(fill.last_qty)
2454 );
2455 }
2456
2457 #[rstest]
2458 fn test_fee_schedule_refresh_keeps_maker_commission_zero() {
2459 let mut trade: PolymarketUserTrade = load("ws_user_trade.json");
2460 trade.status = PolymarketTradeStatus::Matched;
2461 trade.trader_side = PolymarketLiquiditySide::Maker;
2462 let mut instrument = instrument_for_trade(&trade);
2463 set_taker_fee_rate(&mut instrument, dec!(0.0721));
2464 let token_instruments = AtomicMap::new();
2465 token_instruments.insert(trade.asset_id, instrument.clone());
2466 let maker_order_id = VenueOrderId::from(trade.maker_orders[0].order_id.as_str());
2467 let fill_tracker = OrderFillTrackerMap::new();
2468 fill_tracker.register(
2469 maker_order_id,
2470 Quantity::from("100.000000"),
2471 OrderSide::Buy,
2472 instrument.id(),
2473 instrument.size_precision(),
2474 instrument.price_precision(),
2475 );
2476 let pending_submits = PendingSubmitTracker::default();
2477 let order_contexts = OrderContextRegistry::default();
2478 register_context(
2479 &order_contexts,
2480 maker_order_id,
2481 instrument.id(),
2482 "O-MAKER-FEE",
2483 );
2484 order_contexts.mark_accepted(maker_order_id);
2485 let mut emitter = test_emitter();
2486 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
2487 emitter.set_sender(sender);
2488 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
2489 settlement.begin_session();
2490 settlement.note_order_submitted(maker_order_id);
2491 let maker_address = trade.maker_orders[0].maker_address.clone();
2492
2493 let ctx = WsDispatchContext {
2494 signer_type: PolymarketSignerType::Owner,
2495 token_instruments: &token_instruments,
2496 fill_tracker: &fill_tracker,
2497 settlement: &settlement,
2498 pending_submits: &pending_submits,
2499 order_contexts: &order_contexts,
2500 emitter: &emitter,
2501 account_id: AccountId::from("POLY-001"),
2502 clock: nautilus_core::time::get_atomic_clock_realtime(),
2503 user_address: maker_address.as_str(),
2504 user_api_key: "ffffffff-ffff-ffff-ffff-ffffffffffff",
2505 };
2506
2507 let mut state = WsDispatchState::default();
2508
2509 dispatch_user_message(&UserWsMessage::Trade(trade.clone()), &ctx, &mut state);
2510
2511 let ExecutionEvent::Order(OrderEventAny::Filled(fill)) = receiver.try_recv().unwrap()
2512 else {
2513 panic!("expected maker fill");
2514 };
2515
2516 settlement.observe_fill_applied(&fill);
2517 set_taker_fee_rate(&mut instrument, dec!(0.01));
2518 token_instruments.insert(trade.asset_id, instrument);
2519 let mut confirmed = trade.clone();
2520 confirmed.status = PolymarketTradeStatus::Confirmed;
2521 dispatch_user_message(&UserWsMessage::Trade(confirmed), &ctx, &mut state);
2522
2523 assert!(fill.commission.unwrap().is_zero());
2524 assert!(receiver.try_recv().is_err());
2525 assert_eq!(
2526 settlement_state(&settlement, &trade.id),
2527 Some(SettlementState::StreamConfirmed)
2528 );
2529 assert_eq!(trade_hard_fault(&settlement, &trade.id), None);
2530 }
2531
2532 #[rstest]
2533 #[case::unknown(false, false, false)]
2534 #[case::applied_unowned_rest(true, false, false)]
2535 #[case::applied_matching_rest(true, false, true)]
2536 #[case::settled_unowned_rest(true, true, false)]
2537 #[case::settled_matching_rest(true, true, true)]
2538 fn test_dispatch_trade_missing_all_owned_legs(
2539 #[case] known: bool,
2540 #[case] rest_settled: bool,
2541 #[case] rest_owned: bool,
2542 ) {
2543 let mut trade: PolymarketUserTrade = load("ws_user_trade.json");
2544 trade.trader_side = PolymarketLiquiditySide::Maker;
2545 trade.side = PolymarketOrderSide::Sell;
2546 trade.status = PolymarketTradeStatus::Matched;
2547 let user_address = trade.maker_orders[0].maker_address.clone();
2548 let api_key = trade.maker_orders[0].owner.clone();
2549 let venue_order_id = VenueOrderId::from(trade.maker_orders[0].order_id.as_str());
2550 let instrument = instrument_for_trade(&trade);
2551 let token_instruments = AtomicMap::new();
2552 token_instruments.insert(trade.asset_id, instrument.clone());
2553 let fill_tracker = OrderFillTrackerMap::new();
2554 fill_tracker.register(
2555 venue_order_id,
2556 Quantity::from("100.000000"),
2557 OrderSide::Buy,
2558 instrument.id(),
2559 instrument.size_precision(),
2560 instrument.price_precision(),
2561 );
2562 let pending_submits = PendingSubmitTracker::default();
2563 let order_contexts = OrderContextRegistry::default();
2564 register_context(
2565 &order_contexts,
2566 venue_order_id,
2567 instrument.id(),
2568 "O-MISSING-MAKER",
2569 );
2570 order_contexts.mark_accepted(venue_order_id);
2571 let mut emitter = test_emitter();
2572 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
2573 emitter.set_sender(sender);
2574 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
2575 settlement.begin_session();
2576 settlement.note_order_submitted(venue_order_id);
2577
2578 let ctx = WsDispatchContext {
2579 signer_type: PolymarketSignerType::Owner,
2580 token_instruments: &token_instruments,
2581 fill_tracker: &fill_tracker,
2582 settlement: &settlement,
2583 pending_submits: &pending_submits,
2584 order_contexts: &order_contexts,
2585 emitter: &emitter,
2586 account_id: AccountId::from("POLY-001"),
2587 clock: nautilus_core::time::get_atomic_clock_realtime(),
2588 user_address: user_address.as_str(),
2589 user_api_key: api_key.as_str(),
2590 };
2591
2592 let mut state = WsDispatchState::default();
2593
2594 if known {
2595 dispatch_user_message(&UserWsMessage::Trade(trade.clone()), &ctx, &mut state);
2596
2597 let ExecutionEvent::Order(OrderEventAny::Filled(fill)) = receiver.try_recv().unwrap()
2598 else {
2599 panic!("expected original maker fill");
2600 };
2601
2602 assert_eq!(fill.venue_order_id, venue_order_id);
2603 assert_eq!(fill.liquidity_side, LiquiditySide::Maker);
2604 settlement.observe_fill_applied(&fill);
2605 }
2606
2607 if rest_settled {
2608 let mut admitted =
2609 admit_trade_evidence(TradeEvidence::Stream(&trade), &ctx.admission_context())
2610 .unwrap();
2611 admitted.status = PolymarketTradeStatus::Confirmed;
2612 settlement.admit_rest_result(&admitted);
2613 }
2614
2615 let mut rest: PolymarketTradeReport = load("http_trade_report.json");
2616 rest.id = trade.id.clone();
2617 rest.taker_order_id = trade.taker_order_id.clone();
2618 rest.trader_side = trade.trader_side;
2619 rest.side = trade.side;
2620 rest.maker_orders = trade.maker_orders.clone();
2621 for maker in &mut rest.maker_orders {
2622 maker.side = Some(PolymarketOrderSide::Buy);
2623 }
2624
2625 if !rest_owned {
2626 rest.maker_orders.clear();
2627 }
2628
2629 trade.maker_orders.clear();
2630 trade.status = PolymarketTradeStatus::Confirmed;
2631 assert!(settlement.resolution_requested().now_or_never().is_none());
2632
2633 dispatch_user_message(&UserWsMessage::Trade(trade.clone()), &ctx, &mut state);
2634
2635 assert_eq!(
2636 settlement.resolution_requested().now_or_never().is_some(),
2637 known
2638 );
2639 assert!(receiver.try_recv().is_err());
2640 assert_eq!(settlement.record_count(), usize::from(known));
2641
2642 let expected = known.then_some(if rest_settled {
2643 SettlementState::RestConfirmed
2644 } else {
2645 SettlementState::Quarantined
2646 });
2647
2648 assert_eq!(settlement_state(&settlement, &trade.id), expected);
2649 assert_eq!(
2650 settlement.pending_resolutions(),
2651 if known {
2652 vec![trade.id.clone()]
2653 } else {
2654 vec![]
2655 }
2656 );
2657 assert_eq!(trade_hard_fault(&settlement, &trade.id), None);
2658 assert_eq!(
2659 settlement.ensure_resolved(None, "mass status").is_ok(),
2660 !known
2661 );
2662
2663 if known {
2664 apply_rest_trade_evidence(&rest, &ctx, &mut state);
2665
2666 assert!(receiver.try_recv().is_err());
2667 assert_eq!(
2668 settlement_state(&settlement, &trade.id),
2669 Some(if rest_settled || rest_owned {
2670 SettlementState::RestConfirmed
2671 } else {
2672 SettlementState::Quarantined
2673 })
2674 );
2675 assert_eq!(
2676 settlement.pending_resolutions(),
2677 if rest_owned {
2678 vec![]
2679 } else {
2680 vec![trade.id.clone()]
2681 }
2682 );
2683 assert_eq!(trade_hard_fault(&settlement, &trade.id), None);
2684 assert_eq!(
2685 settlement.ensure_resolved(None, "mass status").is_ok(),
2686 rest_owned
2687 );
2688 assert_eq!(
2689 fill_tracker.get_cumulative_filled(&venue_order_id),
2690 Some(Quantity::from("25.000000"))
2691 );
2692 }
2693 }
2694
2695 #[rstest]
2696 fn test_emit_order_rejected_uses_bounded_clean_reason() {
2697 let instrument = test_instrument();
2698 let token_instruments = AtomicMap::new();
2699 let fill_tracker = OrderFillTrackerMap::new();
2700 let pending_submits = PendingSubmitTracker::default();
2701 let order_contexts = OrderContextRegistry::default();
2702 let mut emitter = test_emitter();
2703 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
2704
2705 emitter.set_sender(sender);
2706
2707 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
2708 settlement.mark_live();
2709
2710 let ctx = WsDispatchContext {
2711 signer_type: PolymarketSignerType::Owner,
2712 token_instruments: &token_instruments,
2713 fill_tracker: &fill_tracker,
2714 settlement: &settlement,
2715 pending_submits: &pending_submits,
2716 order_contexts: &order_contexts,
2717 emitter: &emitter,
2718 account_id: AccountId::from("POLY-001"),
2719 clock: nautilus_core::time::get_atomic_clock_realtime(),
2720 user_address: "0xtest",
2721 user_api_key: "test-key",
2722 };
2723 let context = OrderContext {
2724 identity: OrderIdentity {
2725 client_order_id: ClientOrderId::from("O-WS-REJECT"),
2726 strategy_id: StrategyId::from("S-001"),
2727 instrument_id: instrument.id(),
2728 order_side: OrderSide::Buy,
2729 order_type: OrderType::Limit,
2730 },
2731 quantity: Quantity::from("10"),
2732 price: Some(Price::from("0.50")),
2733 trigger_price: None,
2734 trigger_type: None,
2735 time_in_force: TimeInForce::Gtc,
2736 is_post_only: false,
2737 is_reduce_only: false,
2738 is_quote_quantity: false,
2739 };
2740
2741 emit_order_rejected(
2742 &context,
2743 " invalid post-only order:\norder crosses book ",
2744 UnixNanos::from(1_000_000_000),
2745 &ctx,
2746 );
2747
2748 match receiver.try_recv().expect("expected rejected event") {
2749 ExecutionEvent::Order(OrderEventAny::Rejected(event)) => {
2750 assert_eq!(event.reason, "invalid post-only order: order crosses book");
2751 assert!(event.due_post_only);
2752 }
2753 other => panic!("expected rejected event, was {other:?}"),
2754 }
2755 }
2756
2757 #[rstest]
2758 #[case::empty_price("", "1.01", "0")]
2759 #[case::zero_price("0", "1.01", "0")]
2760 #[case::malformed("bad", "100", "0")]
2761 #[case::too_precise("0.50000000000000000000000000001", "100", "0")]
2762 #[case::quantity_overflow("0.5", "79228162514264337593543950335", "0")]
2763 #[case::filled_overflow("0.5", "100", "79228162514264337593543950335")]
2764 fn test_ws_order_report_rejects_invalid_values(
2765 #[case] price: &str,
2766 #[case] quantity: &str,
2767 #[case] filled: &str,
2768 ) {
2769 let mut order: PolymarketUserOrder = load("ws_user_order_placement.json");
2770 order.price = price.into();
2771 order.original_size = quantity.into();
2772 order.size_matched = filled.into();
2773 assert!(
2774 build_ws_order_status_report(
2775 &order,
2776 order.status.as_ref().unwrap(),
2777 order.order_type.unwrap(),
2778 &test_instrument(),
2779 AccountId::from("POLY-001"),
2780 UnixNanos::default(),
2781 UnixNanos::default()
2782 )
2783 .is_err()
2784 );
2785 }
2786
2787 #[rstest]
2788 fn test_ws_fok_order_report_rejects_share_overflow() {
2789 let mut order: PolymarketUserOrder = load("ws_user_order_fok_buy_pusd_size.json");
2790 order.original_size = Decimal::MAX.to_string();
2791 order.price = "0.5".into();
2792 let result = build_ws_order_status_report(
2793 &order,
2794 order.status.as_ref().unwrap(),
2795 PolymarketOrderType::FOK,
2796 &test_instrument(),
2797 AccountId::from("POLY-001"),
2798 UnixNanos::default(),
2799 UnixNanos::default(),
2800 );
2801
2802 assert_eq!(
2803 result.unwrap_err().to_string(),
2804 "order share quantity overflow"
2805 );
2806 }
2807
2808 #[rstest]
2809 fn test_build_ws_order_status_report() {
2810 let order: PolymarketUserOrder = load("ws_user_order_placement.json");
2811 let instrument = test_instrument();
2812 let ts_event = UnixNanos::from(1_000_000_000u64);
2813 let ts_init = UnixNanos::from(2_000_000_000u64);
2814
2815 let report = build_ws_order_status_report(
2816 &order,
2817 order.status.as_ref().unwrap(),
2818 order.order_type.unwrap(),
2819 &instrument,
2820 AccountId::from("POLY-001"),
2821 ts_event,
2822 ts_init,
2823 )
2824 .unwrap();
2825
2826 assert_eq!(report.order_side, Some(OrderSide::Buy));
2827 assert_eq!(report.order_type, OrderType::Limit);
2828 assert_eq!(report.quantity.as_decimal(), dec!(100));
2830 assert_eq!(
2831 report.price.map(|price| price.as_decimal()),
2832 Some(dec!(0.5))
2833 );
2834 assert_eq!(report.ts_accepted, ts_event);
2835 assert_eq!(report.ts_init, ts_init);
2836 }
2837
2838 #[rstest]
2839 fn test_build_ws_order_status_report_venue_cancel_maps_to_canceled() {
2840 let order: PolymarketUserOrder = load("ws_user_order_venue_cancel.json");
2841 let instrument = test_instrument();
2842 let ts_event = UnixNanos::from(1_000_000_000u64);
2843 let ts_init = UnixNanos::from(2_000_000_000u64);
2844
2845 let report = build_ws_order_status_report(
2846 &order,
2847 order.status.as_ref().unwrap(),
2848 order.order_type.unwrap(),
2849 &instrument,
2850 AccountId::from("POLY-001"),
2851 ts_event,
2852 ts_init,
2853 )
2854 .unwrap();
2855
2856 assert_eq!(report.order_status, OrderStatus::Canceled);
2857 }
2858
2859 #[rstest]
2860 #[case::before_window(939_000_000_000, OrderStatus::Canceled)]
2861 #[case::window_start(940_000_000_000, OrderStatus::Expired)]
2862 #[case::security_threshold(999_000_000_000, OrderStatus::Expired)]
2863 #[case::at_expiry(1_000_000_000_000, OrderStatus::Expired)]
2864 #[case::after_expiry(1_000_000_001_000, OrderStatus::Expired)]
2865 fn test_build_ws_order_status_report_gtd_expiry_maps_to_expired(
2866 #[case] ts_event: u64,
2867 #[case] expected: OrderStatus,
2868 ) {
2869 let mut order: PolymarketUserOrder = load("ws_user_order_venue_cancel.json");
2870 order.order_type = Some(PolymarketOrderType::GTD);
2871 order.expiration = Some("1000".to_string());
2872 let instrument = test_instrument();
2873
2874 let report = build_ws_order_status_report(
2875 &order,
2876 order.status.as_ref().unwrap(),
2877 order.order_type.unwrap(),
2878 &instrument,
2879 AccountId::from("POLY-001"),
2880 UnixNanos::from(ts_event),
2881 UnixNanos::from(2_000_000_000u64),
2882 )
2883 .unwrap();
2884
2885 assert_eq!(report.order_status, expected);
2886 assert_eq!(
2887 report.expire_time,
2888 Some(UnixNanos::from(1_000_000_000_000u64))
2889 );
2890 }
2891
2892 #[rstest]
2893 #[case::gtc_in_window(PolymarketOrderType::GTC, Some("1000"))]
2894 #[case::gtd_zero_expiration(PolymarketOrderType::GTD, Some("0"))]
2895 #[case::gtd_missing_expiration(PolymarketOrderType::GTD, None)]
2896 fn test_build_ws_order_status_report_non_expiry_cancel_stays_canceled(
2897 #[case] order_type: PolymarketOrderType,
2898 #[case] expiration: Option<&str>,
2899 ) {
2900 let mut order: PolymarketUserOrder = load("ws_user_order_venue_cancel.json");
2901 order.order_type = Some(order_type);
2902 order.expiration = expiration.map(str::to_string);
2903 let instrument = test_instrument();
2904
2905 let report = build_ws_order_status_report(
2906 &order,
2907 order.status.as_ref().unwrap(),
2908 order.order_type.unwrap(),
2909 &instrument,
2910 AccountId::from("POLY-001"),
2911 UnixNanos::from(999_000_000_000u64),
2912 UnixNanos::from(2_000_000_000u64),
2913 )
2914 .unwrap();
2915
2916 assert_eq!(report.order_status, OrderStatus::Canceled);
2917 }
2918
2919 #[rstest]
2920 fn test_reemit_terminal_expiry_emits_expired() {
2921 let mut order: PolymarketUserOrder = load("ws_user_order_venue_cancel.json");
2922 order.order_type = Some(PolymarketOrderType::GTD);
2923 order.expiration = Some("1000".to_string());
2924 let instrument = test_instrument();
2925 let report = build_ws_order_status_report(
2926 &order,
2927 order.status.as_ref().unwrap(),
2928 PolymarketOrderType::GTD,
2929 &instrument,
2930 AccountId::from("POLY-001"),
2931 UnixNanos::from(1_000_000_000_000u64),
2932 UnixNanos::from(2_000_000_000u64),
2933 )
2934 .unwrap();
2935 let venue_order_id = report.venue_order_id;
2936 let mut state = WsDispatchState::default();
2937 state.record_terminal_cancel_report(report);
2938
2939 let token_instruments = AtomicMap::new();
2940 let fill_tracker = OrderFillTrackerMap::new();
2941 let pending_submits = PendingSubmitTracker::default();
2942 let order_contexts = OrderContextRegistry::default();
2943
2944 let context = OrderContext {
2945 identity: OrderIdentity {
2946 client_order_id: ClientOrderId::from("O-GTD-EXPIRE"),
2947 strategy_id: StrategyId::from("S-001"),
2948 instrument_id: instrument.id(),
2949 order_side: OrderSide::Buy,
2950 order_type: OrderType::Limit,
2951 },
2952 quantity: Quantity::from("10"),
2953 price: Some(Price::from("0.50")),
2954 trigger_price: None,
2955 trigger_type: None,
2956 time_in_force: TimeInForce::Gtd,
2957 is_post_only: false,
2958 is_reduce_only: false,
2959 is_quote_quantity: false,
2960 };
2961
2962 order_contexts.register_context(venue_order_id, context);
2963 let mut emitter = test_emitter();
2964 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
2965 emitter.set_sender(sender);
2966 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
2967
2968 let ctx = WsDispatchContext {
2969 signer_type: PolymarketSignerType::Owner,
2970 token_instruments: &token_instruments,
2971 fill_tracker: &fill_tracker,
2972 settlement: &settlement,
2973 pending_submits: &pending_submits,
2974 order_contexts: &order_contexts,
2975 emitter: &emitter,
2976 account_id: AccountId::from("POLY-001"),
2977 clock: nautilus_core::time::get_atomic_clock_realtime(),
2978 user_address: "0xtest",
2979 user_api_key: "test-key",
2980 };
2981
2982 reemit_terminal_cancel(venue_order_id, &state, &ctx);
2983
2984 match receiver.try_recv().expect("expected expired event") {
2985 ExecutionEvent::Order(OrderEventAny::Expired(event)) => {
2986 assert_eq!(event.client_order_id, ClientOrderId::from("O-GTD-EXPIRE"));
2987 }
2988 other => panic!("expected OrderExpired, was {other:?}"),
2989 }
2990 }
2991
2992 #[rstest]
2995 #[case(
2996 PolymarketOrderSide::Buy,
2997 PolymarketOrderType::FOK,
2998 dec!(1.01),
2999 dec!(0.01),
3000 dec!(101)
3001 )]
3002 #[case(
3003 PolymarketOrderSide::Buy,
3004 PolymarketOrderType::FOK,
3005 dec!(12),
3006 dec!(0.6),
3007 dec!(20)
3008 )]
3009 #[case(
3010 PolymarketOrderSide::Buy,
3011 PolymarketOrderType::FAK,
3012 dec!(1),
3013 dec!(0.01),
3014 dec!(100)
3015 )]
3016 #[case(
3017 PolymarketOrderSide::Buy,
3018 PolymarketOrderType::GTC,
3019 dec!(20),
3020 dec!(0.18),
3021 dec!(20)
3022 )]
3023 #[case(
3024 PolymarketOrderSide::Buy,
3025 PolymarketOrderType::GTD,
3026 dec!(20),
3027 dec!(0.18),
3028 dec!(20)
3029 )]
3030 #[case(
3031 PolymarketOrderSide::Sell,
3032 PolymarketOrderType::FOK,
3033 dec!(20),
3034 dec!(0.6),
3035 dec!(20)
3036 )]
3037 fn test_original_size_to_shares(
3038 #[case] side: PolymarketOrderSide,
3039 #[case] order_type: PolymarketOrderType,
3040 #[case] original_size: Decimal,
3041 #[case] price: Decimal,
3042 #[case] expected: Decimal,
3043 ) {
3044 let shares = original_size_to_shares(original_size, price, side, order_type).unwrap();
3045
3046 assert_eq!(shares, expected);
3047 }
3048
3049 #[rstest]
3051 #[case("1", "0.03", "33.333333", "0.03")]
3052 fn test_build_ws_order_status_report_fok_buy_quantity(
3053 #[case] original_size: &str,
3054 #[case] price: &str,
3055 #[case] expected_quantity: &str,
3056 #[case] expected_price: &str,
3057 ) {
3058 let mut order: PolymarketUserOrder = load("ws_user_order_fok_buy_pusd_size.json");
3059 order.original_size = original_size.to_string();
3060 order.price = price.to_string();
3061 let instrument = test_instrument();
3062
3063 let report = build_ws_order_status_report(
3064 &order,
3065 order.status.as_ref().unwrap(),
3066 order.order_type.unwrap(),
3067 &instrument,
3068 AccountId::from("POLY-001"),
3069 UnixNanos::from(1_000_000_000u64),
3070 UnixNanos::from(2_000_000_000u64),
3071 )
3072 .unwrap();
3073
3074 assert_eq!(
3075 report.quantity.as_decimal(),
3076 Decimal::from_str_exact(expected_quantity).unwrap()
3077 );
3078 assert_eq!(
3079 report.price.map(|price| price.as_decimal()),
3080 Some(Decimal::from_str_exact(expected_price).unwrap())
3081 );
3082 }
3083
3084 #[rstest]
3085 fn test_dispatch_fok_buy_registers_share_quantity_for_in_flight_submit() {
3086 let order: PolymarketUserOrder = load("ws_user_order_fok_buy_pusd_size.json");
3087 let instrument = test_instrument();
3088
3089 let token_instruments = AtomicMap::new();
3090 token_instruments.insert(order.asset_id, instrument.clone());
3091
3092 let fill_tracker = OrderFillTrackerMap::new();
3094 let pending_submits = PendingSubmitTracker::default();
3095 let order_contexts = OrderContextRegistry::default();
3096 let emitter = test_emitter();
3097
3098 let venue_order_id = VenueOrderId::from(order.id.as_str());
3099 let client_order_id = ClientOrderId::from("O-FOK-IN-FLIGHT");
3100 pending_submits.insert(venue_order_id, client_order_id);
3101 register_context(
3102 &order_contexts,
3103 venue_order_id,
3104 instrument.id(),
3105 client_order_id.as_str(),
3106 );
3107
3108 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
3109 settlement.mark_live();
3110 settlement.note_order_submitted(venue_order_id);
3111
3112 let ctx = WsDispatchContext {
3113 signer_type: PolymarketSignerType::Owner,
3114 token_instruments: &token_instruments,
3115 fill_tracker: &fill_tracker,
3116 settlement: &settlement,
3117 pending_submits: &pending_submits,
3118 order_contexts: &order_contexts,
3119 emitter: &emitter,
3120 account_id: AccountId::from("POLY-001"),
3121 clock: nautilus_core::time::get_atomic_clock_realtime(),
3122 user_address: "0xtest",
3123 user_api_key: "test-key",
3124 };
3125 let mut state = WsDispatchState::default();
3126
3127 dispatch_user_message(&UserWsMessage::Order(order), &ctx, &mut state);
3128
3129 assert_eq!(
3131 fill_tracker
3132 .submitted_qty(&venue_order_id)
3133 .map(|qty| qty.as_decimal()),
3134 Some(dec!(101)),
3135 );
3136 }
3137
3138 #[rstest]
3139 fn test_dispatch_fok_buy_report_quantity_is_shares_without_identity() {
3140 let order: PolymarketUserOrder = load("ws_user_order_fok_buy_pusd_size.json");
3141 let instrument = test_instrument();
3142
3143 let token_instruments = AtomicMap::new();
3144 token_instruments.insert(order.asset_id, instrument.clone());
3145
3146 let fill_tracker = OrderFillTrackerMap::new();
3147 let venue_order_id = VenueOrderId::from(order.id.as_str());
3148 fill_tracker.register(
3149 venue_order_id,
3150 Quantity::from("101"),
3151 OrderSide::Buy,
3152 instrument.id(),
3153 instrument.size_precision(),
3154 instrument.price_precision(),
3155 );
3156
3157 let pending_submits = PendingSubmitTracker::default();
3158 let order_contexts = OrderContextRegistry::default();
3160 let mut emitter = test_emitter();
3161 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
3162 emitter.set_sender(sender);
3163
3164 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
3165 settlement.mark_live();
3166 settlement.note_order_submitted(venue_order_id);
3167
3168 let ctx = WsDispatchContext {
3169 signer_type: PolymarketSignerType::Owner,
3170 token_instruments: &token_instruments,
3171 fill_tracker: &fill_tracker,
3172 settlement: &settlement,
3173 pending_submits: &pending_submits,
3174 order_contexts: &order_contexts,
3175 emitter: &emitter,
3176 account_id: AccountId::from("POLY-001"),
3177 clock: nautilus_core::time::get_atomic_clock_realtime(),
3178 user_address: "0xtest",
3179 user_api_key: "test-key",
3180 };
3181 let mut state = WsDispatchState::default();
3182
3183 dispatch_user_message(&UserWsMessage::Order(order), &ctx, &mut state);
3184
3185 let event = receiver.try_recv().expect("expected order report");
3186 let ExecutionEvent::Report(ExecutionReport::Order(report)) = event else {
3187 panic!("expected an order report, was {event:?}");
3188 };
3189
3190 assert_eq!(report.venue_order_id, venue_order_id);
3191 assert_eq!(report.order_side, Some(OrderSide::Buy));
3192 assert_eq!(report.time_in_force, TimeInForce::Fok);
3193 assert_eq!(report.order_status, OrderStatus::Canceled);
3194 assert_eq!(report.quantity.as_decimal(), dec!(101));
3195 assert_eq!(report.filled_qty.as_decimal(), dec!(0));
3196 assert_eq!(
3197 report.price.map(|price| price.as_decimal()),
3198 Some(dec!(0.01))
3199 );
3200 }
3201
3202 #[rstest]
3203 fn test_admit_stream_taker_trade_builds_the_leg() {
3204 let trade: PolymarketUserTrade = load("ws_user_trade.json");
3205 let instrument = instrument_for_trade(&trade);
3206 let instrument_id = instrument.id();
3207 let token_instruments = AtomicMap::new();
3208 token_instruments.insert(trade.asset_id, instrument);
3209 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
3210
3211 let ctx = WsDispatchContext {
3212 signer_type: PolymarketSignerType::Owner,
3213 token_instruments: &token_instruments,
3214 fill_tracker: &OrderFillTrackerMap::new(),
3215 settlement: &settlement,
3216 pending_submits: &PendingSubmitTracker::default(),
3217 order_contexts: &OrderContextRegistry::default(),
3218 emitter: &test_emitter(),
3219 account_id: AccountId::from("POLY-001"),
3220 clock: nautilus_core::time::get_atomic_clock_realtime(),
3221 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
3222 user_api_key: "00000000-0000-0000-0000-000000000001",
3223 };
3224
3225 let admitted =
3226 admit_trade_evidence(TradeEvidence::Stream(&trade), &ctx.admission_context())
3227 .expect("owned taker trade admits");
3228
3229 assert_eq!(admitted.venue_trade_id, trade.id);
3230 assert_eq!(admitted.status, trade.status);
3231 assert!(admitted.has_economics());
3232 assert_eq!(admitted.legs.len(), 1);
3233 let leg = &admitted.legs[0];
3234 assert_eq!(
3235 leg.venue_order_id,
3236 VenueOrderId::from(trade.taker_order_id.as_str())
3237 );
3238 assert_eq!(leg.trade_id.as_str(), trade.id);
3239 assert_eq!(leg.instrument_id, instrument_id);
3240 assert_eq!(leg.order_side, OrderSide::Buy);
3241 assert_eq!(leg.liquidity_side, LiquiditySide::Taker);
3242 assert_eq!(leg.last_qty.as_decimal(), dec!(25));
3243 assert_eq!(leg.last_px.as_decimal(), dec!(0.5));
3244 assert_eq!(leg.ts_event, UnixNanos::from(1_704_067_200_000_000_000u64));
3246 }
3247
3248 fn admission_context_for(
3249 token_instruments: &AtomicMap<Ustr, InstrumentAny>,
3250 ) -> AdmissionContext<'_> {
3251 AdmissionContext {
3252 signer_type: PolymarketSignerType::Owner,
3253 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
3254 api_key: "00000000-0000-0000-0000-000000000001",
3255 pusd: get_pusd_currency(),
3256 instruments: token_instruments,
3257 fee_bases: None,
3258 }
3259 }
3260
3261 #[rstest]
3262 fn test_admit_stream_fill_without_venue_match_time_is_untimestamped() {
3263 let mut trade: PolymarketUserTrade = load("ws_user_trade.json");
3264 trade.match_time = String::new();
3265 trade.timestamp = "not-a-timestamp".to_string();
3266 let token_instruments = AtomicMap::new();
3267 token_instruments.insert(trade.asset_id, instrument_for_trade(&trade));
3268
3269 let result = admit_trade_evidence(
3270 TradeEvidence::Stream(&trade),
3271 &admission_context_for(&token_instruments),
3272 );
3273
3274 let Err(AdmissionError::Untimestamped(e)) = result else {
3275 panic!("expected untimestamped evidence, was {result:?}");
3276 };
3277
3278 assert_eq!(
3279 e.to_string(),
3280 format!(
3281 "trade {} has no valid venue match timestamp (match_time=)",
3282 trade.id
3283 )
3284 );
3285 }
3286
3287 #[rstest]
3288 fn test_admit_stream_failed_trade_without_match_time_carries_no_economics() {
3289 let mut trade: PolymarketUserTrade = load("ws_user_trade.json");
3290 trade.status = PolymarketTradeStatus::Failed;
3291 trade.match_time = String::new();
3292 trade.timestamp = "not-a-timestamp".to_string();
3293 let instrument = instrument_for_trade(&trade);
3294 let instrument_id = instrument.id();
3295 let token_instruments = AtomicMap::new();
3296 token_instruments.insert(trade.asset_id, instrument);
3297
3298 let admitted = admit_trade_evidence(
3299 TradeEvidence::Stream(&trade),
3300 &admission_context_for(&token_instruments),
3301 )
3302 .expect("failed evidence admits owned-leg identity");
3303
3304 assert!(!admitted.has_economics());
3305 assert_eq!(admitted.legs.len(), 1);
3306 let leg = &admitted.legs[0];
3307 assert_eq!(
3308 leg.venue_order_id,
3309 VenueOrderId::from(trade.taker_order_id.as_str())
3310 );
3311 assert_eq!(leg.instrument_id, instrument_id);
3312 assert_eq!(leg.last_qty.as_decimal(), dec!(0));
3313 assert_eq!(leg.last_px.as_decimal(), dec!(0));
3314 assert!(leg.commission.is_zero());
3315 assert_eq!(leg.ts_event, UnixNanos::default());
3316 }
3317
3318 #[rstest]
3319 fn test_admit_price_keeps_instrument_precision_for_stream_and_wire_precision_for_rest() {
3320 let trade: PolymarketUserTrade = load("ws_user_trade.json");
3321 let instrument = instrument_for_trade(&trade);
3322 let price_precision = instrument.price_precision();
3323 let token_instruments = AtomicMap::new();
3324 token_instruments.insert(trade.asset_id, instrument);
3325 let mut rest: PolymarketTradeReport =
3326 serde_json::from_str(include_str!("../../test_data/http_trade_report.json"))
3327 .expect("REST trade fixture");
3328 rest.id = trade.id.clone();
3329 rest.taker_order_id = trade.taker_order_id.clone();
3330 let ctx = admission_context_for(&token_instruments);
3331
3332 let stream = admit_trade_evidence(TradeEvidence::Stream(&trade), &ctx)
3333 .expect("stream evidence admits");
3334 let rest =
3335 admit_trade_evidence(TradeEvidence::Rest(&rest), &ctx).expect("REST evidence admits");
3336
3337 assert_eq!(price_precision, 4);
3338 assert_eq!(stream.legs[0].last_px.as_decimal(), dec!(0.5));
3339 assert_eq!(stream.legs[0].last_px.precision, price_precision);
3340 assert_eq!(rest.legs[0].last_px, stream.legs[0].last_px);
3341 assert_eq!(rest.legs[0].last_px.precision, 1);
3342 }
3343
3344 #[rstest]
3345 #[case::settlement_rounding("0.4200001023529231", "1.758621", Some(dec!(0.42)))]
3346 #[case::beyond_settlement_rounding("0.42001", "5", None)]
3347 fn test_admit_maker_price_snaps_settlement_rounding_on_both_paths(
3348 #[case] wire_price: &str,
3349 #[case] matched_amount: &str,
3350 #[case] snapped: Option<Decimal>,
3351 ) {
3352 let wire_price = Decimal::from_str_exact(wire_price).unwrap();
3353 let matched_amount = Decimal::from_str_exact(matched_amount).unwrap();
3354 let mut trade: PolymarketUserTrade = load("ws_user_trade.json");
3355 trade.trader_side = PolymarketLiquiditySide::Maker;
3356 trade.maker_orders[0].price = wire_price;
3357 trade.maker_orders[0].matched_amount = matched_amount;
3358 let mut rest: PolymarketTradeReport =
3359 serde_json::from_str(include_str!("../../test_data/http_trade_report.json"))
3360 .expect("REST trade fixture");
3361 rest.trader_side = PolymarketLiquiditySide::Maker;
3362 rest.maker_orders[0].price = wire_price;
3363 rest.maker_orders[0].matched_amount = matched_amount;
3364 let token_instruments = AtomicMap::new();
3365 token_instruments.insert(trade.asset_id, instrument_for_trade(&trade));
3366 let maker_address = trade.maker_orders[0].maker_address.clone();
3367 let ctx = AdmissionContext {
3368 signer_type: PolymarketSignerType::Owner,
3369 user_address: &maker_address,
3370 api_key: "ffffffff-ffff-ffff-ffff-ffffffffffff",
3371 pusd: get_pusd_currency(),
3372 instruments: &token_instruments,
3373 fee_bases: None,
3374 };
3375
3376 let stream = admit_trade_evidence(TradeEvidence::Stream(&trade), &ctx);
3377 let rest =
3378 admit_trade_evidence(TradeEvidence::Rest(&rest), &ctx).expect("REST evidence admits");
3379
3380 if let Some(snapped) = snapped {
3381 let stream = stream.expect("stream evidence admits");
3382 assert_eq!(stream.legs[0].last_px.as_decimal(), snapped);
3383 assert_eq!(stream.legs[0].last_px.precision, 4);
3384 assert_eq!(rest.legs[0].last_px, stream.legs[0].last_px);
3385 } else {
3386 let Err(AdmissionError::Invalid(e)) = stream else {
3387 panic!("expected invalid stream evidence, was {stream:?}");
3388 };
3389 assert!(
3390 format!("{e:#}").contains("not exactly representable"),
3391 "unexpected error: {e:#}"
3392 );
3393 assert_eq!(rest.legs[0].last_px.as_decimal(), wire_price);
3394 }
3395 }
3396
3397 #[rstest]
3398 #[case::taker_order_id(false, "invalid venue order ID")]
3399 #[case::maker_trade_id(true, "invalid trade ID source")]
3400 fn test_admit_stream_rejects_invalid_identifiers_without_panicking(
3401 #[case] maker: bool,
3402 #[case] expected_error: &str,
3403 ) {
3404 let mut trade: PolymarketUserTrade = load("ws_user_trade.json");
3405 let token_instruments = AtomicMap::new();
3406 token_instruments.insert(trade.asset_id, instrument_for_trade(&trade));
3407 let maker_address = trade.maker_orders[0].maker_address.clone();
3408
3409 let user_address = if maker {
3410 trade.trader_side = PolymarketLiquiditySide::Maker;
3411 trade.id = "trade-\u{e9}".to_string();
3412 maker_address.as_str()
3413 } else {
3414 trade.taker_order_id = "order-\u{e9}".to_string();
3415 "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266"
3416 };
3417
3418 let ctx = AdmissionContext {
3419 signer_type: PolymarketSignerType::Owner,
3420 user_address,
3421 api_key: "ffffffff-ffff-ffff-ffff-ffffffffffff",
3422 pusd: get_pusd_currency(),
3423 instruments: &token_instruments,
3424 fee_bases: None,
3425 };
3426
3427 let result = admit_trade_evidence(TradeEvidence::Stream(&trade), &ctx);
3428
3429 let Err(AdmissionError::Invalid(e)) = result else {
3430 panic!("expected invalid evidence, was {result:?}");
3431 };
3432
3433 assert!(
3434 format!("{e:#}").contains(expected_error),
3435 "unexpected error: {e:#}"
3436 );
3437 }
3438
3439 #[rstest]
3440 fn test_trade_fill_info_flattens_raw_trade() {
3441 let trade: PolymarketUserTrade = load("ws_user_trade.json");
3442
3443 let info = trade_fill_info(&trade).expect("info should be present");
3444
3445 assert_eq!(info.len(), 21);
3447 assert_eq!(info[&Ustr::from("id")], Ustr::from("trade-0xabcdef1234"));
3448 assert_eq!(info[&Ustr::from("fee_rate_bps")], Ustr::from("0"));
3449 assert_eq!(
3450 info[&Ustr::from("transaction_hash")],
3451 Ustr::from("0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab")
3452 );
3453 assert_eq!(info[&Ustr::from("bucket_index")], Ustr::from("1"));
3455 assert_eq!(info[&Ustr::from("size")], Ustr::from("25.0"));
3456 assert_eq!(
3457 info[&Ustr::from("taker_order_id")],
3458 Ustr::from("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12")
3459 );
3460 assert_eq!(info[&Ustr::from("type")], Ustr::from("TRADE"));
3462 let maker_orders = info[&Ustr::from("maker_orders")].as_str();
3464 assert!(maker_orders.starts_with('['));
3465 assert!(maker_orders.contains("order_id"));
3466
3467 let empty_hash_trade: PolymarketUserTrade = load("ws_user_trade_msg.json");
3468 let empty_hash_info =
3469 trade_fill_info(&empty_hash_trade).expect("empty hash info should be present");
3470 assert!(!empty_hash_info.contains_key(&Ustr::from("transaction_hash")));
3471 }
3472
3473 #[rstest]
3474 fn test_dispatch_order_message_buffers_when_not_accepted() {
3475 let order: PolymarketUserOrder = load("ws_user_order_placement.json");
3476 let instrument = test_instrument();
3477
3478 let token_instruments = AtomicMap::new();
3479 token_instruments.insert(order.asset_id, instrument);
3480
3481 let fill_tracker = OrderFillTrackerMap::new();
3482 let pending_submits = PendingSubmitTracker::default();
3483 let order_contexts = OrderContextRegistry::default();
3484 let emitter = test_emitter();
3485
3486 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
3487 settlement.mark_live();
3488
3489 let ctx = WsDispatchContext {
3490 signer_type: PolymarketSignerType::Owner,
3491 token_instruments: &token_instruments,
3492 fill_tracker: &fill_tracker,
3493 settlement: &settlement,
3494 pending_submits: &pending_submits,
3495 order_contexts: &order_contexts,
3496 emitter: &emitter,
3497 account_id: AccountId::from("POLY-001"),
3498 clock: nautilus_core::time::get_atomic_clock_realtime(),
3499 user_address: "0xtest",
3500 user_api_key: "test-key",
3501 };
3502 let mut state = WsDispatchState::default();
3503
3504 let result = dispatch_user_message(&UserWsMessage::Order(order.clone()), &ctx, &mut state);
3505 assert!(result.is_none());
3506
3507 let venue_order_id = VenueOrderId::from(order.id.as_str());
3509 assert!(fill_tracker.has_pending_report(&venue_order_id));
3510 }
3511
3512 #[rstest]
3513 fn test_dispatch_order_message_ignores_missing_lifecycle_fields() {
3514 let order: PolymarketUserOrder = load("ws_user_order_placement.json");
3515 let instrument = test_instrument();
3516 let token_instruments = AtomicMap::new();
3517 token_instruments.insert(order.asset_id, instrument);
3518 let fill_tracker = OrderFillTrackerMap::new();
3519 let pending_submits = PendingSubmitTracker::default();
3520 let order_contexts = OrderContextRegistry::default();
3521 let emitter = test_emitter();
3522 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
3523 settlement.mark_live();
3524
3525 let ctx = WsDispatchContext {
3526 signer_type: PolymarketSignerType::Owner,
3527 token_instruments: &token_instruments,
3528 fill_tracker: &fill_tracker,
3529 settlement: &settlement,
3530 pending_submits: &pending_submits,
3531 order_contexts: &order_contexts,
3532 emitter: &emitter,
3533 account_id: AccountId::from("POLY-001"),
3534 clock: nautilus_core::time::get_atomic_clock_realtime(),
3535 user_address: "0xtest",
3536 user_api_key: "test-key",
3537 };
3538 let venue_order_id = VenueOrderId::from(order.id.as_str());
3539
3540 let mut missing_status = order.clone();
3541 missing_status.status = None;
3542 dispatch_user_message(
3543 &UserWsMessage::Order(missing_status),
3544 &ctx,
3545 &mut WsDispatchState::default(),
3546 );
3547 assert!(!fill_tracker.has_pending_report(&venue_order_id));
3548
3549 let mut missing_order_type = order;
3550 missing_order_type.order_type = None;
3551 dispatch_user_message(
3552 &UserWsMessage::Order(missing_order_type),
3553 &ctx,
3554 &mut WsDispatchState::default(),
3555 );
3556 assert!(!fill_tracker.has_pending_report(&venue_order_id));
3557 }
3558
3559 #[rstest]
3560 fn test_dispatch_order_message_uses_pending_submit_client_order_id() {
3561 let order: PolymarketUserOrder = load("ws_user_order_placement.json");
3562 let instrument = test_instrument();
3563
3564 let token_instruments = AtomicMap::new();
3565 token_instruments.insert(order.asset_id, instrument);
3566
3567 let fill_tracker = OrderFillTrackerMap::new();
3568 let pending_submits = PendingSubmitTracker::default();
3569 let order_contexts = OrderContextRegistry::default();
3570 let mut emitter = test_emitter();
3571 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
3572 emitter.set_sender(sender);
3573
3574 let venue_order_id = VenueOrderId::from(order.id.as_str());
3575 let client_order_id = ClientOrderId::from("O-UNKNOWN-SUBMIT");
3576 pending_submits.insert(venue_order_id, client_order_id);
3577 register_context(
3578 &order_contexts,
3579 venue_order_id,
3580 test_instrument().id(),
3581 "O-UNKNOWN-SUBMIT",
3582 );
3583
3584 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
3585 settlement.mark_live();
3586 settlement.note_order_submitted(venue_order_id);
3587
3588 let ctx = WsDispatchContext {
3589 signer_type: PolymarketSignerType::Owner,
3590 token_instruments: &token_instruments,
3591 fill_tracker: &fill_tracker,
3592 settlement: &settlement,
3593 pending_submits: &pending_submits,
3594 order_contexts: &order_contexts,
3595 emitter: &emitter,
3596 account_id: AccountId::from("POLY-001"),
3597 clock: nautilus_core::time::get_atomic_clock_realtime(),
3598 user_address: "0xtest",
3599 user_api_key: "test-key",
3600 };
3601 let mut state = WsDispatchState::default();
3602
3603 let _ = dispatch_user_message(&UserWsMessage::Order(order), &ctx, &mut state);
3604
3605 let event = receiver.try_recv().expect("expected accepted event");
3607 match event {
3608 ExecutionEvent::Order(OrderEventAny::Accepted(accepted)) => {
3609 assert_eq!(accepted.client_order_id, client_order_id);
3610 }
3611 other => panic!("Expected accepted event, was {other:?}"),
3612 }
3613
3614 assert!(!fill_tracker.has_pending_report(&venue_order_id));
3615 }
3616
3617 #[rstest]
3618 #[case(PolymarketSignerType::Owner, 1)]
3619 #[case(PolymarketSignerType::Session, 0)]
3620 fn test_dispatch_maker_fill_owned_by_case_variant_address(
3621 #[case] signer_type: PolymarketSignerType,
3622 #[case] expected_fills: usize,
3623 ) {
3624 let mut trade: PolymarketUserTrade = load("ws_user_trade.json");
3625 trade.trader_side = PolymarketLiquiditySide::Maker;
3626 let configured_address = trade.maker_orders[0].maker_address.clone();
3627 let case_variant_address = configured_address
3628 .to_ascii_uppercase()
3629 .replacen("0X", "0x", 1);
3630 assert_ne!(case_variant_address, configured_address);
3631 trade.maker_orders[0].maker_address = case_variant_address;
3632 let foreign_api_key = "ffffffff-ffff-ffff-ffff-ffffffffffff";
3633 assert_ne!(trade.maker_orders[0].owner, foreign_api_key);
3634
3635 let venue_order_id = VenueOrderId::from(trade.maker_orders[0].order_id.as_str());
3636 let token_instruments = AtomicMap::new();
3637 token_instruments.insert(trade.maker_orders[0].asset_id, instrument_for_trade(&trade));
3638 let fill_tracker = OrderFillTrackerMap::new();
3639 let pending_submits = PendingSubmitTracker::default();
3640 let order_contexts = OrderContextRegistry::default();
3641 let emitter = test_emitter();
3642 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
3643 settlement.mark_live();
3644 settlement.note_order_submitted(venue_order_id);
3645
3646 let ctx = WsDispatchContext {
3647 signer_type,
3648 token_instruments: &token_instruments,
3649 fill_tracker: &fill_tracker,
3650 settlement: &settlement,
3651 pending_submits: &pending_submits,
3652 order_contexts: &order_contexts,
3653 emitter: &emitter,
3654 account_id: AccountId::from("POLY-001"),
3655 clock: nautilus_core::time::get_atomic_clock_realtime(),
3656 user_address: &configured_address,
3657 user_api_key: foreign_api_key,
3658 };
3659 let mut state = WsDispatchState::default();
3660
3661 let _ = dispatch_user_message(&UserWsMessage::Trade(trade), &ctx, &mut state);
3662
3663 let fills = fill_tracker.pending_fills_for(&venue_order_id);
3664 assert_eq!(fills.len(), expected_fills);
3665
3666 for fill in fills {
3667 assert_eq!(fill.venue_order_id, venue_order_id);
3668 }
3669 }
3670
3671 #[rstest]
3672 fn test_dispatch_maker_fill_with_settlement_rounded_price_applies_without_quarantine() {
3673 let mut trade: PolymarketUserTrade = load("ws_user_trade.json");
3674 trade.trader_side = PolymarketLiquiditySide::Maker;
3675 trade.status = PolymarketTradeStatus::Matched;
3676 trade.maker_orders[0].price = dec!(0.4200001023529231);
3677 trade.maker_orders[0].matched_amount = dec!(1.758621);
3678 let configured_address = trade.maker_orders[0].maker_address.clone();
3679
3680 let venue_order_id = VenueOrderId::from(trade.maker_orders[0].order_id.as_str());
3681 let token_instruments = AtomicMap::new();
3682 token_instruments.insert(trade.maker_orders[0].asset_id, instrument_for_trade(&trade));
3683 let fill_tracker = OrderFillTrackerMap::new();
3684 let pending_submits = PendingSubmitTracker::default();
3685 let order_contexts = OrderContextRegistry::default();
3686 let emitter = test_emitter();
3687 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
3688 settlement.mark_live();
3689 settlement.note_order_submitted(venue_order_id);
3690
3691 let ctx = WsDispatchContext {
3692 signer_type: PolymarketSignerType::Owner,
3693 token_instruments: &token_instruments,
3694 fill_tracker: &fill_tracker,
3695 settlement: &settlement,
3696 pending_submits: &pending_submits,
3697 order_contexts: &order_contexts,
3698 emitter: &emitter,
3699 account_id: AccountId::from("POLY-001"),
3700 clock: nautilus_core::time::get_atomic_clock_realtime(),
3701 user_address: &configured_address,
3702 user_api_key: "ffffffff-ffff-ffff-ffff-ffffffffffff",
3703 };
3704 let mut state = WsDispatchState::default();
3705 let trade_id = trade.id.clone();
3706
3707 let _ = dispatch_user_message(&UserWsMessage::Trade(trade), &ctx, &mut state);
3708
3709 let fills = fill_tracker.pending_fills_for(&venue_order_id);
3710 assert_eq!(fills.len(), 1);
3711 assert_eq!(fills[0].last_px, Price::from("0.4200"));
3712 assert_eq!(fills[0].last_qty, Quantity::from("1.758621"));
3713 assert_eq!(
3714 settlement_state(&settlement, &trade_id),
3715 Some(SettlementState::Provisional)
3716 );
3717 }
3718
3719 #[rstest]
3720 #[case(dec!(-1))]
3721 #[case(Decimal::MAX)]
3722 fn test_dispatch_maker_numeric_failure_quarantines_trade(#[case] invalid_amount: Decimal) {
3723 let mut trade: PolymarketUserTrade = load("ws_user_trade.json");
3724 trade.trader_side = PolymarketLiquiditySide::Maker;
3725 let configured_address = trade.maker_orders[0].maker_address.clone();
3726 let foreign_api_key = "ffffffff-ffff-ffff-ffff-ffffffffffff";
3727 assert_ne!(trade.maker_orders[0].owner, foreign_api_key);
3728
3729 let venue_order_id = VenueOrderId::from(trade.maker_orders[0].order_id.as_str());
3730 let token_instruments = AtomicMap::new();
3731 token_instruments.insert(trade.maker_orders[0].asset_id, instrument_for_trade(&trade));
3732 let fill_tracker = OrderFillTrackerMap::new();
3733 let pending_submits = PendingSubmitTracker::default();
3734 let order_contexts = OrderContextRegistry::default();
3735 let emitter = test_emitter();
3736 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
3737 settlement.mark_live();
3738 settlement.note_order_submitted(venue_order_id);
3739
3740 let ctx = WsDispatchContext {
3741 signer_type: PolymarketSignerType::Owner,
3742 token_instruments: &token_instruments,
3743 fill_tracker: &fill_tracker,
3744 settlement: &settlement,
3745 pending_submits: &pending_submits,
3746 order_contexts: &order_contexts,
3747 emitter: &emitter,
3748 account_id: AccountId::from("POLY-001"),
3749 clock: nautilus_core::time::get_atomic_clock_realtime(),
3750 user_address: &configured_address,
3751 user_api_key: foreign_api_key,
3752 };
3753
3754 let mut state = WsDispatchState::default();
3755
3756 let mut invalid_trade = trade.clone();
3757 invalid_trade.maker_orders[0].matched_amount = invalid_amount;
3758 dispatch_user_message(&UserWsMessage::Trade(invalid_trade), &ctx, &mut state);
3759 assert_eq!(fill_tracker.pending_fills_for(&venue_order_id).len(), 0);
3760 assert_eq!(
3761 settlement_state(&settlement, &trade.id),
3762 Some(SettlementState::Quarantined)
3763 );
3764
3765 dispatch_user_message(&UserWsMessage::Trade(trade.clone()), &ctx, &mut state);
3766
3767 assert!(
3768 fill_tracker.pending_fills_for(&venue_order_id).is_empty(),
3769 "invalid stream evidence quarantines; a later valid copy stays REST-gated"
3770 );
3771 assert_eq!(
3772 settlement_state(&settlement, &trade.id),
3773 Some(SettlementState::Quarantined)
3774 );
3775 }
3776
3777 #[rstest]
3778 fn test_dispatch_trade_dedup() {
3779 let trade: PolymarketUserTrade = load("ws_user_trade.json");
3780 let instrument = instrument_for_trade(&trade);
3781
3782 let token_instruments = AtomicMap::new();
3783 token_instruments.insert(trade.asset_id, instrument);
3784
3785 let fill_tracker = OrderFillTrackerMap::new();
3786 let pending_submits = PendingSubmitTracker::default();
3787 let order_contexts = OrderContextRegistry::default();
3788 let emitter = test_emitter();
3789
3790 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
3791 settlement.note_order_submitted(VenueOrderId::from(trade.taker_order_id.as_str()));
3792
3793 let ctx = WsDispatchContext {
3794 signer_type: PolymarketSignerType::Owner,
3795 token_instruments: &token_instruments,
3796 fill_tracker: &fill_tracker,
3797 settlement: &settlement,
3798 pending_submits: &pending_submits,
3799 order_contexts: &order_contexts,
3800 emitter: &emitter,
3801 account_id: AccountId::from("POLY-001"),
3802 clock: nautilus_core::time::get_atomic_clock_realtime(),
3803 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
3804 user_api_key: "00000000-0000-0000-0000-000000000001",
3805 };
3806
3807 let mut state = WsDispatchState::default();
3808
3809 let venue_order_id = VenueOrderId::from(trade.taker_order_id.as_str());
3810
3811 let _ = dispatch_user_message(&UserWsMessage::Trade(trade.clone()), &ctx, &mut state);
3813 assert_eq!(fill_tracker.pending_fills_for(&venue_order_id).len(), 1);
3814
3815 let _ = dispatch_user_message(&UserWsMessage::Trade(trade), &ctx, &mut state);
3817 assert_eq!(fill_tracker.pending_fills_for(&venue_order_id).len(), 1);
3818 }
3819
3820 #[rstest]
3821 fn test_dispatch_taker_commission_failure_quarantines_trade() {
3822 let trade: PolymarketUserTrade = load("ws_user_trade.json");
3823 let valid_instrument = instrument_for_trade(&trade);
3824 let mut invalid_instrument = valid_instrument.clone();
3825 set_taker_fee_rate(
3826 &mut invalid_instrument,
3827 Decimal::from_i128_with_scale(100_000_000_000_000_000_000_000_000i128, 0),
3828 );
3829
3830 let token_instruments = AtomicMap::new();
3831 token_instruments.insert(trade.asset_id, invalid_instrument);
3832 let fill_tracker = OrderFillTrackerMap::new();
3833 let venue_order_id = VenueOrderId::from(trade.taker_order_id.as_str());
3834 fill_tracker.register(
3835 venue_order_id,
3836 Quantity::from("100"),
3837 OrderSide::Buy,
3838 valid_instrument.id(),
3839 valid_instrument.size_precision(),
3840 valid_instrument.price_precision(),
3841 );
3842 let pending_submits = PendingSubmitTracker::default();
3843 let order_contexts = OrderContextRegistry::default();
3844 register_context(
3845 &order_contexts,
3846 venue_order_id,
3847 valid_instrument.id(),
3848 "O-COMMISSION-REPLAY",
3849 );
3850 order_contexts.mark_accepted(venue_order_id);
3851 let mut emitter = test_emitter();
3852 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
3853 emitter.set_sender(sender);
3854 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
3855 settlement.mark_live();
3856 settlement.note_order_submitted(venue_order_id);
3857
3858 let ctx = WsDispatchContext {
3859 signer_type: PolymarketSignerType::Owner,
3860 token_instruments: &token_instruments,
3861 fill_tracker: &fill_tracker,
3862 settlement: &settlement,
3863 pending_submits: &pending_submits,
3864 order_contexts: &order_contexts,
3865 emitter: &emitter,
3866 account_id: AccountId::from("POLY-001"),
3867 clock: nautilus_core::time::get_atomic_clock_realtime(),
3868 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
3869 user_api_key: "00000000-0000-0000-0000-000000000001",
3870 };
3871
3872 let mut state = WsDispatchState::default();
3873
3874 let failed = dispatch_user_message(&UserWsMessage::Trade(trade.clone()), &ctx, &mut state);
3875
3876 assert!(failed.is_none());
3877 assert_eq!(
3878 settlement_state(&settlement, &trade.id),
3879 Some(SettlementState::Quarantined)
3880 );
3881 assert_eq!(
3882 fill_tracker.get_cumulative_filled(&venue_order_id),
3883 Some(Quantity::zero(valid_instrument.size_precision()))
3884 );
3885 assert!(receiver.try_recv().is_err());
3886
3887 token_instruments.insert(trade.asset_id, valid_instrument);
3889 let replay = dispatch_user_message(&UserWsMessage::Trade(trade.clone()), &ctx, &mut state);
3890
3891 assert!(replay.is_none());
3892 assert_eq!(
3893 settlement_state(&settlement, &trade.id),
3894 Some(SettlementState::Quarantined)
3895 );
3896 assert!(receiver.try_recv().is_err());
3897 }
3898
3899 #[rstest]
3900 fn test_dispatch_untimestamped_trade_quarantines_and_requests_resolution() {
3901 let mut trade: PolymarketUserTrade = load("ws_user_trade.json");
3902 trade.status = PolymarketTradeStatus::Matched;
3903 trade.match_time = String::new();
3904 trade.timestamp = "not-a-timestamp".to_string();
3905 let token_instruments = AtomicMap::new();
3906 token_instruments.insert(trade.asset_id, instrument_for_trade(&trade));
3907 let fill_tracker = OrderFillTrackerMap::new();
3908 let pending_submits = PendingSubmitTracker::default();
3909 let order_contexts = OrderContextRegistry::default();
3910 let mut emitter = test_emitter();
3911 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
3912 emitter.set_sender(sender);
3913 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
3914 settlement.mark_live();
3915 settlement.note_order_submitted(VenueOrderId::from(trade.taker_order_id.as_str()));
3916
3917 let ctx = WsDispatchContext {
3918 signer_type: PolymarketSignerType::Owner,
3919 token_instruments: &token_instruments,
3920 fill_tracker: &fill_tracker,
3921 settlement: &settlement,
3922 pending_submits: &pending_submits,
3923 order_contexts: &order_contexts,
3924 emitter: &emitter,
3925 account_id: AccountId::from("POLY-001"),
3926 clock: nautilus_core::time::get_atomic_clock_realtime(),
3927 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
3928 user_api_key: "00000000-0000-0000-0000-000000000001",
3929 };
3930
3931 let mut state = WsDispatchState::default();
3932
3933 let refresh = dispatch_user_message(&UserWsMessage::Trade(trade.clone()), &ctx, &mut state);
3934
3935 assert!(refresh.is_none());
3936 assert_eq!(
3937 settlement_state(&settlement, &trade.id),
3938 Some(SettlementState::Quarantined)
3939 );
3940 assert_eq!(settlement.pending_resolutions(), vec![trade.id]);
3941 assert!(receiver.try_recv().is_err());
3942 }
3943
3944 #[rstest]
3945 fn test_dispatch_trade_replays_after_instrument_becomes_available() {
3946 let trade: PolymarketUserTrade = load("ws_user_trade.json");
3947 let instrument = instrument_for_trade(&trade);
3948 let token_instruments = AtomicMap::new();
3949 let fill_tracker = OrderFillTrackerMap::new();
3950 let pending_submits = PendingSubmitTracker::default();
3951 let order_contexts = OrderContextRegistry::default();
3952 let emitter = test_emitter();
3953 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
3954 settlement.note_order_submitted(VenueOrderId::from(trade.taker_order_id.as_str()));
3955
3956 let ctx = WsDispatchContext {
3957 signer_type: PolymarketSignerType::Owner,
3958 token_instruments: &token_instruments,
3959 fill_tracker: &fill_tracker,
3960 settlement: &settlement,
3961 pending_submits: &pending_submits,
3962 order_contexts: &order_contexts,
3963 emitter: &emitter,
3964 account_id: AccountId::from("POLY-001"),
3965 clock: nautilus_core::time::get_atomic_clock_realtime(),
3966 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
3967 user_api_key: "00000000-0000-0000-0000-000000000001",
3968 };
3969
3970 let mut state = WsDispatchState::default();
3971 let venue_order_id = VenueOrderId::from(trade.taker_order_id.as_str());
3972
3973 let first_result =
3974 dispatch_user_message(&UserWsMessage::Trade(trade.clone()), &ctx, &mut state);
3975 token_instruments.insert(trade.asset_id, instrument);
3976 let replay_result = dispatch_user_message(&UserWsMessage::Trade(trade), &ctx, &mut state);
3977
3978 assert!(first_result.is_none());
3979 assert!(replay_result.is_some());
3980 assert_eq!(fill_tracker.pending_fills_for(&venue_order_id).len(), 1);
3981 }
3982
3983 #[rstest]
3984 #[case(crate::common::enums::PolymarketTradeStatus::Mined)]
3985 #[case(crate::common::enums::PolymarketTradeStatus::Retrying)]
3986 fn test_dispatch_trade_applies_mined_and_retrying_provisionally(
3987 #[case] status: crate::common::enums::PolymarketTradeStatus,
3988 ) {
3989 let mut trade: PolymarketUserTrade = load("ws_user_trade.json");
3990 trade.status = status;
3991 let instrument = instrument_for_trade(&trade);
3992 let token_instruments = AtomicMap::new();
3993 token_instruments.insert(trade.asset_id, instrument);
3994 let fill_tracker = OrderFillTrackerMap::new();
3995 let pending_submits = PendingSubmitTracker::default();
3996 let order_contexts = OrderContextRegistry::default();
3997 let emitter = test_emitter();
3998 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
3999 settlement.note_order_submitted(VenueOrderId::from(trade.taker_order_id.as_str()));
4000
4001 let ctx = WsDispatchContext {
4002 signer_type: PolymarketSignerType::Owner,
4003 token_instruments: &token_instruments,
4004 fill_tracker: &fill_tracker,
4005 settlement: &settlement,
4006 pending_submits: &pending_submits,
4007 order_contexts: &order_contexts,
4008 emitter: &emitter,
4009 account_id: AccountId::from("POLY-001"),
4010 clock: nautilus_core::time::get_atomic_clock_realtime(),
4011 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
4012 user_api_key: "00000000-0000-0000-0000-000000000001",
4013 };
4014
4015 let mut state = WsDispatchState::default();
4016 let venue_order_id = VenueOrderId::from(trade.taker_order_id.as_str());
4017 let trade_id = trade.id.clone();
4018
4019 let result = dispatch_user_message(&UserWsMessage::Trade(trade), &ctx, &mut state);
4020
4021 assert!(result.is_none());
4022 assert_eq!(fill_tracker.pending_fills_for(&venue_order_id).len(), 1);
4023 assert_eq!(
4024 settlement_state(&settlement, &trade_id),
4025 Some(SettlementState::Provisional)
4026 );
4027 }
4028
4029 #[rstest]
4030 #[case::taker(false)]
4031 #[case::maker(true)]
4032 fn test_dispatch_matched_fill_confirmed_by_rest_at_wire_precision_does_not_conflict(
4033 #[case] maker: bool,
4034 ) {
4035 let mut trade: PolymarketUserTrade = load("ws_user_trade.json");
4036 trade.status = PolymarketTradeStatus::Matched;
4037
4038 if maker {
4039 trade.trader_side = PolymarketLiquiditySide::Maker;
4040 trade.side = PolymarketOrderSide::Sell;
4041 }
4042
4043 let (order_id, user_address, api_key) = if maker {
4044 let leg = &trade.maker_orders[0];
4045 (
4046 leg.order_id.as_str(),
4047 leg.maker_address.as_str(),
4048 leg.owner.as_str(),
4049 )
4050 } else {
4051 (
4052 trade.taker_order_id.as_str(),
4053 trade.maker_address.as_str(),
4054 trade.owner.as_str(),
4055 )
4056 };
4057
4058 let instrument = instrument_for_trade(&trade);
4059 let token_instruments = AtomicMap::new();
4060 token_instruments.insert(trade.asset_id, instrument.clone());
4061 let fill_tracker = OrderFillTrackerMap::new();
4062 let venue_order_id = VenueOrderId::from(order_id);
4063 fill_tracker.register(
4064 venue_order_id,
4065 Quantity::from("100"),
4066 OrderSide::Buy,
4067 instrument.id(),
4068 instrument.size_precision(),
4069 instrument.price_precision(),
4070 );
4071 let pending_submits = PendingSubmitTracker::default();
4072 let order_contexts = OrderContextRegistry::default();
4073 register_context(
4074 &order_contexts,
4075 venue_order_id,
4076 instrument.id(),
4077 "O-MATCHED-CONFIRMED",
4078 );
4079 order_contexts.mark_accepted(venue_order_id);
4080 let mut emitter = test_emitter();
4081 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
4082 emitter.set_sender(sender);
4083 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
4084 settlement.mark_live();
4085 settlement.note_order_submitted(venue_order_id);
4086
4087 let ctx = WsDispatchContext {
4088 signer_type: PolymarketSignerType::Owner,
4089 token_instruments: &token_instruments,
4090 fill_tracker: &fill_tracker,
4091 settlement: &settlement,
4092 pending_submits: &pending_submits,
4093 order_contexts: &order_contexts,
4094 emitter: &emitter,
4095 account_id: AccountId::from("POLY-001"),
4096 clock: nautilus_core::time::get_atomic_clock_realtime(),
4097 user_address,
4098 user_api_key: api_key,
4099 };
4100
4101 let mut state = WsDispatchState::default();
4102
4103 let _ = dispatch_user_message(&UserWsMessage::Trade(trade.clone()), &ctx, &mut state);
4104
4105 let filled = match receiver.try_recv().unwrap() {
4106 ExecutionEvent::Order(OrderEventAny::Filled(event)) => event,
4107 other => panic!("expected matched fill, was {other:?}"),
4108 };
4109
4110 settlement.observe_fill_applied(&filled);
4111
4112 let mut rest_confirmed: PolymarketTradeReport =
4113 serde_json::from_str(include_str!("../../test_data/http_trade_report.json"))
4114 .expect("REST trade fixture");
4115 rest_confirmed.id = trade.id.clone();
4116 rest_confirmed.taker_order_id = trade.taker_order_id.clone();
4117 rest_confirmed.trader_side = trade.trader_side;
4118 rest_confirmed.side = trade.side;
4119
4120 if maker {
4121 rest_confirmed.maker_orders[0].side = Some(PolymarketOrderSide::Buy);
4122 }
4123
4124 let rest_evidence = admit_trade_evidence(
4125 TradeEvidence::Rest(&rest_confirmed),
4126 &ctx.admission_context(),
4127 )
4128 .expect("REST CONFIRMED evidence admits");
4129 let rest_actions = settlement.admit_rest_result(&rest_evidence);
4130 execute_settlement_actions(
4131 rest_actions,
4132 rest_trade_info(&rest_confirmed).as_ref(),
4133 &ctx,
4134 &mut state,
4135 );
4136
4137 assert_eq!(rest_evidence.legs[0].last_px, filled.last_px);
4138 assert_eq!(rest_evidence.legs[0].venue_order_id, filled.venue_order_id);
4139 assert_eq!(rest_evidence.legs[0].trade_id, filled.trade_id);
4140 assert_eq!(rest_evidence.legs[0].instrument_id, filled.instrument_id);
4141 assert_eq!(rest_evidence.legs[0].order_side, OrderSide::Buy);
4142 assert_eq!(filled.order_side, OrderSide::Buy);
4143
4144 let liquidity = if maker {
4145 LiquiditySide::Maker
4146 } else {
4147 LiquiditySide::Taker
4148 };
4149
4150 assert_eq!(rest_evidence.legs[0].liquidity_side, liquidity);
4151 assert_eq!(filled.liquidity_side, liquidity);
4152 assert_eq!(rest_evidence.legs[0].last_qty, filled.last_qty);
4153 assert_eq!(rest_evidence.legs[0].commission, filled.commission.unwrap());
4154 assert_eq!(rest_evidence.legs[0].ts_event, filled.ts_event);
4155 assert_ne!(
4156 rest_evidence.legs[0].last_px.precision,
4157 filled.last_px.precision
4158 );
4159 assert!(trade_hard_fault(&settlement, &trade.id).is_none());
4160 assert_eq!(
4161 settlement_state(&settlement, &trade.id),
4162 Some(SettlementState::RestConfirmed)
4163 );
4164 assert!(receiver.try_recv().is_err());
4165 assert_eq!(
4166 fill_tracker.get_cumulative_filled(&venue_order_id),
4167 Some(filled.last_qty)
4168 );
4169 }
4170
4171 #[rstest]
4172 #[case::open_order(PolymarketOrderStatus::Live, None, true, false)]
4173 #[case::canceled_order(PolymarketOrderStatus::Canceled, None, true, false)]
4174 #[case::confirmed_fill(
4175 PolymarketOrderStatus::Matched,
4176 Some(PolymarketTradeStatus::Confirmed),
4177 true,
4178 true
4179 )]
4180 #[case::pending_trade(
4181 PolymarketOrderStatus::Matched,
4182 Some(PolymarketTradeStatus::Matched),
4183 false,
4184 false
4185 )]
4186 #[case::matched_before_trades_listed(PolymarketOrderStatus::Matched, None, false, false)]
4187 fn test_apply_uncertain_order_evidence(
4188 #[case] order_status: PolymarketOrderStatus,
4189 #[case] trade_status: Option<PolymarketTradeStatus>,
4190 #[case] expected_applied: bool,
4191 #[case] expected_fill: bool,
4192 #[values(false, true)] locally_closed: bool,
4193 #[values(false, true)] replacement_promoted: bool,
4194 ) {
4195 let mut order: PolymarketOpenOrder =
4196 serde_json::from_str(include_str!("../../test_data/http_open_order.json"))
4197 .expect("REST open order fixture");
4198 order.status = order_status;
4199 order.original_size = dec!(25);
4200
4201 order.size_matched = if order_status == PolymarketOrderStatus::Matched {
4202 dec!(25)
4203 } else {
4204 Decimal::ZERO
4205 };
4206
4207 let mut trade: PolymarketTradeReport =
4208 serde_json::from_str(include_str!("../../test_data/http_trade_report.json"))
4209 .expect("REST trade fixture");
4210 trade.status = trade_status.unwrap_or(PolymarketTradeStatus::Confirmed);
4211 let trades = trade_status
4212 .map(|_| vec![trade.clone()])
4213 .unwrap_or_default();
4214 let ws_trade: PolymarketUserTrade = load("ws_user_trade.json");
4215 let instrument = instrument_for_trade(&ws_trade);
4216 let token_instruments = AtomicMap::new();
4217 token_instruments.insert(order.asset_id, instrument.clone());
4218 let venue_order_id = VenueOrderId::from(order.id.as_str());
4219 let client_order_id = ClientOrderId::from("O-UNCERTAIN");
4220 let fill_tracker = OrderFillTrackerMap::new();
4221 let pending_submits = PendingSubmitTracker::default();
4222 pending_submits.insert(venue_order_id, client_order_id);
4223 let order_contexts = OrderContextRegistry::default();
4224 register_context(
4225 &order_contexts,
4226 venue_order_id,
4227 instrument.id(),
4228 client_order_id.as_str(),
4229 );
4230 order_contexts.set_closed(client_order_id, locally_closed);
4231 let mut emitter = test_emitter();
4232 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
4233 emitter.set_sender(sender);
4234 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
4235 settlement.begin_session();
4236
4237 let ctx = WsDispatchContext {
4238 signer_type: PolymarketSignerType::Owner,
4239 token_instruments: &token_instruments,
4240 fill_tracker: &fill_tracker,
4241 settlement: &settlement,
4242 pending_submits: &pending_submits,
4243 order_contexts: &order_contexts,
4244 emitter: &emitter,
4245 account_id: AccountId::from("POLY-001"),
4246 clock: nautilus_core::time::get_atomic_clock_realtime(),
4247 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
4248 user_api_key: "00000000-0000-0000-0000-000000000001",
4249 };
4250
4251 let mut state = WsDispatchState::default();
4252
4253 if replacement_promoted {
4254 let replacement_id = VenueOrderId::from("V-REPLACEMENT");
4255 assert!(state.begin_modify(client_order_id, venue_order_id, instrument.id()));
4256 assert!(state.set_modify_replacement(
4257 client_order_id,
4258 replacement_id,
4259 Quantity::from("25"),
4260 Quantity::from("25"),
4261 Quantity::from("0"),
4262 Price::from("0.6"),
4263 ));
4264 assert!(state.claim_modify_replacement(replacement_id).is_some());
4265 order_contexts.mark_accepted(venue_order_id);
4266 }
4267
4268 let applied =
4269 apply_uncertain_order_evidence(venue_order_id, &order, &trades, &ctx, &mut state);
4270
4271 let mut events = Vec::new();
4272
4273 while let Ok(event) = receiver.try_recv() {
4274 events.push(event);
4275 }
4276
4277 let accepted = events.iter().filter(|event| {
4278 matches!(event, ExecutionEvent::Order(OrderEventAny::Accepted(accepted))
4279 if accepted.client_order_id == client_order_id
4280 && accepted.venue_order_id == venue_order_id)
4281 });
4282
4283 let fills: Vec<&OrderFilled> = events
4284 .iter()
4285 .filter_map(|event| match event {
4286 ExecutionEvent::Order(OrderEventAny::Filled(filled)) => Some(filled),
4287 _ => None,
4288 })
4289 .collect();
4290
4291 assert_eq!(applied, expected_applied);
4292 assert_eq!(
4293 accepted.count(),
4294 usize::from(expected_applied && !locally_closed && !replacement_promoted)
4295 );
4296 assert_eq!(fill_tracker.contains(&venue_order_id), expected_applied);
4297
4298 if expected_fill {
4299 assert_eq!(fills.len(), 1);
4300 assert_eq!(fills[0].client_order_id, client_order_id);
4301 assert_eq!(fills[0].venue_order_id, venue_order_id);
4302 assert_eq!(fills[0].trade_id, TradeId::from(trade.id.as_str()));
4303 assert_eq!(fills[0].last_qty.as_decimal(), dec!(25));
4304 assert_eq!(fills[0].last_px.as_decimal(), dec!(0.5));
4305 assert_eq!(
4306 settlement_state(&settlement, &trade.id),
4307 Some(SettlementState::RestConfirmed)
4308 );
4309 } else {
4310 assert!(fills.is_empty());
4311 }
4312
4313 let canceled = events
4314 .iter()
4315 .filter(|event| {
4316 matches!(event, ExecutionEvent::Order(OrderEventAny::Canceled(canceled))
4317 if canceled.client_order_id == client_order_id
4318 && canceled.venue_order_id == Some(venue_order_id))
4319 })
4320 .count();
4321
4322 let expected_cancel = usize::from(
4323 order_status == PolymarketOrderStatus::Canceled
4324 && !locally_closed
4325 && !replacement_promoted,
4326 );
4327
4328 assert_eq!(canceled, expected_cancel);
4329 assert_eq!(
4330 events.len(),
4331 usize::from(expected_applied && !locally_closed && !replacement_promoted)
4332 + fills.len()
4333 + expected_cancel
4334 );
4335 }
4336
4337 #[rstest]
4338 fn test_apply_rest_trade_evidence_closes_ioc_remainder_on_confirmation() {
4339 let trade: PolymarketTradeReport =
4340 serde_json::from_str(include_str!("../../test_data/http_trade_report.json"))
4341 .expect("REST trade fixture");
4342 let ws_trade: PolymarketUserTrade = load("ws_user_trade.json");
4343 let instrument = instrument_for_trade(&ws_trade);
4344 let token_instruments = AtomicMap::new();
4345 token_instruments.insert(trade.asset_id, instrument.clone());
4346 let venue_order_id = VenueOrderId::from(trade.taker_order_id.as_str());
4347 let fill_tracker = OrderFillTrackerMap::new();
4348 fill_tracker.register(
4349 venue_order_id,
4350 Quantity::from("100.0000"),
4351 OrderSide::Buy,
4352 instrument.id(),
4353 instrument.size_precision(),
4354 instrument.price_precision(),
4355 );
4356 let pending_submits = PendingSubmitTracker::default();
4357 let order_contexts = OrderContextRegistry::default();
4358 register_context(&order_contexts, venue_order_id, instrument.id(), "O-IOC");
4359 let mut context = order_contexts
4360 .get(&venue_order_id)
4361 .expect("registered context");
4362 context.time_in_force = TimeInForce::Ioc;
4363 order_contexts.register_context(venue_order_id, context);
4364 order_contexts.mark_accepted(venue_order_id);
4365 let mut emitter = test_emitter();
4366 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
4367 emitter.set_sender(sender);
4368 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
4369 settlement.begin_session();
4370
4371 let ctx = WsDispatchContext {
4372 signer_type: PolymarketSignerType::Owner,
4373 token_instruments: &token_instruments,
4374 fill_tracker: &fill_tracker,
4375 settlement: &settlement,
4376 pending_submits: &pending_submits,
4377 order_contexts: &order_contexts,
4378 emitter: &emitter,
4379 account_id: AccountId::from("POLY-001"),
4380 clock: nautilus_core::time::get_atomic_clock_realtime(),
4381 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
4382 user_api_key: "00000000-0000-0000-0000-000000000001",
4383 };
4384 let mut state = WsDispatchState::default();
4385
4386 apply_rest_trade_evidence(&trade, &ctx, &mut state);
4387
4388 let filled = match receiver.try_recv().unwrap() {
4389 ExecutionEvent::Order(OrderEventAny::Filled(event)) => event,
4390 other => panic!("expected REST-confirmed fill, was {other:?}"),
4391 };
4392
4393 let canceled = match receiver.try_recv().unwrap() {
4394 ExecutionEvent::Order(OrderEventAny::Canceled(event)) => event,
4395 other => panic!("expected IOC remainder cancel, was {other:?}"),
4396 };
4397
4398 assert_eq!(filled.last_qty.as_decimal(), dec!(25));
4399 assert_eq!(filled.trade_id, TradeId::from(trade.id.as_str()));
4400 assert_eq!(canceled.client_order_id, ClientOrderId::from("O-IOC"));
4401 assert_eq!(canceled.venue_order_id, Some(venue_order_id));
4402 assert!(receiver.try_recv().is_err());
4403 assert_eq!(
4404 settlement_state(&settlement, &trade.id),
4405 Some(SettlementState::RestConfirmed)
4406 );
4407
4408 apply_rest_trade_evidence(&trade, &ctx, &mut state);
4409 assert!(receiver.try_recv().is_err());
4410 }
4411
4412 #[rstest]
4413 fn test_apply_rest_trade_evidence_normalizes_fok_dust_once() {
4414 let trade: PolymarketTradeReport =
4415 serde_json::from_str(include_str!("../../test_data/http_trade_report.json"))
4416 .expect("REST trade fixture");
4417 let ws_trade: PolymarketUserTrade = load("ws_user_trade.json");
4418 let instrument = instrument_for_trade(&ws_trade);
4419 let token_instruments = AtomicMap::new();
4420 token_instruments.insert(trade.asset_id, instrument.clone());
4421 let venue_order_id = VenueOrderId::from(trade.taker_order_id.as_str());
4422 let submitted_qty = Quantity::from("25.005000");
4423 let fill_tracker = OrderFillTrackerMap::new();
4424 fill_tracker.register(
4425 venue_order_id,
4426 submitted_qty,
4427 OrderSide::Buy,
4428 instrument.id(),
4429 instrument.size_precision(),
4430 instrument.price_precision(),
4431 );
4432 let pending_submits = PendingSubmitTracker::default();
4433 let order_contexts = OrderContextRegistry::default();
4434 register_context(&order_contexts, venue_order_id, instrument.id(), "O-FOK");
4435 let mut context = order_contexts
4436 .get(&venue_order_id)
4437 .expect("registered context");
4438 context.time_in_force = TimeInForce::Fok;
4439 order_contexts.register_context(venue_order_id, context);
4440 order_contexts.mark_accepted(venue_order_id);
4441 let mut emitter = test_emitter();
4442 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
4443 emitter.set_sender(sender);
4444 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
4445 settlement.begin_session();
4446
4447 let ctx = WsDispatchContext {
4448 signer_type: PolymarketSignerType::Owner,
4449 token_instruments: &token_instruments,
4450 fill_tracker: &fill_tracker,
4451 settlement: &settlement,
4452 pending_submits: &pending_submits,
4453 order_contexts: &order_contexts,
4454 emitter: &emitter,
4455 account_id: AccountId::from("POLY-001"),
4456 clock: nautilus_core::time::get_atomic_clock_realtime(),
4457 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
4458 user_api_key: "00000000-0000-0000-0000-000000000001",
4459 };
4460 let mut state = WsDispatchState::default();
4461
4462 apply_rest_trade_evidence(&trade, &ctx, &mut state);
4463
4464 let filled = match receiver.try_recv().unwrap() {
4465 ExecutionEvent::Order(OrderEventAny::Filled(event)) => event,
4466 other => panic!("expected REST-confirmed fill, was {other:?}"),
4467 };
4468 let updated = match receiver.try_recv().unwrap() {
4469 ExecutionEvent::Order(OrderEventAny::Updated(event)) => event,
4470 other => panic!("expected FOK quantity normalization, was {other:?}"),
4471 };
4472
4473 assert_eq!(filled.last_qty.as_decimal(), dec!(25));
4474 assert_eq!(updated.quantity, filled.last_qty);
4475 assert_eq!(updated.client_order_id, ClientOrderId::from("O-FOK"));
4476 assert!(receiver.try_recv().is_err());
4477
4478 apply_rest_trade_evidence(&trade, &ctx, &mut state);
4479 assert!(receiver.try_recv().is_err());
4480 assert_eq!(
4481 settlement_state(&settlement, &trade.id),
4482 Some(SettlementState::RestConfirmed)
4483 );
4484 assert_eq!(trade_hard_fault(&settlement, &trade.id), None);
4485 }
4486
4487 struct TerminalFaultHarness {
4488 trade: PolymarketUserTrade,
4489 token_instruments: AtomicMap<Ustr, InstrumentAny>,
4490 venue_order_id: VenueOrderId,
4491 fill_tracker: OrderFillTrackerMap,
4492 pending_submits: PendingSubmitTracker,
4493 order_contexts: OrderContextRegistry,
4494 emitter: ExecutionEventEmitter,
4495 receiver: tokio::sync::mpsc::UnboundedReceiver<ExecutionEvent>,
4496 settlement: SettlementRegistry,
4497 fill: OrderFilled,
4498 }
4499
4500 impl TerminalFaultHarness {
4501 fn new(time_in_force: TimeInForce, submitted_qty: &str, client_order_id: &str) -> Self {
4502 let mut trade: PolymarketUserTrade = load("ws_user_trade.json");
4503 trade.status = PolymarketTradeStatus::Matched;
4504 let mut instrument = instrument_for_trade(&trade);
4505 set_taker_fee_rate(&mut instrument, dec!(0.0721));
4506 let token_instruments = AtomicMap::new();
4507 token_instruments.insert(trade.asset_id, instrument.clone());
4508 let venue_order_id = VenueOrderId::from(trade.taker_order_id.as_str());
4509 let fill_tracker = OrderFillTrackerMap::new();
4510 fill_tracker.register(
4511 venue_order_id,
4512 Quantity::from(submitted_qty),
4513 OrderSide::Buy,
4514 instrument.id(),
4515 instrument.size_precision(),
4516 instrument.price_precision(),
4517 );
4518 let pending_submits = PendingSubmitTracker::default();
4519 pending_submits.insert(venue_order_id, ClientOrderId::from(client_order_id));
4520 let order_contexts = OrderContextRegistry::default();
4521 register_context(
4522 &order_contexts,
4523 venue_order_id,
4524 instrument.id(),
4525 client_order_id,
4526 );
4527 let mut context = order_contexts
4528 .get(&venue_order_id)
4529 .expect("registered context");
4530 context.time_in_force = time_in_force;
4531 order_contexts.register_context(venue_order_id, context);
4532 order_contexts.mark_accepted(venue_order_id);
4533 let mut emitter = test_emitter();
4534 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
4535 emitter.set_sender(sender);
4536 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
4537 settlement.begin_session();
4538 settlement.note_order_submitted(venue_order_id);
4539 let ctx = WsDispatchContext {
4540 signer_type: PolymarketSignerType::Owner,
4541 token_instruments: &token_instruments,
4542 fill_tracker: &fill_tracker,
4543 settlement: &settlement,
4544 pending_submits: &pending_submits,
4545 order_contexts: &order_contexts,
4546 emitter: &emitter,
4547 account_id: AccountId::from("POLY-001"),
4548 clock: nautilus_core::time::get_atomic_clock_realtime(),
4549 user_address: trade.maker_address.as_str(),
4550 user_api_key: trade.owner.as_str(),
4551 };
4552 let mut state = WsDispatchState::default();
4553 dispatch_user_message(&UserWsMessage::Trade(trade.clone()), &ctx, &mut state);
4554 let fill = match receiver.try_recv().expect("matched fill") {
4555 ExecutionEvent::Order(OrderEventAny::Filled(fill)) => fill,
4556 other => panic!("expected matched fill, was {other:?}"),
4557 };
4558 settlement.observe_fill_applied(&fill);
4559 assert!(
4560 receiver.try_recv().is_err(),
4561 "provisional match emitted terminal bookkeeping"
4562 );
4563
4564 Self {
4565 trade,
4566 token_instruments,
4567 venue_order_id,
4568 fill_tracker,
4569 pending_submits,
4570 order_contexts,
4571 emitter,
4572 receiver,
4573 settlement,
4574 fill,
4575 }
4576 }
4577
4578 fn ctx(&self) -> WsDispatchContext<'_> {
4579 WsDispatchContext {
4580 signer_type: PolymarketSignerType::Owner,
4581 token_instruments: &self.token_instruments,
4582 fill_tracker: &self.fill_tracker,
4583 settlement: &self.settlement,
4584 pending_submits: &self.pending_submits,
4585 order_contexts: &self.order_contexts,
4586 emitter: &self.emitter,
4587 account_id: AccountId::from("POLY-001"),
4588 clock: nautilus_core::time::get_atomic_clock_realtime(),
4589 user_address: self.trade.maker_address.as_str(),
4590 user_api_key: self.trade.owner.as_str(),
4591 }
4592 }
4593
4594 fn contradicting_rest(&self) -> PolymarketTradeReport {
4595 let mut rest: PolymarketTradeReport = load("http_trade_report.json");
4596 rest.id = self.trade.id.clone();
4597 rest.taker_order_id = self.trade.taker_order_id.clone();
4598 rest.asset_id = self.trade.asset_id;
4599 rest.price = dec!(0.6);
4600 rest.status = PolymarketTradeStatus::Confirmed;
4601 rest
4602 }
4603 }
4604
4605 fn assert_no_fault_bookkeeping(events: &[ExecutionEvent]) {
4606 assert!(
4607 events.iter().all(|event| !matches!(
4608 event,
4609 ExecutionEvent::Order(
4610 OrderEventAny::Updated(_)
4611 | OrderEventAny::Canceled(_)
4612 | OrderEventAny::Filled(_)
4613 | OrderEventAny::FillVoided(_)
4614 )
4615 )),
4616 "faulted trade authorized bookkeeping, was {events:?}"
4617 );
4618 }
4619
4620 fn assert_fault_retained(harness: &TerminalFaultHarness) {
4621 assert_eq!(harness.fill.last_qty, Quantity::from("25.000000"));
4622 assert_eq!(harness.fill.last_px.as_decimal(), dec!(0.5));
4623 assert_eq!(harness.fill.commission.unwrap().as_decimal(), dec!(0.45062));
4624 assert_eq!(
4625 settlement_state(&harness.settlement, &harness.trade.id),
4626 Some(SettlementState::RestConfirmed)
4627 );
4628 assert!(trade_hard_fault(&harness.settlement, &harness.trade.id).is_some());
4629 assert!(harness.settlement.is_trade_confirmed(&harness.trade.id));
4630 assert!(
4631 harness
4632 .settlement
4633 .ensure_resolved(None, "mass status")
4634 .is_err()
4635 );
4636 assert!(harness.fill_tracker.contains(&harness.venue_order_id));
4637 }
4638
4639 #[rstest]
4640 #[case::fok_dust(TimeInForce::Fok, "25.005000", "O-FAULT-FOK")]
4641 #[case::ioc_partial(TimeInForce::Ioc, "100.000000", "O-FAULT-IOC")]
4642 fn test_contradicting_rest_does_not_authorize_terminal_bookkeeping(
4643 #[case] time_in_force: TimeInForce,
4644 #[case] submitted_qty: &str,
4645 #[case] client_order_id: &str,
4646 ) {
4647 let mut harness = TerminalFaultHarness::new(time_in_force, submitted_qty, client_order_id);
4648 let rest = harness.contradicting_rest();
4649 let mut state = WsDispatchState::default();
4650 {
4651 let ctx = harness.ctx();
4652 apply_rest_trade_evidence(&rest, &ctx, &mut state);
4653 apply_rest_trade_evidence(&rest, &ctx, &mut state);
4654 }
4655
4656 let events: Vec<_> = std::iter::from_fn(|| harness.receiver.try_recv().ok()).collect();
4657 assert_no_fault_bookkeeping(&events);
4658 assert_fault_retained(&harness);
4659 }
4660
4661 #[rstest]
4662 #[case::fok_dust(TimeInForce::Fok, "25.005000", "O-UNCERTAIN-FOK", PolymarketOrderType::FOK, dec!(25.005))]
4663 #[case::ioc_partial(TimeInForce::Ioc, "100.000000", "O-UNCERTAIN-IOC", PolymarketOrderType::FAK, dec!(100))]
4664 fn test_uncertain_filled_recovery_does_not_bypass_fault_guard(
4665 #[case] time_in_force: TimeInForce,
4666 #[case] submitted_qty: &str,
4667 #[case] client_order_id: &str,
4668 #[case] order_type: PolymarketOrderType,
4669 #[case] original_size: Decimal,
4670 ) {
4671 let mut harness = TerminalFaultHarness::new(time_in_force, submitted_qty, client_order_id);
4672 let rest = harness.contradicting_rest();
4673 let mut order: PolymarketOpenOrder =
4674 serde_json::from_str(include_str!("../../test_data/http_open_order.json"))
4675 .expect("REST open order fixture");
4676 order.status = PolymarketOrderStatus::Matched;
4677 order.order_type = order_type;
4678 order.original_size = original_size;
4679 order.size_matched = dec!(25);
4680 let mut state = WsDispatchState::default();
4681 let applied = {
4682 let ctx = harness.ctx();
4683 apply_uncertain_order_evidence(
4684 harness.venue_order_id,
4685 &order,
4686 std::slice::from_ref(&rest),
4687 &ctx,
4688 &mut state,
4689 )
4690 };
4691
4692 let events: Vec<_> = std::iter::from_fn(|| harness.receiver.try_recv().ok()).collect();
4693 assert!(applied);
4694 assert_no_fault_bookkeeping(&events);
4695 assert_fault_retained(&harness);
4696 }
4697
4698 #[rstest]
4699 fn test_uncertain_venue_cancel_still_emits_after_hard_fault() {
4700 let mut harness =
4701 TerminalFaultHarness::new(TimeInForce::Fok, "25.005000", "O-VENUE-CANCEL");
4702 let rest = harness.contradicting_rest();
4703 let mut state = WsDispatchState::default();
4704 {
4705 let ctx = harness.ctx();
4706 apply_rest_trade_evidence(&rest, &ctx, &mut state);
4707 }
4708 let fault_events: Vec<_> =
4709 std::iter::from_fn(|| harness.receiver.try_recv().ok()).collect();
4710 assert_no_fault_bookkeeping(&fault_events);
4711
4712 let mut order: PolymarketOpenOrder =
4713 serde_json::from_str(include_str!("../../test_data/http_open_order.json"))
4714 .expect("REST open order fixture");
4715 order.status = PolymarketOrderStatus::Canceled;
4716 order.order_type = PolymarketOrderType::FAK;
4717 order.original_size = dec!(100);
4718 order.size_matched = dec!(25);
4719 let applied = {
4720 let ctx = harness.ctx();
4721 apply_uncertain_order_evidence(
4722 harness.venue_order_id,
4723 &order,
4724 std::slice::from_ref(&rest),
4725 &ctx,
4726 &mut state,
4727 )
4728 };
4729 let canceled = match harness.receiver.try_recv().expect("venue cancel") {
4730 ExecutionEvent::Order(OrderEventAny::Canceled(event)) => event,
4731 other => panic!("expected venue cancel, was {other:?}"),
4732 };
4733
4734 assert!(applied);
4735 assert_eq!(
4736 canceled.client_order_id,
4737 ClientOrderId::from("O-VENUE-CANCEL")
4738 );
4739 assert_eq!(canceled.venue_order_id, Some(harness.venue_order_id));
4740 assert!(harness.receiver.try_recv().is_err());
4741 assert_fault_retained(&harness);
4742 }
4743
4744 #[rstest]
4745 fn test_apply_uncertain_order_evidence_normalizes_filled_fok_dust() {
4746 let mut order: PolymarketOpenOrder =
4747 serde_json::from_str(include_str!("../../test_data/http_open_order.json"))
4748 .expect("REST open order fixture");
4749 order.status = PolymarketOrderStatus::Matched;
4750 order.original_size = dec!(25.005);
4751 order.size_matched = dec!(25);
4752 let trade: PolymarketTradeReport =
4753 serde_json::from_str(include_str!("../../test_data/http_trade_report.json"))
4754 .expect("REST trade fixture");
4755 let ws_trade: PolymarketUserTrade = load("ws_user_trade.json");
4756 let instrument = instrument_for_trade(&ws_trade);
4757 let token_instruments = AtomicMap::new();
4758 token_instruments.insert(order.asset_id, instrument.clone());
4759 let venue_order_id = VenueOrderId::from(order.id.as_str());
4760 let client_order_id = ClientOrderId::from("O-FOK");
4761 let fill_tracker = OrderFillTrackerMap::new();
4762 let pending_submits = PendingSubmitTracker::default();
4763 pending_submits.insert(venue_order_id, client_order_id);
4764 let order_contexts = OrderContextRegistry::default();
4765 register_context(
4766 &order_contexts,
4767 venue_order_id,
4768 instrument.id(),
4769 client_order_id.as_str(),
4770 );
4771 let mut context = order_contexts
4772 .get(&venue_order_id)
4773 .expect("registered context");
4774 context.time_in_force = TimeInForce::Fok;
4775 order_contexts.register_context(venue_order_id, context);
4776 let mut emitter = test_emitter();
4777 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
4778 emitter.set_sender(sender);
4779 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
4780 settlement.begin_session();
4781
4782 let ctx = WsDispatchContext {
4783 signer_type: PolymarketSignerType::Owner,
4784 token_instruments: &token_instruments,
4785 fill_tracker: &fill_tracker,
4786 settlement: &settlement,
4787 pending_submits: &pending_submits,
4788 order_contexts: &order_contexts,
4789 emitter: &emitter,
4790 account_id: AccountId::from("POLY-001"),
4791 clock: nautilus_core::time::get_atomic_clock_realtime(),
4792 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
4793 user_api_key: "00000000-0000-0000-0000-000000000001",
4794 };
4795 let mut state = WsDispatchState::default();
4796
4797 let applied = apply_uncertain_order_evidence(
4798 venue_order_id,
4799 &order,
4800 std::slice::from_ref(&trade),
4801 &ctx,
4802 &mut state,
4803 );
4804
4805 let mut events = Vec::new();
4806
4807 while let Ok(ExecutionEvent::Order(event)) = receiver.try_recv() {
4808 events.push(event);
4809 }
4810
4811 assert!(applied);
4812 assert_eq!(events.len(), 3, "unexpected events: {events:?}");
4813 assert!(matches!(&events[0], OrderEventAny::Accepted(_)));
4814
4815 let OrderEventAny::Filled(filled) = &events[1] else {
4816 panic!("expected fill, was {:?}", events[1]);
4817 };
4818
4819 let OrderEventAny::Updated(updated) = &events[2] else {
4820 panic!("expected terminal quantity update, was {:?}", events[2]);
4821 };
4822
4823 assert_eq!(filled.last_qty.as_decimal(), dec!(25));
4824 assert_eq!(updated.client_order_id, client_order_id);
4825 assert_eq!(updated.quantity.as_decimal(), dec!(25));
4826
4827 let repeated = apply_uncertain_order_evidence(
4828 venue_order_id,
4829 &order,
4830 std::slice::from_ref(&trade),
4831 &ctx,
4832 &mut state,
4833 );
4834 assert!(repeated);
4835 assert!(receiver.try_recv().is_err());
4836 }
4837
4838 #[rstest]
4839 #[case::identical_repeat(dec!(25), dec!(50))]
4840 #[case::contradicting_repeat(dec!(10), dec!(25))]
4841 fn test_apply_uncertain_order_evidence_deduplicates_repeated_rows(
4842 #[case] repeat_size: Decimal,
4843 #[case] size_matched: Decimal,
4844 ) {
4845 let mut order: PolymarketOpenOrder =
4846 serde_json::from_str(include_str!("../../test_data/http_open_order.json"))
4847 .expect("REST open order fixture");
4848 order.status = PolymarketOrderStatus::Matched;
4849 order.original_size = size_matched;
4850 order.size_matched = size_matched;
4851 let trade: PolymarketTradeReport =
4852 serde_json::from_str(include_str!("../../test_data/http_trade_report.json"))
4853 .expect("REST trade fixture");
4854 let mut repeat = trade.clone();
4855 repeat.size = repeat_size;
4856 let ws_trade: PolymarketUserTrade = load("ws_user_trade.json");
4857 let instrument = instrument_for_trade(&ws_trade);
4858 let token_instruments = AtomicMap::new();
4859 token_instruments.insert(order.asset_id, instrument.clone());
4860 let venue_order_id = VenueOrderId::from(order.id.as_str());
4861 let fill_tracker = OrderFillTrackerMap::new();
4862 let pending_submits = PendingSubmitTracker::default();
4863 pending_submits.insert(venue_order_id, ClientOrderId::from("O-UNCERTAIN"));
4864 let order_contexts = OrderContextRegistry::default();
4865 register_context(
4866 &order_contexts,
4867 venue_order_id,
4868 instrument.id(),
4869 "O-UNCERTAIN",
4870 );
4871 let mut emitter = test_emitter();
4872 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
4873 emitter.set_sender(sender);
4874 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
4875 settlement.begin_session();
4876
4877 let ctx = WsDispatchContext {
4878 signer_type: PolymarketSignerType::Owner,
4879 token_instruments: &token_instruments,
4880 fill_tracker: &fill_tracker,
4881 settlement: &settlement,
4882 pending_submits: &pending_submits,
4883 order_contexts: &order_contexts,
4884 emitter: &emitter,
4885 account_id: AccountId::from("POLY-001"),
4886 clock: nautilus_core::time::get_atomic_clock_realtime(),
4887 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
4888 user_api_key: "00000000-0000-0000-0000-000000000001",
4889 };
4890 let mut state = WsDispatchState::default();
4891
4892 let applied = apply_uncertain_order_evidence(
4893 venue_order_id,
4894 &order,
4895 &[trade, repeat],
4896 &ctx,
4897 &mut state,
4898 );
4899
4900 assert!(!applied);
4901 assert!(!fill_tracker.contains(&venue_order_id));
4902 assert!(receiver.try_recv().is_err());
4903 }
4904
4905 #[rstest]
4906 #[case::other_venue_order_id(Some("0xother"), PolymarketOrderSide::Buy)]
4907 #[case::contradicting_side(None, PolymarketOrderSide::Sell)]
4908 fn test_apply_uncertain_order_evidence_rejects_contradicting_row(
4909 #[case] row_id: Option<&str>,
4910 #[case] row_side: PolymarketOrderSide,
4911 ) {
4912 let mut order: PolymarketOpenOrder =
4913 serde_json::from_str(include_str!("../../test_data/http_open_order.json"))
4914 .expect("REST open order fixture");
4915 let venue_order_id = VenueOrderId::from(order.id.as_str());
4916
4917 if let Some(row_id) = row_id {
4918 order.id = row_id.to_string();
4919 }
4920
4921 order.side = row_side;
4922 let ws_trade: PolymarketUserTrade = load("ws_user_trade.json");
4923 let instrument = instrument_for_trade(&ws_trade);
4924 let token_instruments = AtomicMap::new();
4925 token_instruments.insert(order.asset_id, instrument.clone());
4926 let fill_tracker = OrderFillTrackerMap::new();
4927 let pending_submits = PendingSubmitTracker::default();
4928 pending_submits.insert(venue_order_id, ClientOrderId::from("O-UNCERTAIN"));
4929 let order_contexts = OrderContextRegistry::default();
4930 register_context(
4931 &order_contexts,
4932 venue_order_id,
4933 instrument.id(),
4934 "O-UNCERTAIN",
4935 );
4936 let mut emitter = test_emitter();
4937 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
4938 emitter.set_sender(sender);
4939 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
4940
4941 let ctx = WsDispatchContext {
4942 signer_type: PolymarketSignerType::Owner,
4943 token_instruments: &token_instruments,
4944 fill_tracker: &fill_tracker,
4945 settlement: &settlement,
4946 pending_submits: &pending_submits,
4947 order_contexts: &order_contexts,
4948 emitter: &emitter,
4949 account_id: AccountId::from("POLY-001"),
4950 clock: nautilus_core::time::get_atomic_clock_realtime(),
4951 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
4952 user_api_key: "00000000-0000-0000-0000-000000000001",
4953 };
4954 let mut state = WsDispatchState::default();
4955
4956 let applied = apply_uncertain_order_evidence(venue_order_id, &order, &[], &ctx, &mut state);
4957
4958 assert!(!applied);
4959 assert!(!fill_tracker.contains(&venue_order_id));
4960 assert!(receiver.try_recv().is_err());
4961 }
4962
4963 #[rstest]
4964 fn test_apply_stream_gap_order_evidence_applies_only_unseen_trades() {
4965 let mut order: PolymarketOpenOrder =
4966 serde_json::from_str(include_str!("../../test_data/http_open_order.json"))
4967 .expect("REST open order fixture");
4968 order.status = PolymarketOrderStatus::Canceled;
4969 order.original_size = dec!(100);
4970 order.size_matched = dec!(50);
4971 let seen: PolymarketTradeReport =
4972 serde_json::from_str(include_str!("../../test_data/http_trade_report.json"))
4973 .expect("REST trade fixture");
4974 let mut unseen = seen.clone();
4975 unseen.id = "trade-0xunseen".to_string();
4976 let trades = vec![seen.clone(), unseen.clone()];
4977 let ws_trade: PolymarketUserTrade = load("ws_user_trade.json");
4978 let instrument = instrument_for_trade(&ws_trade);
4979 let token_instruments = AtomicMap::new();
4980 token_instruments.insert(order.asset_id, instrument.clone());
4981 let venue_order_id = VenueOrderId::from(order.id.as_str());
4982 let client_order_id = ClientOrderId::from("O-STREAM-GAP");
4983 let fill_tracker = OrderFillTrackerMap::new();
4984 fill_tracker.register(
4985 venue_order_id,
4986 Quantity::from("100.0000"),
4987 OrderSide::Buy,
4988 instrument.id(),
4989 instrument.size_precision(),
4990 instrument.price_precision(),
4991 );
4992 let pending_submits = PendingSubmitTracker::default();
4993 pending_submits.insert(venue_order_id, client_order_id);
4994 let order_contexts = OrderContextRegistry::default();
4995 register_context(
4996 &order_contexts,
4997 venue_order_id,
4998 instrument.id(),
4999 client_order_id.as_str(),
5000 );
5001 order_contexts.mark_accepted(venue_order_id);
5002 let mut emitter = test_emitter();
5003 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
5004 emitter.set_sender(sender);
5005 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
5006 settlement.begin_session();
5007
5008 settlement.hydrate_fill(&OrderFilled::new(
5010 TraderId::from("TESTER-001"),
5011 StrategyId::from("S-001"),
5012 instrument.id(),
5013 client_order_id,
5014 venue_order_id,
5015 AccountId::from("POLY-001"),
5016 TradeId::from(seen.id.as_str()),
5017 OrderSide::Buy,
5018 OrderType::Limit,
5019 Quantity::from("24.9900"),
5020 Price::from("0.5000"),
5021 Currency::pUSD(),
5022 LiquiditySide::Taker,
5023 UUID4::new(),
5024 UnixNanos::from(1_u64),
5025 UnixNanos::from(1_u64),
5026 false,
5027 None,
5028 None,
5029 None,
5030 ));
5031
5032 let ctx = WsDispatchContext {
5033 signer_type: PolymarketSignerType::Owner,
5034 token_instruments: &token_instruments,
5035 fill_tracker: &fill_tracker,
5036 settlement: &settlement,
5037 pending_submits: &pending_submits,
5038 order_contexts: &order_contexts,
5039 emitter: &emitter,
5040 account_id: AccountId::from("POLY-001"),
5041 clock: nautilus_core::time::get_atomic_clock_realtime(),
5042 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
5043 user_api_key: "00000000-0000-0000-0000-000000000001",
5044 };
5045
5046 let mut state = WsDispatchState::default();
5047
5048 let applied =
5049 apply_stream_gap_order_evidence(venue_order_id, &order, &trades, &ctx, &mut state);
5050
5051 let mut events = Vec::new();
5052
5053 while let Ok(event) = receiver.try_recv() {
5054 events.push(event);
5055 }
5056
5057 let ExecutionEvent::Order(OrderEventAny::Filled(filled)) = &events[0] else {
5058 panic!("expected the unseen trade's fill, was {:?}", events[0]);
5059 };
5060
5061 assert!(applied);
5062 assert_eq!(events.len(), 1);
5063 assert_eq!(filled.client_order_id, client_order_id);
5064 assert_eq!(filled.venue_order_id, venue_order_id);
5065 assert_eq!(filled.trade_id, TradeId::from(unseen.id.as_str()));
5066 assert_eq!(filled.last_qty.as_decimal(), dec!(25));
5067 assert_eq!(filled.last_px.as_decimal(), dec!(0.5));
5068 assert_eq!(
5069 settlement_state(&settlement, &unseen.id),
5070 Some(SettlementState::RestConfirmed)
5071 );
5072 assert_eq!(
5073 settlement_state(&settlement, &seen.id),
5074 Some(SettlementState::Provisional)
5075 );
5076 assert!(trade_hard_fault(&settlement, &seen.id).is_none());
5077 }
5078
5079 #[rstest]
5080 fn test_dispatch_matched_trade_emits_fill_and_failed_trade_quarantines_then_rest_voids_it() {
5081 let mut trade: PolymarketUserTrade = load("ws_user_trade.json");
5082 trade.status = crate::common::enums::PolymarketTradeStatus::Matched;
5083 let instrument = instrument_for_trade(&trade);
5084 let token_instruments = AtomicMap::new();
5085 token_instruments.insert(trade.asset_id, instrument.clone());
5086 let fill_tracker = OrderFillTrackerMap::new();
5087 let venue_order_id = VenueOrderId::from(trade.taker_order_id.as_str());
5088 fill_tracker.register(
5089 venue_order_id,
5090 Quantity::from("100"),
5091 OrderSide::Buy,
5092 instrument.id(),
5093 instrument.size_precision(),
5094 instrument.price_precision(),
5095 );
5096 let pending_submits = PendingSubmitTracker::default();
5097 let order_contexts = OrderContextRegistry::default();
5098 register_context(
5099 &order_contexts,
5100 venue_order_id,
5101 instrument.id(),
5102 "O-MATCHED-FAILED",
5103 );
5104 order_contexts.mark_accepted(venue_order_id);
5105 let mut emitter = test_emitter();
5106 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
5107 emitter.set_sender(sender);
5108 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
5109 settlement.mark_live();
5110 settlement.note_order_submitted(venue_order_id);
5111
5112 let ctx = WsDispatchContext {
5113 signer_type: PolymarketSignerType::Owner,
5114 token_instruments: &token_instruments,
5115 fill_tracker: &fill_tracker,
5116 settlement: &settlement,
5117 pending_submits: &pending_submits,
5118 order_contexts: &order_contexts,
5119 emitter: &emitter,
5120 account_id: AccountId::from("POLY-001"),
5121 clock: nautilus_core::time::get_atomic_clock_realtime(),
5122 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
5123 user_api_key: "00000000-0000-0000-0000-000000000001",
5124 };
5125 let mut state = WsDispatchState::default();
5126
5127 let matched = dispatch_user_message(&UserWsMessage::Trade(trade.clone()), &ctx, &mut state);
5129 let filled = match receiver.try_recv().unwrap() {
5130 ExecutionEvent::Order(OrderEventAny::Filled(event)) => event,
5131 other => panic!("expected matched fill, was {other:?}"),
5132 };
5133
5134 assert!(matched.is_none());
5135 assert_eq!(
5136 settlement_state(&settlement, &trade.id),
5137 Some(SettlementState::Provisional)
5138 );
5139
5140 settlement.observe_fill_applied(&filled);
5142
5143 trade.status = crate::common::enums::PolymarketTradeStatus::Failed;
5145 let failed = dispatch_user_message(&UserWsMessage::Trade(trade.clone()), &ctx, &mut state);
5146 assert!(failed.is_none());
5147 assert_eq!(
5148 settlement_state(&settlement, &trade.id),
5149 Some(SettlementState::Quarantined)
5150 );
5151 assert!(receiver.try_recv().is_err());
5152 assert_eq!(
5153 fill_tracker.get_cumulative_filled(&venue_order_id),
5154 Some(filled.last_qty)
5155 );
5156
5157 let mut rest_failed: PolymarketTradeReport =
5159 serde_json::from_str(include_str!("../../test_data/http_trade_report.json"))
5160 .expect("REST trade fixture");
5161 rest_failed.id = trade.id.clone();
5162 rest_failed.taker_order_id = trade.taker_order_id.clone();
5163 rest_failed.status = PolymarketTradeStatus::Failed;
5164 rest_failed.trader_side = PolymarketLiquiditySide::Taker;
5165 let rest_evidence =
5166 admit_trade_evidence(TradeEvidence::Rest(&rest_failed), &ctx.admission_context())
5167 .expect("REST FAILED evidence admits");
5168 let rest_actions = settlement.admit_rest_result(&rest_evidence);
5169 execute_settlement_actions(
5170 rest_actions,
5171 rest_trade_info(&rest_failed).as_ref(),
5172 &ctx,
5173 &mut state,
5174 );
5175
5176 let voided = match receiver.try_recv().unwrap() {
5177 ExecutionEvent::Order(OrderEventAny::FillVoided(event)) => event,
5178 other => panic!("expected failed fill correction, was {other:?}"),
5179 };
5180
5181 assert!(settlement.take_account_refresh());
5182 assert!(trade_hard_fault(&settlement, &trade.id).is_none());
5183 assert_eq!(
5184 settlement_state(&settlement, &trade.id),
5185 Some(SettlementState::RestFailed)
5186 );
5187 assert_eq!(voided.trade_id, filled.trade_id);
5188 assert_eq!(voided.voided_qty, filled.last_qty);
5189 assert_eq!(voided.commission_voided, filled.commission);
5190 assert_eq!(voided.last_px, filled.last_px);
5191 assert!(!voided.is_reopened);
5192 assert_eq!(voided.causation_id, Some(filled.event_id));
5193 assert_eq!(
5194 fill_tracker.get_cumulative_filled(&venue_order_id),
5195 Some(Quantity::zero(instrument.size_precision()))
5196 );
5197
5198 trade.status = PolymarketTradeStatus::Matched;
5200 let stale_trade_id = trade.id.clone();
5201 let matched_after_failure =
5202 dispatch_user_message(&UserWsMessage::Trade(trade), &ctx, &mut state);
5203 assert!(matched_after_failure.is_none());
5204 assert_eq!(
5205 settlement_state(&settlement, &stale_trade_id),
5206 Some(SettlementState::RestFailed)
5207 );
5208 assert!(receiver.try_recv().is_err());
5209 }
5210
5211 #[rstest]
5212 fn test_dispatch_trade_uses_pending_submit_client_order_id() {
5213 let trade: PolymarketUserTrade = load("ws_user_trade.json");
5214 let instrument = instrument_for_trade(&trade);
5215
5216 let token_instruments = AtomicMap::new();
5217 token_instruments.insert(trade.asset_id, instrument);
5218
5219 let fill_tracker = OrderFillTrackerMap::new();
5220 let pending_submits = PendingSubmitTracker::default();
5221 let order_contexts = OrderContextRegistry::default();
5222 let emitter = test_emitter();
5223
5224 let venue_order_id = VenueOrderId::from(trade.taker_order_id.as_str());
5225 let client_order_id = ClientOrderId::from("O-UNKNOWN-FILL");
5226 pending_submits.insert(venue_order_id, client_order_id);
5227
5228 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
5229 settlement.mark_live();
5230 settlement.note_order_submitted(venue_order_id);
5231
5232 let ctx = WsDispatchContext {
5233 signer_type: PolymarketSignerType::Owner,
5234 token_instruments: &token_instruments,
5235 fill_tracker: &fill_tracker,
5236 settlement: &settlement,
5237 pending_submits: &pending_submits,
5238 order_contexts: &order_contexts,
5239 emitter: &emitter,
5240 account_id: AccountId::from("POLY-001"),
5241 clock: nautilus_core::time::get_atomic_clock_realtime(),
5242 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
5243 user_api_key: "00000000-0000-0000-0000-000000000001",
5244 };
5245 let mut state = WsDispatchState::default();
5246
5247 let _ = dispatch_user_message(&UserWsMessage::Trade(trade), &ctx, &mut state);
5248
5249 let fills = fill_tracker.pending_fills_for(&venue_order_id);
5250 assert_eq!(fills[0].client_order_id, Some(client_order_id));
5251 }
5252
5253 #[rstest]
5254 fn test_dispatch_late_fill_stays_tracked_after_later_registrations() {
5255 let trade: PolymarketUserTrade = load("ws_user_trade.json");
5256 let market: GammaMarket = load("gamma_market_sports_market_money_line.json");
5257 let defs = parse_gamma_market(&market).unwrap();
5258 let instrument = bind_instrument(
5259 create_instrument_from_def(&defs[0], UnixNanos::from(1_000_000_000u64)).unwrap(),
5260 trade.market.as_str(),
5261 trade.outcome,
5262 );
5263 let venue_order_id = VenueOrderId::from(trade.taker_order_id.as_str());
5264
5265 let token_instruments = AtomicMap::new();
5266 token_instruments.insert(trade.asset_id, instrument.clone());
5267
5268 let fill_tracker = OrderFillTrackerMap::new();
5269 fill_tracker.register(
5270 venue_order_id,
5271 Quantity::from("100"),
5272 OrderSide::Buy,
5273 instrument.id(),
5274 instrument.size_precision(),
5275 instrument.price_precision(),
5276 );
5277
5278 let pending_submits = PendingSubmitTracker::default();
5279 let order_contexts = OrderContextRegistry::default();
5280 register_context(
5281 &order_contexts,
5282 venue_order_id,
5283 instrument.id(),
5284 "O-LATE-FILL",
5285 );
5286 order_contexts.mark_accepted(venue_order_id);
5287 assert!(order_contexts.get(&venue_order_id).is_some());
5288
5289 for index in 0..10_000 {
5290 let later_venue_order_id = VenueOrderId::from(format!("V-LATER-{index}").as_str());
5291 let later_client_order_id = format!("O-LATER-{index}");
5292 register_context(
5293 &order_contexts,
5294 later_venue_order_id,
5295 instrument.id(),
5296 &later_client_order_id,
5297 );
5298 order_contexts.mark_accepted(later_venue_order_id);
5299 fill_tracker.register(
5300 later_venue_order_id,
5301 Quantity::from("1"),
5302 OrderSide::Sell,
5303 instrument.id(),
5304 instrument.size_precision(),
5305 instrument.price_precision(),
5306 );
5307 }
5308 assert!(order_contexts.get(&venue_order_id).is_some());
5309 assert!(fill_tracker.contains(&venue_order_id));
5310
5311 let mut emitter = test_emitter();
5312 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
5313 emitter.set_sender(sender);
5314
5315 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
5316 settlement.mark_live();
5317 settlement.note_order_submitted(venue_order_id);
5318
5319 let ctx = WsDispatchContext {
5320 signer_type: PolymarketSignerType::Owner,
5321 token_instruments: &token_instruments,
5322 fill_tracker: &fill_tracker,
5323 settlement: &settlement,
5324 pending_submits: &pending_submits,
5325 order_contexts: &order_contexts,
5326 emitter: &emitter,
5327 account_id: AccountId::from("POLY-001"),
5328 clock: nautilus_core::time::get_atomic_clock_realtime(),
5329 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
5330 user_api_key: "00000000-0000-0000-0000-000000000001",
5331 };
5332 let mut state = WsDispatchState::default();
5333
5334 dispatch_user_message(&UserWsMessage::Trade(trade.clone()), &ctx, &mut state);
5335
5336 let event = receiver.try_recv().expect("expected tracked late fill");
5337 let ExecutionEvent::Order(OrderEventAny::Filled(filled)) = event else {
5338 panic!("expected tracked OrderFilled after later registrations, was {event:?}");
5339 };
5340
5341 assert_eq!(filled.client_order_id, ClientOrderId::from("O-LATE-FILL"));
5342 assert_eq!(filled.venue_order_id, venue_order_id);
5343 assert_eq!(filled.trade_id, TradeId::from(trade.id.as_str()));
5344 assert_eq!(filled.instrument_id, instrument.id());
5345 assert_eq!(
5346 filled.last_qty.as_decimal(),
5347 Decimal::from_str_exact(&trade.size).unwrap()
5348 );
5349 assert_eq!(
5350 filled.last_px.as_decimal(),
5351 Decimal::from_str_exact(&trade.price).unwrap()
5352 );
5353 assert_eq!(filled.order_side, OrderSide::Buy);
5354 assert_eq!(filled.liquidity_side, LiquiditySide::Taker);
5355 let commission = filled.commission.expect("tracked fill has commission");
5356 assert_eq!(commission.as_decimal(), dec!(0.1875));
5357 assert_eq!(commission.currency, Currency::pUSD());
5358 assert!(receiver.try_recv().is_err());
5359 }
5360
5361 #[rstest]
5362 fn test_dispatch_order_matched_caps_filled_qty_when_no_trades_tracked() {
5363 let order: PolymarketUserOrder = load("ws_user_order_matched.json");
5364 let instrument = test_instrument();
5365
5366 let token_instruments = AtomicMap::new();
5367 token_instruments.insert(order.asset_id, instrument.clone());
5368
5369 let fill_tracker = OrderFillTrackerMap::new();
5370 let venue_order_id = VenueOrderId::from(order.id.as_str());
5371
5372 fill_tracker.register(
5374 venue_order_id,
5375 Quantity::from("100"),
5376 OrderSide::Buy,
5377 instrument.id(),
5378 instrument.size_precision(),
5379 instrument.price_precision(),
5380 );
5381
5382 let pending_submits = PendingSubmitTracker::default();
5383 let order_contexts = OrderContextRegistry::default();
5386 let mut emitter = test_emitter();
5387 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
5388 emitter.set_sender(sender);
5389
5390 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
5391 settlement.mark_live();
5392 settlement.note_order_submitted(venue_order_id);
5393
5394 let ctx = WsDispatchContext {
5395 signer_type: PolymarketSignerType::Owner,
5396 token_instruments: &token_instruments,
5397 fill_tracker: &fill_tracker,
5398 settlement: &settlement,
5399 pending_submits: &pending_submits,
5400 order_contexts: &order_contexts,
5401 emitter: &emitter,
5402 account_id: AccountId::from("POLY-001"),
5403 clock: nautilus_core::time::get_atomic_clock_realtime(),
5404 user_address: "0xtest",
5405 user_api_key: "test-key",
5406 };
5407 let mut state = WsDispatchState::default();
5408
5409 dispatch_user_message(&UserWsMessage::Order(order), &ctx, &mut state);
5410
5411 let event = receiver.try_recv().expect("Expected report");
5412 match event {
5413 ExecutionEvent::Report(report) => match report {
5414 ExecutionReport::Order(order_report) => {
5415 assert_eq!(order_report.filled_qty, Quantity::from("0"));
5416 }
5417 other => panic!("Expected order report, was {other:?}"),
5418 },
5419 other => panic!("Expected report event, was {other:?}"),
5420 }
5421 }
5422
5423 #[rstest]
5424 fn test_dispatch_order_matched_uses_tracked_fills_for_filled_qty() {
5425 let order: PolymarketUserOrder = load("ws_user_order_matched.json");
5426 let instrument = test_instrument();
5427
5428 let token_instruments = AtomicMap::new();
5429 token_instruments.insert(order.asset_id, instrument.clone());
5430
5431 let fill_tracker = OrderFillTrackerMap::new();
5432 let venue_order_id = VenueOrderId::from(order.id.as_str());
5433
5434 fill_tracker.register(
5436 venue_order_id,
5437 Quantity::from("100"),
5438 OrderSide::Buy,
5439 instrument.id(),
5440 instrument.size_precision(),
5441 instrument.price_precision(),
5442 );
5443 fill_tracker.record_fill(&venue_order_id, Quantity::new(50.0, 6));
5444
5445 let pending_submits = PendingSubmitTracker::default();
5446 let order_contexts = OrderContextRegistry::default();
5449 let mut emitter = test_emitter();
5450 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
5451 emitter.set_sender(sender);
5452
5453 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
5454 settlement.mark_live();
5455 settlement.note_order_submitted(venue_order_id);
5456
5457 let ctx = WsDispatchContext {
5458 signer_type: PolymarketSignerType::Owner,
5459 token_instruments: &token_instruments,
5460 fill_tracker: &fill_tracker,
5461 settlement: &settlement,
5462 pending_submits: &pending_submits,
5463 order_contexts: &order_contexts,
5464 emitter: &emitter,
5465 account_id: AccountId::from("POLY-001"),
5466 clock: nautilus_core::time::get_atomic_clock_realtime(),
5467 user_address: "0xtest",
5468 user_api_key: "test-key",
5469 };
5470 let mut state = WsDispatchState::default();
5471
5472 dispatch_user_message(&UserWsMessage::Order(order), &ctx, &mut state);
5473
5474 let event = receiver.try_recv().expect("Expected report");
5475 match event {
5476 ExecutionEvent::Report(report) => match report {
5477 ExecutionReport::Order(order_report) => {
5478 assert_eq!(order_report.filled_qty, Quantity::from("50"));
5479 }
5480 other => panic!("Expected order report, was {other:?}"),
5481 },
5482 other => panic!("Expected report event, was {other:?}"),
5483 }
5484 }
5485
5486 #[rstest]
5487 fn test_dispatch_order_matched_normalizes_quantity_without_fill() {
5488 let order: PolymarketUserOrder = load("ws_user_order_matched.json");
5489 let instrument = test_instrument();
5490
5491 let token_instruments = AtomicMap::new();
5492 token_instruments.insert(order.asset_id, instrument.clone());
5493
5494 let fill_tracker = OrderFillTrackerMap::new();
5495 let venue_order_id = VenueOrderId::from(order.id.as_str());
5496 fill_tracker.register(
5497 venue_order_id,
5498 Quantity::from("100"),
5499 OrderSide::Buy,
5500 instrument.id(),
5501 instrument.size_precision(),
5502 instrument.price_precision(),
5503 );
5504 fill_tracker.record_fill(&venue_order_id, Quantity::new(99.995, 6));
5505
5506 let pending_submits = PendingSubmitTracker::default();
5507 let order_contexts = OrderContextRegistry::default();
5508 register_context(
5509 &order_contexts,
5510 venue_order_id,
5511 instrument.id(),
5512 "O-MATCHED",
5513 );
5514 order_contexts.mark_accepted(venue_order_id);
5515 let mut emitter = test_emitter();
5516 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
5517 emitter.set_sender(sender);
5518
5519 let clock = Box::leak(Box::new(AtomicTime::new(
5520 false,
5521 UnixNanos::from(2_000_000_000u64),
5522 )));
5523
5524 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
5525 settlement.mark_live();
5526 settlement.note_order_submitted(venue_order_id);
5527
5528 let ctx = WsDispatchContext {
5529 signer_type: PolymarketSignerType::Owner,
5530 token_instruments: &token_instruments,
5531 fill_tracker: &fill_tracker,
5532 settlement: &settlement,
5533 pending_submits: &pending_submits,
5534 order_contexts: &order_contexts,
5535 emitter: &emitter,
5536 account_id: AccountId::from("POLY-001"),
5537 clock,
5538 user_address: "0xtest",
5539 user_api_key: "test-key",
5540 };
5541 let mut state = WsDispatchState::default();
5542
5543 let _ = settlement.admit_rest_result(&AdmittedTrade {
5544 venue_trade_id: "trade-0xfill1".to_string(),
5545 status: PolymarketTradeStatus::Confirmed,
5546 legs: Vec::new(),
5547 });
5548
5549 dispatch_user_message(&UserWsMessage::Order(order), &ctx, &mut state);
5550
5551 let event = receiver.try_recv().expect("expected quantity update");
5552 match event {
5553 ExecutionEvent::Order(OrderEventAny::Updated(updated)) => {
5554 assert_eq!(
5555 updated.ts_event,
5556 UnixNanos::from(1_703_875_201_000_000_000u64)
5557 );
5558 assert_eq!(updated.ts_init, UnixNanos::from(2_000_000_000u64));
5559 assert_eq!(updated.quantity, Quantity::new(99.995, 6));
5560 assert!(updated.reconciliation);
5561 }
5562 other => panic!("expected updated event, was {other:?}"),
5563 }
5564 assert!(receiver.try_recv().is_err());
5565 }
5566
5567 #[rstest]
5568 fn test_confirmed_trade_normalizes_pending_matched_quantity() {
5569 let mut order: PolymarketUserOrder = load("ws_user_order_matched.json");
5570 let mut trade: PolymarketUserTrade = load("ws_user_trade.json");
5571 let instrument = instrument_for_trade(&trade);
5572 order.associate_trades = Some(vec![trade.id.clone()]);
5573 trade.size = "99.995".to_string();
5574 trade.price = order.price.clone();
5575
5576 let token_instruments = AtomicMap::new();
5577 token_instruments.insert(order.asset_id, instrument.clone());
5578 let fill_tracker = OrderFillTrackerMap::new();
5579 let venue_order_id = VenueOrderId::from(order.id.as_str());
5580 fill_tracker.register(
5581 venue_order_id,
5582 Quantity::from("100"),
5583 OrderSide::Buy,
5584 instrument.id(),
5585 instrument.size_precision(),
5586 instrument.price_precision(),
5587 );
5588 let pending_submits = PendingSubmitTracker::default();
5589 let order_contexts = OrderContextRegistry::default();
5590 register_context(
5591 &order_contexts,
5592 venue_order_id,
5593 instrument.id(),
5594 "O-CONFIRMED-DUST",
5595 );
5596 order_contexts.mark_accepted(venue_order_id);
5597 let mut emitter = test_emitter();
5598 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
5599 emitter.set_sender(sender);
5600 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
5601 settlement.mark_live();
5602 settlement.note_order_submitted(venue_order_id);
5603
5604 let ctx = WsDispatchContext {
5605 signer_type: PolymarketSignerType::Owner,
5606 token_instruments: &token_instruments,
5607 fill_tracker: &fill_tracker,
5608 settlement: &settlement,
5609 pending_submits: &pending_submits,
5610 order_contexts: &order_contexts,
5611 emitter: &emitter,
5612 account_id: AccountId::from("POLY-001"),
5613 clock: nautilus_core::time::get_atomic_clock_realtime(),
5614 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
5615 user_api_key: "00000000-0000-0000-0000-000000000001",
5616 };
5617 let mut state = WsDispatchState::default();
5618
5619 dispatch_user_message(&UserWsMessage::Order(order), &ctx, &mut state);
5620 assert!(receiver.try_recv().is_err());
5621
5622 dispatch_user_message(&UserWsMessage::Trade(trade), &ctx, &mut state);
5623
5624 let real_fill = receiver.try_recv().expect("expected confirmed venue fill");
5625 let normalized = receiver
5626 .try_recv()
5627 .expect("expected quantity normalization");
5628
5629 match (real_fill, normalized) {
5630 (
5631 ExecutionEvent::Order(OrderEventAny::Filled(real)),
5632 ExecutionEvent::Order(OrderEventAny::Updated(updated)),
5633 ) => {
5634 assert_eq!(real.last_qty, Quantity::from("99.995"));
5635 assert_eq!(updated.quantity, Quantity::from("99.995"));
5636 assert!(updated.reconciliation);
5637 }
5638 other => panic!("expected fill then quantity update, was {other:?}"),
5639 }
5640 assert!(receiver.try_recv().is_err());
5641 }
5642
5643 #[rstest]
5644 fn test_cancel_reemitted_after_fill_for_canceled_order() {
5645 let cancel_order: PolymarketUserOrder = load("ws_user_order_cancellation.json");
5646 let trade: PolymarketUserTrade = load("ws_user_trade.json");
5647 let instrument = instrument_for_trade(&trade);
5648
5649 let token_instruments = AtomicMap::new();
5650 token_instruments.insert(cancel_order.asset_id, instrument.clone());
5651
5652 let fill_tracker = OrderFillTrackerMap::new();
5653 let venue_order_id = VenueOrderId::from(cancel_order.id.as_str());
5654
5655 fill_tracker.register(
5657 venue_order_id,
5658 Quantity::from("100"),
5659 OrderSide::Buy,
5660 instrument.id(),
5661 instrument.size_precision(),
5662 instrument.price_precision(),
5663 );
5664
5665 let pending_submits = PendingSubmitTracker::default();
5666 let order_contexts = OrderContextRegistry::default();
5667 register_context(&order_contexts, venue_order_id, instrument.id(), "O-CANCEL");
5668 order_contexts.mark_accepted(venue_order_id);
5669 let mut emitter = test_emitter();
5670 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
5671 emitter.set_sender(sender);
5672
5673 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
5674 settlement.mark_live();
5675 settlement.note_order_submitted(venue_order_id);
5676
5677 let ctx = WsDispatchContext {
5678 signer_type: PolymarketSignerType::Owner,
5679 token_instruments: &token_instruments,
5680 fill_tracker: &fill_tracker,
5681 settlement: &settlement,
5682 pending_submits: &pending_submits,
5683 order_contexts: &order_contexts,
5684 emitter: &emitter,
5685 account_id: AccountId::from("POLY-001"),
5686 clock: nautilus_core::time::get_atomic_clock_realtime(),
5687 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
5688 user_api_key: "00000000-0000-0000-0000-000000000001",
5689 };
5690 let mut state = WsDispatchState::default();
5691
5692 dispatch_user_message(&UserWsMessage::Order(cancel_order), &ctx, &mut state);
5694 let cancel_event = receiver.try_recv().expect("Expected canceled event");
5695 match &cancel_event {
5696 ExecutionEvent::Order(OrderEventAny::Canceled(c)) => {
5697 assert_eq!(c.venue_order_id, Some(venue_order_id));
5698 }
5699 other => panic!("Expected canceled event, was {other:?}"),
5700 }
5701
5702 dispatch_user_message(&UserWsMessage::Trade(trade), &ctx, &mut state);
5704
5705 let fill_event = receiver.try_recv().expect("Expected filled event");
5707 match &fill_event {
5708 ExecutionEvent::Order(OrderEventAny::Filled(f)) => {
5709 assert_eq!(f.venue_order_id, venue_order_id);
5710 }
5711 other => panic!("Expected filled event, was {other:?}"),
5712 }
5713
5714 let reemitted_cancel = receiver
5715 .try_recv()
5716 .expect("Expected re-emitted canceled event");
5717
5718 match &reemitted_cancel {
5719 ExecutionEvent::Order(OrderEventAny::Canceled(c)) => {
5720 assert_eq!(c.venue_order_id, Some(venue_order_id));
5721 }
5722 other => panic!("Expected canceled event, was {other:?}"),
5723 }
5724 }
5725
5726 #[rstest]
5727 fn test_gtd_expiry_reemitted_after_late_fill() {
5728 let mut cancel_order: PolymarketUserOrder = load("ws_user_order_cancellation.json");
5729 cancel_order.order_type = Some(PolymarketOrderType::GTD);
5730 cancel_order.expiration = Some("1703875265".to_string());
5731 let trade: PolymarketUserTrade = load("ws_user_trade.json");
5732 let instrument = instrument_for_trade(&trade);
5733 let token_instruments = AtomicMap::new();
5734 token_instruments.insert(cancel_order.asset_id, instrument.clone());
5735 let fill_tracker = OrderFillTrackerMap::new();
5736 let venue_order_id = VenueOrderId::from(cancel_order.id.as_str());
5737 fill_tracker.register(
5738 venue_order_id,
5739 Quantity::from("100"),
5740 OrderSide::Buy,
5741 instrument.id(),
5742 instrument.size_precision(),
5743 instrument.price_precision(),
5744 );
5745 let pending_submits = PendingSubmitTracker::default();
5746 let order_contexts = OrderContextRegistry::default();
5747 register_context(
5748 &order_contexts,
5749 venue_order_id,
5750 instrument.id(),
5751 "O-GTD-EXPIRE",
5752 );
5753 order_contexts.mark_accepted(venue_order_id);
5754 let mut emitter = test_emitter();
5755 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
5756 emitter.set_sender(sender);
5757 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
5758 settlement.mark_live();
5759 settlement.note_order_submitted(venue_order_id);
5760
5761 let ctx = WsDispatchContext {
5762 signer_type: PolymarketSignerType::Owner,
5763 token_instruments: &token_instruments,
5764 fill_tracker: &fill_tracker,
5765 settlement: &settlement,
5766 pending_submits: &pending_submits,
5767 order_contexts: &order_contexts,
5768 emitter: &emitter,
5769 account_id: AccountId::from("POLY-001"),
5770 clock: nautilus_core::time::get_atomic_clock_realtime(),
5771 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
5772 user_api_key: "00000000-0000-0000-0000-000000000001",
5773 };
5774
5775 let mut state = WsDispatchState::default();
5776
5777 dispatch_user_message(&UserWsMessage::Order(cancel_order), &ctx, &mut state);
5778
5779 match receiver.try_recv().expect("expected expired event") {
5780 ExecutionEvent::Order(OrderEventAny::Expired(event)) => {
5781 assert_eq!(event.venue_order_id, Some(venue_order_id));
5782 }
5783 other => panic!("expected OrderExpired, was {other:?}"),
5784 }
5785
5786 dispatch_user_message(&UserWsMessage::Trade(trade), &ctx, &mut state);
5787
5788 match receiver.try_recv().expect("expected fill") {
5789 ExecutionEvent::Order(OrderEventAny::Filled(event)) => {
5790 assert_eq!(event.venue_order_id, venue_order_id);
5791 }
5792 other => panic!("expected fill, was {other:?}"),
5793 }
5794
5795 match receiver.try_recv().expect("expected re-emitted expiry") {
5796 ExecutionEvent::Order(OrderEventAny::Expired(event)) => {
5797 assert_eq!(event.venue_order_id, Some(venue_order_id));
5798 }
5799 other => panic!("expected re-emitted OrderExpired, was {other:?}"),
5800 }
5801 }
5802
5803 #[rstest]
5804 fn test_modified_old_leg_suppresses_gtd_expiry() {
5805 let mut cancel_order: PolymarketUserOrder = load("ws_user_order_cancellation.json");
5806 cancel_order.order_type = Some(PolymarketOrderType::GTD);
5807 cancel_order.expiration = Some("1703875265".to_string());
5808 let trade: PolymarketUserTrade = load("ws_user_trade.json");
5809 let instrument = instrument_for_trade(&trade);
5810 let token_instruments = AtomicMap::new();
5811 token_instruments.insert(cancel_order.asset_id, instrument.clone());
5812 let fill_tracker = OrderFillTrackerMap::new();
5813 let old_venue_order_id = VenueOrderId::from(cancel_order.id.as_str());
5814 fill_tracker.register(
5815 old_venue_order_id,
5816 Quantity::from("100"),
5817 OrderSide::Buy,
5818 instrument.id(),
5819 instrument.size_precision(),
5820 instrument.price_precision(),
5821 );
5822 let client_order_id = ClientOrderId::from("O-MODIFIED-GTD-LEG");
5823 let pending_submits = PendingSubmitTracker::default();
5824 let order_contexts = OrderContextRegistry::default();
5825 register_context(
5826 &order_contexts,
5827 old_venue_order_id,
5828 instrument.id(),
5829 client_order_id.as_str(),
5830 );
5831 order_contexts.mark_accepted(old_venue_order_id);
5832 let mut emitter = test_emitter();
5833 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
5834 emitter.set_sender(sender);
5835 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
5836 settlement.note_order_submitted(old_venue_order_id);
5837
5838 let ctx = WsDispatchContext {
5839 signer_type: PolymarketSignerType::Owner,
5840 token_instruments: &token_instruments,
5841 fill_tracker: &fill_tracker,
5842 settlement: &settlement,
5843 pending_submits: &pending_submits,
5844 order_contexts: &order_contexts,
5845 emitter: &emitter,
5846 account_id: AccountId::from("POLY-001"),
5847 clock: nautilus_core::time::get_atomic_clock_realtime(),
5848 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
5849 user_api_key: "00000000-0000-0000-0000-000000000001",
5850 };
5851
5852 let mut state = WsDispatchState::default();
5853 assert!(state.begin_modify(client_order_id, old_venue_order_id, instrument.id()));
5854 assert!(state.set_modify_replacement(
5855 client_order_id,
5856 VenueOrderId::from("0xreplacement"),
5857 Quantity::from("100"),
5858 Quantity::from("100"),
5859 Quantity::from("0"),
5860 Price::from("0.5"),
5861 ));
5862 assert!(
5863 state
5864 .claim_modify_replacement(VenueOrderId::from("0xreplacement"))
5865 .is_some()
5866 );
5867
5868 dispatch_user_message(&UserWsMessage::Order(cancel_order), &ctx, &mut state);
5869
5870 assert!(receiver.try_recv().is_err());
5871 }
5872
5873 #[rstest]
5874 fn test_modified_old_leg_suppresses_cancel_but_still_emits_late_fill() {
5875 let cancel_order: PolymarketUserOrder = load("ws_user_order_cancellation.json");
5876 let trade: PolymarketUserTrade = load("ws_user_trade.json");
5877 let instrument = instrument_for_trade(&trade);
5878 let token_instruments = AtomicMap::new();
5879 token_instruments.insert(cancel_order.asset_id, instrument.clone());
5880
5881 let fill_tracker = OrderFillTrackerMap::new();
5882 let old_venue_order_id = VenueOrderId::from(cancel_order.id.as_str());
5883 fill_tracker.register(
5884 old_venue_order_id,
5885 Quantity::from("100"),
5886 OrderSide::Buy,
5887 instrument.id(),
5888 instrument.size_precision(),
5889 instrument.price_precision(),
5890 );
5891
5892 let client_order_id = ClientOrderId::from("O-MODIFIED-OLD-LEG");
5893 let pending_submits = PendingSubmitTracker::default();
5894 let order_contexts = OrderContextRegistry::default();
5895 register_context(
5896 &order_contexts,
5897 old_venue_order_id,
5898 instrument.id(),
5899 client_order_id.as_str(),
5900 );
5901 order_contexts.mark_accepted(old_venue_order_id);
5902 let mut emitter = test_emitter();
5903 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
5904 emitter.set_sender(sender);
5905 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
5906 settlement.note_order_submitted(old_venue_order_id);
5907
5908 let ctx = WsDispatchContext {
5909 signer_type: PolymarketSignerType::Owner,
5910 token_instruments: &token_instruments,
5911 fill_tracker: &fill_tracker,
5912 settlement: &settlement,
5913 pending_submits: &pending_submits,
5914 order_contexts: &order_contexts,
5915 emitter: &emitter,
5916 account_id: AccountId::from("POLY-001"),
5917 clock: nautilus_core::time::get_atomic_clock_realtime(),
5918 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
5919 user_api_key: "00000000-0000-0000-0000-000000000001",
5920 };
5921
5922 let mut state = WsDispatchState::default();
5923 let replacement_venue_order_id = VenueOrderId::from("0xreplacement");
5924 assert!(state.begin_modify(client_order_id, old_venue_order_id, instrument.id()));
5925 assert!(state.set_modify_replacement(
5926 client_order_id,
5927 replacement_venue_order_id,
5928 Quantity::from("100"),
5929 Quantity::from("100"),
5930 Quantity::from("0"),
5931 Price::from("0.5"),
5932 ));
5933 assert!(
5934 state
5935 .claim_modify_replacement(replacement_venue_order_id)
5936 .is_some()
5937 );
5938
5939 dispatch_user_message(&UserWsMessage::Order(cancel_order), &ctx, &mut state);
5940 assert!(receiver.try_recv().is_err());
5941
5942 dispatch_user_message(&UserWsMessage::Trade(trade), &ctx, &mut state);
5943
5944 match receiver.try_recv().expect("expected late old-leg fill") {
5945 ExecutionEvent::Order(OrderEventAny::Filled(fill)) => {
5946 assert_eq!(fill.client_order_id, client_order_id);
5947 assert_eq!(fill.venue_order_id, old_venue_order_id);
5948 }
5949 other => panic!("expected late old-leg fill, was {other:?}"),
5950 }
5951
5952 assert!(receiver.try_recv().is_err());
5953 }
5954
5955 #[rstest]
5956 fn test_pending_modify_replacement_ws_activity_emits_updated_without_accepted() {
5957 let mut replacement: PolymarketUserOrder = load("ws_user_order_placement.json");
5958 let instrument = test_instrument();
5959 let old_venue_order_id = VenueOrderId::from("0xold-modify-leg");
5960 let replacement_venue_order_id = VenueOrderId::from("0xreplacement-modify-leg");
5961 replacement.id = replacement_venue_order_id.to_string();
5962
5963 let token_instruments = AtomicMap::new();
5964 token_instruments.insert(replacement.asset_id, instrument.clone());
5965 let fill_tracker = OrderFillTrackerMap::new();
5966 fill_tracker.register(
5967 old_venue_order_id,
5968 Quantity::from("100"),
5969 OrderSide::Buy,
5970 instrument.id(),
5971 instrument.size_precision(),
5972 instrument.price_precision(),
5973 );
5974 let client_order_id = ClientOrderId::from("O-PENDING-MODIFY");
5975 let pending_submits = PendingSubmitTracker::default();
5976 let order_contexts = OrderContextRegistry::default();
5977 register_context(
5978 &order_contexts,
5979 old_venue_order_id,
5980 instrument.id(),
5981 client_order_id.as_str(),
5982 );
5983 order_contexts.mark_accepted(old_venue_order_id);
5984 let mut emitter = test_emitter();
5985 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
5986 emitter.set_sender(sender);
5987 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
5988 settlement.mark_live();
5989
5990 let ctx = WsDispatchContext {
5991 signer_type: PolymarketSignerType::Owner,
5992 token_instruments: &token_instruments,
5993 fill_tracker: &fill_tracker,
5994 settlement: &settlement,
5995 pending_submits: &pending_submits,
5996 order_contexts: &order_contexts,
5997 emitter: &emitter,
5998 account_id: AccountId::from("POLY-001"),
5999 clock: nautilus_core::time::get_atomic_clock_realtime(),
6000 user_address: "0xtest",
6001 user_api_key: "test-key",
6002 };
6003
6004 let mut state = WsDispatchState::default();
6005 assert!(state.begin_modify(client_order_id, old_venue_order_id, instrument.id()));
6006 assert!(state.set_modify_replacement(
6007 client_order_id,
6008 replacement_venue_order_id,
6009 Quantity::from("120"),
6010 Quantity::from("100"),
6011 Quantity::from("20"),
6012 Price::from("0.5"),
6013 ));
6014
6015 let original_context = order_contexts.get(&old_venue_order_id).unwrap();
6016
6017 dispatch_user_message(&UserWsMessage::Order(replacement), &ctx, &mut state);
6018
6019 match receiver.try_recv().expect("expected replacement update") {
6020 ExecutionEvent::Order(OrderEventAny::Updated(updated)) => {
6021 assert_eq!(updated.client_order_id, client_order_id);
6022 assert_eq!(updated.venue_order_id, Some(replacement_venue_order_id));
6023 assert_eq!(updated.quantity, Quantity::from("120"));
6024 assert_eq!(updated.price, Some(Price::from("0.5")));
6025 }
6026 other => panic!("expected replacement update, was {other:?}"),
6027 }
6028
6029 assert_eq!(
6030 order_contexts.venue_order_id(&client_order_id),
6031 Some(replacement_venue_order_id)
6032 );
6033 assert_eq!(
6034 order_contexts.get(&old_venue_order_id),
6035 Some(original_context)
6036 );
6037 assert_eq!(
6038 order_contexts.get(&replacement_venue_order_id),
6039 Some(OrderContext {
6040 quantity: Quantity::from("120"),
6041 price: Some(Price::from("0.5")),
6042 ..original_context
6043 })
6044 );
6045 assert!(receiver.try_recv().is_err());
6046 }
6047
6048 #[rstest]
6049 fn test_modify_replacement_missing_context_consumes_claim_without_publication() {
6050 let instrument_id = InstrumentId::from("TEST.POLYMARKET");
6051 let client_order_id = ClientOrderId::from("O-MISSING-CONTEXT");
6052 let old_id = VenueOrderId::from("V-MISSING-CONTEXT-OLD");
6053 let new_id = VenueOrderId::from("V-MISSING-CONTEXT-NEW");
6054 let mut state = WsDispatchState::default();
6055 assert!(state.begin_modify(client_order_id, old_id, instrument_id));
6056 assert!(state.set_modify_replacement(
6057 client_order_id,
6058 new_id,
6059 Quantity::from("12.34"),
6060 Quantity::from("9.87"),
6061 Quantity::from("2.47"),
6062 Price::from("0.6789"),
6063 ));
6064 let order_contexts = OrderContextRegistry::default();
6065 let fill_tracker = OrderFillTrackerMap::new();
6066 let clock = nautilus_core::time::get_atomic_clock_realtime();
6067 let mut emitter = test_emitter();
6068 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
6069 emitter.set_sender(sender);
6070
6071 let activity = state.promote_modify_replacement(
6072 new_id,
6073 &order_contexts,
6074 &fill_tracker,
6075 &emitter,
6076 clock,
6077 |old_venue_order_id| {
6078 assert_eq!(old_venue_order_id, old_id);
6079 order_contexts
6080 .get(&old_venue_order_id)
6081 .map(|context| (context, emitter.account_id()))
6082 },
6083 || panic!("missing context must stop before resolving event time"),
6084 );
6085
6086 let duplicate = state.promote_modify_replacement(
6087 new_id,
6088 &order_contexts,
6089 &fill_tracker,
6090 &emitter,
6091 clock,
6092 |_| panic!("a consumed claim must stop before resolving context"),
6093 || panic!("a consumed claim must stop before resolving event time"),
6094 );
6095
6096 assert!(activity.is_none());
6097 assert!(duplicate.is_none());
6098 assert!(!state.is_modifying(&client_order_id));
6099 assert!(state.pending_modify_promotion(new_id).is_none());
6100 assert!(state.suppress_modify_cancel(old_id));
6101 assert_eq!(order_contexts.get(&new_id), None);
6102 assert_eq!(order_contexts.venue_order_id(&client_order_id), None);
6103 assert!(!fill_tracker.contains(&new_id));
6104 assert!(receiver.try_recv().is_err());
6105 assert!(order_contexts.mark_accepted(new_id));
6106 }
6107
6108 #[rstest]
6109 fn test_pending_modify_replacement_ws_rejection_emits_once_and_closes_old_leg() {
6110 let mut replacement: PolymarketUserOrder = load("ws_user_order_placement.json");
6111 let instrument = test_instrument();
6112 let old_venue_order_id = VenueOrderId::from("0xold-rejected-modify-leg");
6113 let replacement_venue_order_id = VenueOrderId::from("0xrejected-replacement-modify-leg");
6114 replacement.id = replacement_venue_order_id.to_string();
6115 replacement.status = Some(PolymarketUserOrderStatus::new(
6116 PolymarketOrderStatus::Unmatched,
6117 Some("replacement rejected"),
6118 ));
6119
6120 let token_instruments = AtomicMap::new();
6121 token_instruments.insert(replacement.asset_id, instrument.clone());
6122 let fill_tracker = OrderFillTrackerMap::new();
6123 fill_tracker.register(
6124 old_venue_order_id,
6125 Quantity::from("100"),
6126 OrderSide::Buy,
6127 instrument.id(),
6128 instrument.size_precision(),
6129 instrument.price_precision(),
6130 );
6131 let client_order_id = ClientOrderId::from("O-REJECTED-MODIFY");
6132 let pending_submits = PendingSubmitTracker::default();
6133 let order_contexts = OrderContextRegistry::default();
6134 register_context(
6135 &order_contexts,
6136 old_venue_order_id,
6137 instrument.id(),
6138 client_order_id.as_str(),
6139 );
6140 order_contexts.mark_accepted(old_venue_order_id);
6141 let mut emitter = test_emitter();
6142 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
6143 emitter.set_sender(sender);
6144 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
6145 settlement.mark_live();
6146
6147 let ctx = WsDispatchContext {
6148 signer_type: PolymarketSignerType::Owner,
6149 token_instruments: &token_instruments,
6150 fill_tracker: &fill_tracker,
6151 settlement: &settlement,
6152 pending_submits: &pending_submits,
6153 order_contexts: &order_contexts,
6154 emitter: &emitter,
6155 account_id: AccountId::from("POLY-001"),
6156 clock: nautilus_core::time::get_atomic_clock_realtime(),
6157 user_address: "0xtest",
6158 user_api_key: "test-key",
6159 };
6160
6161 let mut state = WsDispatchState::default();
6162 assert!(state.begin_modify(client_order_id, old_venue_order_id, instrument.id()));
6163 let cancel_ts = UnixNanos::from(123);
6164 assert!(state.confirm_modify_cancel(client_order_id, old_venue_order_id, cancel_ts,));
6165 assert!(state.set_modify_replacement(
6166 client_order_id,
6167 replacement_venue_order_id,
6168 Quantity::from("120"),
6169 Quantity::from("100"),
6170 Quantity::from("20"),
6171 Price::from("0.5"),
6172 ));
6173
6174 dispatch_user_message(&UserWsMessage::Order(replacement), &ctx, &mut state);
6175
6176 match receiver.try_recv().expect("expected modify rejection") {
6177 ExecutionEvent::Order(OrderEventAny::ModifyRejected(rejected)) => {
6178 assert_eq!(rejected.client_order_id, client_order_id);
6179 assert_eq!(rejected.venue_order_id, Some(old_venue_order_id));
6180 assert_eq!(rejected.reason, "replacement rejected");
6181 }
6182 other => panic!("expected modify rejection, was {other:?}"),
6183 }
6184
6185 match receiver.try_recv().expect("expected old-leg cancellation") {
6186 ExecutionEvent::Order(OrderEventAny::Canceled(canceled)) => {
6187 assert_eq!(canceled.client_order_id, client_order_id);
6188 assert_eq!(canceled.venue_order_id, Some(old_venue_order_id));
6189 assert_eq!(canceled.ts_event, cancel_ts);
6190 }
6191 other => panic!("expected old-leg cancellation, was {other:?}"),
6192 }
6193
6194 assert!(
6195 state
6196 .pending_modify_promotion(replacement_venue_order_id)
6197 .is_none()
6198 );
6199 assert!(receiver.try_recv().is_err());
6200 }
6201
6202 #[rstest]
6203 fn test_pending_modify_replacement_fill_promotes_before_fill() {
6204 let trade: PolymarketUserTrade = load("ws_user_trade.json");
6205 let instrument = instrument_for_trade(&trade);
6206 let old_venue_order_id = VenueOrderId::from("0xold-fill-leg");
6207 let replacement_venue_order_id = VenueOrderId::from(trade.taker_order_id.as_str());
6208 let token_instruments = AtomicMap::new();
6209 token_instruments.insert(trade.asset_id, instrument.clone());
6210 let fill_tracker = OrderFillTrackerMap::new();
6211 fill_tracker.register(
6212 old_venue_order_id,
6213 Quantity::from("100"),
6214 OrderSide::Buy,
6215 instrument.id(),
6216 instrument.size_precision(),
6217 instrument.price_precision(),
6218 );
6219 let client_order_id = ClientOrderId::from("O-PENDING-MODIFY-FILL");
6220 let pending_submits = PendingSubmitTracker::default();
6221 let order_contexts = OrderContextRegistry::default();
6222 register_context(
6223 &order_contexts,
6224 old_venue_order_id,
6225 instrument.id(),
6226 client_order_id.as_str(),
6227 );
6228 order_contexts.mark_accepted(old_venue_order_id);
6229 let mut emitter = test_emitter();
6230 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
6231 emitter.set_sender(sender);
6232 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
6233 settlement.note_order_submitted(replacement_venue_order_id);
6234
6235 let ctx = WsDispatchContext {
6236 signer_type: PolymarketSignerType::Owner,
6237 token_instruments: &token_instruments,
6238 fill_tracker: &fill_tracker,
6239 settlement: &settlement,
6240 pending_submits: &pending_submits,
6241 order_contexts: &order_contexts,
6242 emitter: &emitter,
6243 account_id: AccountId::from("POLY-001"),
6244 clock: nautilus_core::time::get_atomic_clock_realtime(),
6245 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
6246 user_api_key: "00000000-0000-0000-0000-000000000001",
6247 };
6248
6249 let mut state = WsDispatchState::default();
6250 assert!(state.begin_modify(client_order_id, old_venue_order_id, instrument.id()));
6251 assert!(state.set_modify_replacement(
6252 client_order_id,
6253 replacement_venue_order_id,
6254 Quantity::from("120"),
6255 Quantity::from("100"),
6256 Quantity::from("20"),
6257 Price::from("0.5"),
6258 ));
6259 let mut cancellation: PolymarketUserOrder = load("ws_user_order_cancellation.json");
6260 cancellation.id = replacement_venue_order_id.to_string();
6261 let cancellation_report = build_ws_order_status_report(
6262 &cancellation,
6263 cancellation.status.as_ref().unwrap(),
6264 cancellation.order_type.unwrap(),
6265 &instrument,
6266 ctx.account_id,
6267 UnixNanos::from(2_000_000_000),
6268 UnixNanos::from(3_000_000_000),
6269 )
6270 .unwrap();
6271 assert!(
6272 fill_tracker
6273 .accept_or_buffer_report(replacement_venue_order_id, cancellation_report)
6274 .is_none()
6275 );
6276
6277 dispatch_user_message(&UserWsMessage::Trade(trade), &ctx, &mut state);
6278
6279 match receiver.try_recv().expect("expected replacement update") {
6280 ExecutionEvent::Order(OrderEventAny::Updated(updated)) => {
6281 assert_eq!(updated.client_order_id, client_order_id);
6282 assert_eq!(updated.venue_order_id, Some(replacement_venue_order_id));
6283 assert_eq!(updated.quantity, Quantity::from("120"));
6284 }
6285 other => panic!("expected replacement update, was {other:?}"),
6286 }
6287
6288 match receiver.try_recv().expect("expected replacement fill") {
6289 ExecutionEvent::Order(OrderEventAny::Filled(fill)) => {
6290 assert_eq!(fill.client_order_id, client_order_id);
6291 assert_eq!(fill.venue_order_id, replacement_venue_order_id);
6292 }
6293 other => panic!("expected replacement fill, was {other:?}"),
6294 }
6295
6296 match receiver.try_recv().expect("expected replacement cancel") {
6297 ExecutionEvent::Order(OrderEventAny::Canceled(cancel)) => {
6298 assert_eq!(cancel.client_order_id, client_order_id);
6299 assert_eq!(cancel.venue_order_id, Some(replacement_venue_order_id));
6300 }
6301 other => panic!("expected replacement cancel, was {other:?}"),
6302 }
6303
6304 assert!(receiver.try_recv().is_err());
6305 }
6306
6307 #[rstest]
6308 fn test_modified_buy_overfill_raises_order_qty_with_prior_legs() {
6309 let mut trade: PolymarketUserTrade = load("ws_user_trade.json");
6310 trade.size = "100.000058".to_string();
6311 let instrument = instrument_for_trade(&trade);
6312 let old_venue_order_id = VenueOrderId::from("0xold-overfill-leg");
6313 let replacement_venue_order_id = VenueOrderId::from(trade.taker_order_id.as_str());
6314 let token_instruments = AtomicMap::new();
6315 token_instruments.insert(trade.asset_id, instrument.clone());
6316 let fill_tracker = OrderFillTrackerMap::new();
6317 fill_tracker.register(
6318 old_venue_order_id,
6319 Quantity::from("120"),
6320 OrderSide::Buy,
6321 instrument.id(),
6322 instrument.size_precision(),
6323 instrument.price_precision(),
6324 );
6325 let client_order_id = ClientOrderId::from("O-MODIFIED-OVERFILL");
6326 let pending_submits = PendingSubmitTracker::default();
6327 let order_contexts = OrderContextRegistry::default();
6328 register_context(
6329 &order_contexts,
6330 old_venue_order_id,
6331 instrument.id(),
6332 client_order_id.as_str(),
6333 );
6334 order_contexts.mark_accepted(old_venue_order_id);
6335 let mut emitter = test_emitter();
6336 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
6337 emitter.set_sender(sender);
6338 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
6339 settlement.note_order_submitted(replacement_venue_order_id);
6340
6341 let ctx = WsDispatchContext {
6342 signer_type: PolymarketSignerType::Owner,
6343 token_instruments: &token_instruments,
6344 fill_tracker: &fill_tracker,
6345 settlement: &settlement,
6346 pending_submits: &pending_submits,
6347 order_contexts: &order_contexts,
6348 emitter: &emitter,
6349 account_id: AccountId::from("POLY-001"),
6350 clock: nautilus_core::time::get_atomic_clock_realtime(),
6351 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
6352 user_api_key: "00000000-0000-0000-0000-000000000001",
6353 };
6354
6355 let mut state = WsDispatchState::default();
6357 assert!(state.begin_modify(client_order_id, old_venue_order_id, instrument.id()));
6358 assert!(state.set_modify_replacement(
6359 client_order_id,
6360 replacement_venue_order_id,
6361 Quantity::from("120"),
6362 Quantity::from("100"),
6363 Quantity::from("20"),
6364 Price::from("0.5"),
6365 ));
6366
6367 dispatch_user_message(&UserWsMessage::Trade(trade), &ctx, &mut state);
6368
6369 let promoted = receiver.try_recv().expect("expected replacement update");
6370 let bumped = receiver.try_recv().expect("expected overfill update");
6371 let filled = receiver.try_recv().expect("expected replacement fill");
6372
6373 match promoted {
6374 ExecutionEvent::Order(OrderEventAny::Updated(updated)) => {
6375 assert_eq!(updated.quantity, Quantity::from("120"));
6376 }
6377 other => panic!("expected replacement update, was {other:?}"),
6378 }
6379
6380 match bumped {
6381 ExecutionEvent::Order(OrderEventAny::Updated(updated)) => {
6382 assert_eq!(updated.client_order_id, client_order_id);
6383 assert_eq!(updated.venue_order_id, Some(replacement_venue_order_id));
6384 assert_eq!(updated.quantity.as_decimal(), dec!(120.000058));
6385 }
6386 other => panic!("expected overfill update, was {other:?}"),
6387 }
6388
6389 match filled {
6390 ExecutionEvent::Order(OrderEventAny::Filled(fill)) => {
6391 assert_eq!(fill.venue_order_id, replacement_venue_order_id);
6392 assert_eq!(fill.last_qty.as_decimal(), dec!(100.000058));
6393 }
6394 other => panic!("expected replacement fill, was {other:?}"),
6395 }
6396
6397 assert!(receiver.try_recv().is_err());
6398 }
6399
6400 #[rstest]
6401 fn test_late_modify_completion_does_not_finish_newer_modify() {
6402 let instrument_id = InstrumentId::from("TEST.POLYMARKET");
6403 let client_order_id = ClientOrderId::from("O-MODIFY-GENERATION");
6404 let old_venue_order_id = VenueOrderId::from("0xmodify-generation-old");
6405 let first_replacement_venue_order_id = VenueOrderId::from("0xmodify-generation-first");
6406 let second_replacement_venue_order_id = VenueOrderId::from("0xmodify-generation-second");
6407 let mut state = WsDispatchState::default();
6408
6409 assert!(state.begin_modify(client_order_id, old_venue_order_id, instrument_id));
6410 assert!(state.set_modify_replacement(
6411 client_order_id,
6412 first_replacement_venue_order_id,
6413 Quantity::from("12"),
6414 Quantity::from("12"),
6415 Quantity::from("0"),
6416 Price::from("0.5"),
6417 ));
6418 assert!(
6419 state
6420 .claim_modify_replacement(first_replacement_venue_order_id)
6421 .is_some()
6422 );
6423 assert!(state.begin_modify(
6424 client_order_id,
6425 first_replacement_venue_order_id,
6426 instrument_id,
6427 ));
6428 assert!(state.set_modify_replacement(
6429 client_order_id,
6430 second_replacement_venue_order_id,
6431 Quantity::from("15"),
6432 Quantity::from("15"),
6433 Quantity::from("0"),
6434 Price::from("0.6"),
6435 ));
6436
6437 assert!(
6438 state
6439 .finish_modify_without_replacement(
6440 client_order_id,
6441 old_venue_order_id,
6442 true,
6443 UnixNanos::from(123),
6444 )
6445 .is_none()
6446 );
6447 let promotion = state
6448 .pending_modify_promotion(second_replacement_venue_order_id)
6449 .expect("newer modification must remain pending");
6450 assert_eq!(promotion.client_order_id, client_order_id);
6451 assert_eq!(
6452 promotion.old_venue_order_id,
6453 first_replacement_venue_order_id
6454 );
6455 assert_eq!(promotion.venue_order_id, second_replacement_venue_order_id);
6456 assert_eq!(promotion.quantity, Quantity::from("15"));
6457 assert_eq!(promotion.leg_quantity, Quantity::from("15"));
6458 assert_eq!(promotion.price, Price::from("0.6"));
6459 }
6460
6461 #[rstest]
6462 fn test_pending_modify_lookup_selects_matching_replacement() {
6463 let instrument_id = InstrumentId::from("TEST.POLYMARKET");
6464 let first_client_order_id = ClientOrderId::from("O-MODIFY-LOOKUP-1");
6465 let second_client_order_id = ClientOrderId::from("O-MODIFY-LOOKUP-2");
6466 let first_old_venue_order_id = VenueOrderId::from("0xmodify-lookup-old-1");
6467 let second_old_venue_order_id = VenueOrderId::from("0xmodify-lookup-old-2");
6468 let first_replacement_venue_order_id = VenueOrderId::from("0xmodify-lookup-new-1");
6469 let second_replacement_venue_order_id = VenueOrderId::from("0xmodify-lookup-new-2");
6470 let mut state = WsDispatchState::default();
6471
6472 assert!(state.begin_modify(
6473 first_client_order_id,
6474 first_old_venue_order_id,
6475 instrument_id,
6476 ));
6477 assert!(state.set_modify_replacement(
6478 first_client_order_id,
6479 first_replacement_venue_order_id,
6480 Quantity::from("11"),
6481 Quantity::from("10"),
6482 Quantity::from("1"),
6483 Price::from("0.4"),
6484 ));
6485 assert!(state.begin_modify(
6486 second_client_order_id,
6487 second_old_venue_order_id,
6488 instrument_id,
6489 ));
6490 assert!(state.set_modify_replacement(
6491 second_client_order_id,
6492 second_replacement_venue_order_id,
6493 Quantity::from("22"),
6494 Quantity::from("20"),
6495 Quantity::from("2"),
6496 Price::from("0.6"),
6497 ));
6498
6499 let second = state
6500 .claim_modify_replacement(second_replacement_venue_order_id)
6501 .expect("second replacement must be selected");
6502 assert_eq!(second.client_order_id, second_client_order_id);
6503 assert_eq!(second.old_venue_order_id, second_old_venue_order_id);
6504 assert_eq!(second.venue_order_id, second_replacement_venue_order_id);
6505 assert_eq!(second.quantity, Quantity::from("22"));
6506 assert_eq!(second.leg_quantity, Quantity::from("20"));
6507 assert_eq!(second.price, Price::from("0.6"));
6508
6509 let first = state
6510 .pending_modify_promotion(first_replacement_venue_order_id)
6511 .expect("first replacement must remain pending");
6512 assert_eq!(first.client_order_id, first_client_order_id);
6513 assert_eq!(first.old_venue_order_id, first_old_venue_order_id);
6514 assert_eq!(first.venue_order_id, first_replacement_venue_order_id);
6515 assert_eq!(first.quantity, Quantity::from("11"));
6516 assert_eq!(first.leg_quantity, Quantity::from("10"));
6517 assert_eq!(first.price, Price::from("0.4"));
6518 }
6519
6520 #[rstest]
6521 fn test_begin_cancels_is_atomic_when_one_order_conflicts() {
6522 let instrument_id = InstrumentId::from("TEST.POLYMARKET");
6523 let available_client_order_id = ClientOrderId::from("O-CANCEL-AVAILABLE");
6524 let conflicting_client_order_id = ClientOrderId::from("O-CANCEL-CONFLICT");
6525 let mut state = WsDispatchState::default();
6526
6527 assert!(state.begin_modify(
6528 conflicting_client_order_id,
6529 VenueOrderId::from("0xmodify-conflict"),
6530 instrument_id,
6531 ));
6532 assert!(!state.begin_cancels(&[
6533 (available_client_order_id, instrument_id),
6534 (conflicting_client_order_id, instrument_id),
6535 ]));
6536 assert!(state.begin_modify(
6537 available_client_order_id,
6538 VenueOrderId::from("0xmodify-available"),
6539 instrument_id,
6540 ));
6541 }
6542
6543 #[rstest]
6544 fn test_begin_available_cancels_skips_existing_cancel() {
6545 let instrument_id = InstrumentId::from("TEST.POLYMARKET");
6546 let pending_client_order_id = ClientOrderId::from("O-CANCEL-PENDING");
6547 let available_client_order_id = ClientOrderId::from("O-CANCEL-AVAILABLE");
6548 let modifying_client_order_id = ClientOrderId::from("O-MODIFY-PENDING");
6549 let unreserved_client_order_id = ClientOrderId::from("O-CANCEL-UNRESERVED");
6550 let mut state = WsDispatchState::default();
6551
6552 assert!(state.begin_cancels(&[(pending_client_order_id, instrument_id)]));
6553 assert_eq!(
6554 state
6555 .begin_available_cancels(&[
6556 (pending_client_order_id, instrument_id),
6557 (available_client_order_id, instrument_id),
6558 ])
6559 .unwrap(),
6560 vec![available_client_order_id],
6561 );
6562 assert!(!state.begin_modify(
6563 pending_client_order_id,
6564 VenueOrderId::from("0xcancel-pending"),
6565 instrument_id,
6566 ));
6567 assert!(!state.begin_modify(
6568 available_client_order_id,
6569 VenueOrderId::from("0xcancel-available"),
6570 instrument_id,
6571 ));
6572
6573 state.finish_cancels(&[pending_client_order_id, available_client_order_id]);
6574 assert!(state.begin_modify(
6575 modifying_client_order_id,
6576 VenueOrderId::from("0xmodify-pending"),
6577 instrument_id,
6578 ));
6579 assert!(
6580 state
6581 .begin_available_cancels(&[
6582 (unreserved_client_order_id, instrument_id),
6583 (modifying_client_order_id, instrument_id),
6584 ])
6585 .is_none()
6586 );
6587 assert!(state.begin_modify(
6588 unreserved_client_order_id,
6589 VenueOrderId::from("0xcancel-unreserved"),
6590 instrument_id,
6591 ));
6592 }
6593
6594 #[rstest]
6595 fn test_cancel_and_modify_are_mutually_exclusive() {
6596 let instrument_id = InstrumentId::from("TEST.POLYMARKET");
6597 let client_order_id = ClientOrderId::from("O-MODIFY-CANCEL-ALL");
6598 let other_client_order_id = ClientOrderId::from("O-MODIFY-MARKET-CANCEL");
6599 let venue_order_id = VenueOrderId::from("0xmodify-cancel-all");
6600 let mut state = WsDispatchState::default();
6601
6602 assert!(state.begin_cancels(&[(client_order_id, instrument_id)]));
6603 assert!(!state.begin_modify(client_order_id, venue_order_id, instrument_id));
6604 assert!(state.begin_market_cancel(instrument_id));
6605 assert!(!state.begin_modify(
6606 other_client_order_id,
6607 VenueOrderId::from("0xmodify-market-cancel"),
6608 instrument_id,
6609 ));
6610 state.finish_market_cancel(instrument_id);
6611 state.finish_cancels(&[client_order_id]);
6612 assert!(state.begin_modify(client_order_id, venue_order_id, instrument_id));
6613 assert!(!state.begin_cancels(&[(client_order_id, instrument_id)]));
6614 assert!(!state.begin_market_cancel(instrument_id));
6615 assert!(state.set_modify_replacement(
6616 client_order_id,
6617 VenueOrderId::from("0xmodify-cancel-all-new"),
6618 Quantity::from("12"),
6619 Quantity::from("12"),
6620 Quantity::from("0"),
6621 Price::from("0.5"),
6622 ));
6623 assert!(!state.begin_cancels(&[(client_order_id, instrument_id)]));
6624 assert!(!state.begin_market_cancel(instrument_id));
6625 assert!(
6626 state
6627 .finish_modify_without_replacement(
6628 client_order_id,
6629 venue_order_id,
6630 false,
6631 UnixNanos::default(),
6632 )
6633 .is_some()
6634 );
6635 assert!(state.begin_market_cancel(instrument_id));
6636 assert!(!state.begin_modify(client_order_id, venue_order_id, instrument_id));
6637 state.finish_market_cancel(instrument_id);
6638 }
6639
6640 #[rstest]
6641 fn test_reset_preserves_modify_recovery_and_stale_leg_safety() {
6642 let instrument_id = InstrumentId::from("TEST.POLYMARKET");
6643 let pending_client_order_id = ClientOrderId::from("O-PENDING-RESET");
6644 let pending_old_venue_order_id = VenueOrderId::from("0xpending-old-reset");
6645 let pending_new_venue_order_id = VenueOrderId::from("0xpending-new-reset");
6646 let replaced_client_order_id = ClientOrderId::from("O-REPLACED-RESET");
6647 let replaced_old_venue_order_id = VenueOrderId::from("0xreplaced-old-reset");
6648 let replaced_new_venue_order_id = VenueOrderId::from("0xreplaced-new-reset");
6649 let closed_client_order_id = ClientOrderId::from("O-CLOSED-RESET");
6650 let closed_venue_order_id = VenueOrderId::from("0xclosed-reset");
6651 let cancel_client_order_id = ClientOrderId::from("O-CANCEL-RESET");
6652 let trade_id = TradeId::from("T-RESET");
6653 let mut state = WsDispatchState::default();
6654
6655 assert!(state.begin_modify(
6656 pending_client_order_id,
6657 pending_old_venue_order_id,
6658 instrument_id,
6659 ));
6660 assert!(state.set_modify_replacement(
6661 pending_client_order_id,
6662 pending_new_venue_order_id,
6663 Quantity::from("12"),
6664 Quantity::from("10"),
6665 Quantity::from("2"),
6666 Price::from("0.5"),
6667 ));
6668 assert!(state.begin_modify(
6669 replaced_client_order_id,
6670 replaced_old_venue_order_id,
6671 instrument_id,
6672 ));
6673 assert!(state.set_modify_replacement(
6674 replaced_client_order_id,
6675 replaced_new_venue_order_id,
6676 Quantity::from("12"),
6677 Quantity::from("10"),
6678 Quantity::from("2"),
6679 Price::from("0.5"),
6680 ));
6681 assert!(
6682 state
6683 .claim_modify_replacement(replaced_new_venue_order_id)
6684 .is_some()
6685 );
6686 assert!(state.begin_modify(closed_client_order_id, closed_venue_order_id, instrument_id,));
6687 assert!(
6688 state
6689 .finish_modify_without_replacement(
6690 closed_client_order_id,
6691 closed_venue_order_id,
6692 true,
6693 UnixNanos::from(1),
6694 )
6695 .is_some()
6696 );
6697 state.record_reconciled_fill(trade_id, pending_old_venue_order_id);
6698 assert!(state.begin_cancels(&[(cancel_client_order_id, instrument_id)]));
6699
6700 state.reset_session();
6701
6702 assert_eq!(
6703 state
6704 .pending_modify_promotion(pending_new_venue_order_id)
6705 .unwrap()
6706 .client_order_id,
6707 pending_client_order_id,
6708 );
6709 assert!(state.suppress_modify_cancel(replaced_old_venue_order_id));
6710 assert!(state.suppress_modify_cancel(closed_venue_order_id));
6711 assert!(state.suppress_modify_cancel_reemit(pending_old_venue_order_id));
6712 assert!(state.suppress_modify_cancel_reemit(replaced_old_venue_order_id));
6713 assert!(!state.suppress_modify_cancel_reemit(closed_venue_order_id));
6714 assert!(
6715 state
6716 .reconciled_fills
6717 .contains(&(trade_id, pending_old_venue_order_id))
6718 );
6719 assert!(state.begin_cancels(&[(cancel_client_order_id, instrument_id)]));
6720 }
6721
6722 #[rstest]
6723 fn test_reconciled_fill_is_not_reapplied_from_websocket() {
6724 let trade: PolymarketUserTrade = load("ws_user_trade.json");
6725 let instrument = instrument_for_trade(&trade);
6726 let venue_order_id = VenueOrderId::from(trade.taker_order_id.as_str());
6727 let token_instruments = AtomicMap::new();
6728 token_instruments.insert(trade.asset_id, instrument.clone());
6729 let fill_tracker = OrderFillTrackerMap::new();
6730 fill_tracker.restore_order(
6731 venue_order_id,
6732 Quantity::from("100"),
6733 Quantity::from("0"),
6734 Quantity::from("0"),
6735 Quantity::from("25"),
6736 OrderSide::Buy,
6737 );
6738 let pending_submits = PendingSubmitTracker::default();
6739 let order_contexts = OrderContextRegistry::default();
6740 register_context(
6741 &order_contexts,
6742 venue_order_id,
6743 instrument.id(),
6744 "O-RECONCILED-FILL",
6745 );
6746 let mut emitter = test_emitter();
6747 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
6748 emitter.set_sender(sender);
6749 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
6750 settlement.mark_live();
6751 settlement.note_order_submitted(venue_order_id);
6752
6753 let ctx = WsDispatchContext {
6754 signer_type: PolymarketSignerType::Owner,
6755 token_instruments: &token_instruments,
6756 fill_tracker: &fill_tracker,
6757 settlement: &settlement,
6758 pending_submits: &pending_submits,
6759 order_contexts: &order_contexts,
6760 emitter: &emitter,
6761 account_id: AccountId::from("POLY-001"),
6762 clock: nautilus_core::time::get_atomic_clock_realtime(),
6763 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
6764 user_api_key: "00000000-0000-0000-0000-000000000001",
6765 };
6766
6767 let mut state = WsDispatchState::default();
6768 state.record_reconciled_fill(TradeId::from(trade.id.as_str()), venue_order_id);
6769
6770 dispatch_user_message(&UserWsMessage::Trade(trade), &ctx, &mut state);
6771
6772 assert_eq!(
6773 fill_tracker.get_cumulative_filled(&venue_order_id),
6774 Some(Quantity::from("25")),
6775 );
6776 assert!(receiver.try_recv().is_err());
6777 assert!(settlement.ensure_resolved(None, "mass status").is_ok());
6779 }
6780
6781 #[rstest]
6782 fn test_cancel_not_reemitted_when_fill_completes_order() {
6783 let cancel_order: PolymarketUserOrder = load("ws_user_order_cancellation.json");
6784 let trade: PolymarketUserTrade = load("ws_user_trade.json");
6785 let instrument = instrument_for_trade(&trade);
6786
6787 let token_instruments = AtomicMap::new();
6788 token_instruments.insert(cancel_order.asset_id, instrument.clone());
6789
6790 let fill_tracker = OrderFillTrackerMap::new();
6791 let venue_order_id = VenueOrderId::from(cancel_order.id.as_str());
6792
6793 fill_tracker.register(
6795 venue_order_id,
6796 Quantity::from("25"),
6797 OrderSide::Buy,
6798 instrument.id(),
6799 instrument.size_precision(),
6800 instrument.price_precision(),
6801 );
6802
6803 let pending_submits = PendingSubmitTracker::default();
6804 let order_contexts = OrderContextRegistry::default();
6805 register_context(
6806 &order_contexts,
6807 venue_order_id,
6808 instrument.id(),
6809 "O-CANCEL-FULL",
6810 );
6811 order_contexts.mark_accepted(venue_order_id);
6812 let mut emitter = test_emitter();
6813 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
6814 emitter.set_sender(sender);
6815
6816 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
6817 settlement.mark_live();
6818 settlement.note_order_submitted(venue_order_id);
6819
6820 let ctx = WsDispatchContext {
6821 signer_type: PolymarketSignerType::Owner,
6822 token_instruments: &token_instruments,
6823 fill_tracker: &fill_tracker,
6824 settlement: &settlement,
6825 pending_submits: &pending_submits,
6826 order_contexts: &order_contexts,
6827 emitter: &emitter,
6828 account_id: AccountId::from("POLY-001"),
6829 clock: nautilus_core::time::get_atomic_clock_realtime(),
6830 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
6831 user_api_key: "00000000-0000-0000-0000-000000000001",
6832 };
6833 let mut state = WsDispatchState::default();
6834
6835 dispatch_user_message(&UserWsMessage::Order(cancel_order), &ctx, &mut state);
6837 let _cancel = receiver.try_recv().expect("Expected canceled event");
6838
6839 dispatch_user_message(&UserWsMessage::Trade(trade), &ctx, &mut state);
6840 let _fill = receiver.try_recv().expect("Expected filled event");
6841
6842 assert!(
6844 receiver.try_recv().is_err(),
6845 "Should not re-emit cancel when fill completes the order"
6846 );
6847 }
6848
6849 #[rstest]
6850 fn test_cancel_saved_before_acceptance_and_retained_for_modify_reset() {
6851 let cancel_order: PolymarketUserOrder = load("ws_user_order_cancellation.json");
6852 let instrument = test_instrument();
6853 let instrument_id = instrument.id();
6854
6855 let token_instruments = AtomicMap::new();
6856 token_instruments.insert(cancel_order.asset_id, instrument);
6857
6858 let fill_tracker = OrderFillTrackerMap::new();
6860 let venue_order_id = VenueOrderId::from(cancel_order.id.as_str());
6861
6862 let pending_submits = PendingSubmitTracker::default();
6863 let order_contexts = OrderContextRegistry::default();
6864 let emitter = test_emitter();
6865
6866 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
6867 settlement.mark_live();
6868 settlement.note_order_submitted(venue_order_id);
6869
6870 let ctx = WsDispatchContext {
6871 signer_type: PolymarketSignerType::Owner,
6872 token_instruments: &token_instruments,
6873 fill_tracker: &fill_tracker,
6874 settlement: &settlement,
6875 pending_submits: &pending_submits,
6876 order_contexts: &order_contexts,
6877 emitter: &emitter,
6878 account_id: AccountId::from("POLY-001"),
6879 clock: nautilus_core::time::get_atomic_clock_realtime(),
6880 user_address: "0xtest",
6881 user_api_key: "test-key",
6882 };
6883 let mut state = WsDispatchState::default();
6884
6885 dispatch_user_message(&UserWsMessage::Order(cancel_order), &ctx, &mut state);
6887
6888 assert!(fill_tracker.has_pending_report(&venue_order_id));
6890 assert!(state.terminal_cancel_reports.get(&venue_order_id).is_some());
6891
6892 let client_order_id = ClientOrderId::from("O-CANCEL-RESET");
6893 assert!(state.begin_modify(client_order_id, venue_order_id, instrument_id));
6894 state.reset_session();
6895 assert!(
6896 state
6897 .finish_modify_without_replacement(
6898 client_order_id,
6899 venue_order_id,
6900 false,
6901 UnixNanos::default(),
6902 )
6903 .unwrap()
6904 .1
6905 .is_some()
6906 );
6907 }
6908
6909 #[rstest]
6912 #[case(PolymarketOrderStatus::Canceled, "Canceled", OrderStatus::Canceled)]
6913 #[case(
6914 PolymarketOrderStatus::CanceledMarketResolved,
6915 "Expired",
6916 OrderStatus::Expired
6917 )]
6918 fn test_buffered_fill_emitted_before_terminal_status(
6919 #[case] status: PolymarketOrderStatus,
6920 #[case] expected_terminal: &str,
6921 #[case] expected_order_status: OrderStatus,
6922 #[values(false, true)] context_before_update: bool,
6923 ) {
6924 let mut terminal_order: PolymarketUserOrder = load("ws_user_order_cancellation.json");
6925 terminal_order.status = Some(status.into());
6926 let trade: PolymarketUserTrade = load("ws_user_trade.json");
6927 let instrument = instrument_for_trade(&trade);
6928
6929 let token_instruments = AtomicMap::new();
6930 token_instruments.insert(terminal_order.asset_id, instrument.clone());
6931
6932 let fill_tracker = OrderFillTrackerMap::new();
6934 let venue_order_id = VenueOrderId::from(terminal_order.id.as_str());
6935 let client_order_id = ClientOrderId::from("O-BUFFERED");
6936
6937 let pending_submits = PendingSubmitTracker::default();
6938 pending_submits.insert(venue_order_id, client_order_id);
6939 let order_contexts = OrderContextRegistry::default();
6940
6941 if context_before_update {
6942 register_context(
6943 &order_contexts,
6944 venue_order_id,
6945 instrument.id(),
6946 client_order_id.as_str(),
6947 );
6948 }
6949
6950 let mut emitter = test_emitter();
6951 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
6952 emitter.set_sender(sender);
6953
6954 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
6955 settlement.mark_live();
6956 settlement.note_order_submitted(venue_order_id);
6957
6958 let ctx = WsDispatchContext {
6959 signer_type: PolymarketSignerType::Owner,
6960 token_instruments: &token_instruments,
6961 fill_tracker: &fill_tracker,
6962 settlement: &settlement,
6963 pending_submits: &pending_submits,
6964 order_contexts: &order_contexts,
6965 emitter: &emitter,
6966 account_id: AccountId::from("POLY-001"),
6967 clock: nautilus_core::time::get_atomic_clock_realtime(),
6968 user_address: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
6969 user_api_key: "00000000-0000-0000-0000-000000000001",
6970 };
6971 let mut state = WsDispatchState::default();
6972
6973 dispatch_user_message(&UserWsMessage::Trade(trade), &ctx, &mut state);
6975 assert!(
6976 receiver.try_recv().is_err(),
6977 "a buffered fill must emit no event before the order is registered",
6978 );
6979
6980 if !context_before_update {
6981 dispatch_user_message(
6982 &UserWsMessage::Order(terminal_order.clone()),
6983 &ctx,
6984 &mut state,
6985 );
6986 assert!(receiver.try_recv().is_err());
6987 assert!(!fill_tracker.contains(&venue_order_id));
6988 register_context(
6989 &order_contexts,
6990 venue_order_id,
6991 instrument.id(),
6992 client_order_id.as_str(),
6993 );
6994 }
6995
6996 dispatch_user_message(&UserWsMessage::Order(terminal_order), &ctx, &mut state);
6998
6999 let mut emitted = Vec::new();
7000
7001 while let Ok(event) = receiver.try_recv() {
7002 match event {
7003 ExecutionEvent::Order(order_event) => emitted.push(order_event),
7004 other => panic!("expected only order events, was {other:?}"),
7005 }
7006 }
7007
7008 assert_eq!(emitted.len(), 3, "emitted sequence was {emitted:?}");
7009 match &emitted[0] {
7010 OrderEventAny::Accepted(accepted) => {
7011 assert_eq!(accepted.client_order_id, client_order_id);
7012 assert_eq!(accepted.venue_order_id, venue_order_id);
7013 }
7014 other => panic!("expected accepted event first, was {other:?}"),
7015 }
7016
7017 match &emitted[1] {
7018 OrderEventAny::Filled(filled) => {
7019 assert_eq!(filled.client_order_id, client_order_id);
7020 assert_eq!(filled.venue_order_id, venue_order_id);
7021 assert_eq!(filled.last_qty.as_decimal(), dec!(25));
7022 }
7023 other => panic!("expected filled event before the terminal status, was {other:?}"),
7024 }
7025
7026 let terminal = match &emitted[2] {
7027 OrderEventAny::Canceled(canceled) => {
7028 assert_eq!(canceled.client_order_id, client_order_id);
7029 assert_eq!(canceled.venue_order_id, Some(venue_order_id));
7030 "Canceled"
7031 }
7032 OrderEventAny::Expired(expired) => {
7033 assert_eq!(expired.client_order_id, client_order_id);
7034 assert_eq!(expired.venue_order_id, Some(venue_order_id));
7035 "Expired"
7036 }
7037 other => panic!("expected a terminal order event last, was {other:?}"),
7038 };
7039 assert_eq!(terminal, expected_terminal);
7040
7041 let mut order = OrderTestBuilder::new(OrderType::Limit)
7043 .instrument_id(instrument.id())
7044 .client_order_id(client_order_id)
7045 .strategy_id(StrategyId::from("S-001"))
7046 .side(OrderSide::Buy)
7047 .price(Price::from("0.5"))
7048 .quantity(Quantity::from("100"))
7049 .build();
7050
7051 for event in emitted {
7052 order.apply(event).expect("emitted sequence must be valid");
7053 }
7054
7055 assert_eq!(order.status(), expected_order_status);
7056 assert_eq!(order.filled_qty().as_decimal(), dec!(25));
7057 }
7058
7059 #[rstest]
7071 fn test_issue_3797_interleaved_cancel_fill_sequence() {
7072 use crate::common::{
7073 enums::{
7074 PolymarketEventType, PolymarketLiquiditySide, PolymarketOrderSide,
7075 PolymarketOrderStatus, PolymarketOrderType, PolymarketOutcome,
7076 PolymarketTradeStatus,
7077 },
7078 models::PolymarketMakerOrder,
7079 };
7080
7081 let instrument = bind_instrument(test_instrument(), "0x4134", PolymarketOutcome::yes());
7082 let asset_id = instrument.id().symbol.inner();
7083
7084 let order_id =
7085 "0xe743f6c823ecdfa9ddaaf08673b2441d15a38d89e14dcb25b3b70c284be4f6ad".to_string();
7086 let venue_order_id = VenueOrderId::from(order_id.as_str());
7087
7088 let token_instruments = AtomicMap::new();
7089 token_instruments.insert(asset_id, instrument.clone());
7090
7091 let fill_tracker = OrderFillTrackerMap::new();
7092 fill_tracker.register(
7093 venue_order_id,
7094 Quantity::from("20"),
7095 OrderSide::Buy,
7096 instrument.id(),
7097 instrument.size_precision(),
7098 instrument.price_precision(),
7099 );
7100
7101 let pending_submits = PendingSubmitTracker::default();
7102 let order_contexts = OrderContextRegistry::default();
7103 register_context(&order_contexts, venue_order_id, instrument.id(), "O-3797");
7104 order_contexts.mark_accepted(venue_order_id);
7105 let mut emitter = test_emitter();
7106 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
7107 emitter.set_sender(sender);
7108
7109 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
7110 settlement.mark_live();
7111 settlement.note_order_submitted(venue_order_id);
7112
7113 let ctx = WsDispatchContext {
7114 signer_type: PolymarketSignerType::Owner,
7115 token_instruments: &token_instruments,
7116 fill_tracker: &fill_tracker,
7117 settlement: &settlement,
7118 pending_submits: &pending_submits,
7119 order_contexts: &order_contexts,
7120 emitter: &emitter,
7121 account_id: AccountId::from("POLY-001"),
7122 clock: nautilus_core::time::get_atomic_clock_realtime(),
7123 user_address: "0xabc",
7124 user_api_key: "xxx",
7125 };
7126 let mut state = WsDispatchState::default();
7127
7128 let make_order =
7129 |size_matched: &str, ts: &str, event_type: PolymarketEventType| PolymarketUserOrder {
7130 asset_id,
7131 associate_trades: None,
7132 created_at: Some("1775074735".to_string()),
7133 expiration: Some("0".to_string()),
7134 id: order_id.clone(),
7135 maker_address: Some(Ustr::from("0xabc")),
7136 market: Ustr::from("0x4134"),
7137 order_owner: Some(Ustr::from("xxx")),
7138 order_type: Some(PolymarketOrderType::GTC),
7139 original_size: "20".to_string(),
7140 outcome: Some(PolymarketOutcome::yes()),
7141 owner: Ustr::from("xxx"),
7142 price: "0.18".to_string(),
7143 side: PolymarketOrderSide::Buy,
7144 size_matched: size_matched.to_string(),
7145 status: Some(PolymarketOrderStatus::Canceled.into()),
7146 timestamp: ts.to_string(),
7147 event_type,
7148 };
7149
7150 let make_trade = |trade_id: &str, matched_amount: f64, ts: &str| PolymarketUserTrade {
7151 asset_id,
7152 bucket_index: 0,
7153 fee_rate_bps: "1000".to_string(),
7154 id: trade_id.to_string(),
7155 last_update: "1775074738".to_string(),
7156 maker_address: Ustr::from("0xother"),
7157 maker_orders: vec![PolymarketMakerOrder {
7158 asset_id,
7159 maker_address: "0xabc".to_string(),
7160 matched_amount: Decimal::from_str_exact(&matched_amount.to_string())
7161 .unwrap_or(Decimal::ZERO),
7162 order_id: order_id.clone(),
7163 outcome: PolymarketOutcome::yes(),
7164 owner: "xxx".to_string(),
7165 price: Decimal::from_str_exact("0.18").unwrap_or(Decimal::ZERO),
7166 side: None,
7167 }],
7168 market: Ustr::from("0x4134"),
7169 match_time: "1775074735".to_string(),
7170 outcome: PolymarketOutcome::yes(),
7171 owner: Ustr::from("other-owner"),
7172 price: "0.82".to_string(),
7173 side: PolymarketOrderSide::Buy,
7174 size: "1.219511".to_string(),
7175 status: PolymarketTradeStatus::Confirmed,
7176 taker_order_id: "0xtaker01".to_string(),
7177 timestamp: ts.to_string(),
7178 trade_owner: Ustr::from("other-owner"),
7179 transaction_hash: None,
7180 trader_side: PolymarketLiquiditySide::Maker,
7181 event_type: PolymarketEventType::Trade,
7182 };
7183
7184 let msg_a = make_order("0", "1775074738031", PolymarketEventType::Cancellation);
7186 dispatch_user_message(&UserWsMessage::Order(msg_a), &ctx, &mut state);
7187
7188 let evt = receiver.try_recv().expect("(A) canceled event");
7189 match &evt {
7190 ExecutionEvent::Order(OrderEventAny::Canceled(c)) => {
7191 assert_eq!(c.venue_order_id, Some(venue_order_id));
7192 }
7193 other => panic!("(A) expected canceled event, was {other:?}"),
7194 }
7195
7196 let msg_b = make_trade("trade-b", 1.219511, "1775074738032");
7198 dispatch_user_message(&UserWsMessage::Trade(msg_b), &ctx, &mut state);
7199
7200 let evt = receiver.try_recv().expect("(B) filled event");
7201 match &evt {
7202 ExecutionEvent::Order(OrderEventAny::Filled(f)) => {
7203 assert_eq!(f.venue_order_id, venue_order_id);
7204 }
7205 other => panic!("(B) expected filled event, was {other:?}"),
7206 }
7207 let evt = receiver.try_recv().expect("(B) re-emitted cancel");
7209 match &evt {
7210 ExecutionEvent::Order(OrderEventAny::Canceled(c)) => {
7211 assert_eq!(c.venue_order_id, Some(venue_order_id));
7212 }
7213 other => panic!("(B) expected re-emitted cancel, was {other:?}"),
7214 }
7215
7216 let msg_c = make_order("1.219511", "1775074738034", PolymarketEventType::Update);
7218 dispatch_user_message(&UserWsMessage::Order(msg_c), &ctx, &mut state);
7219
7220 let evt = receiver.try_recv().expect("(C) canceled event");
7221 match &evt {
7222 ExecutionEvent::Order(OrderEventAny::Canceled(c)) => {
7223 assert_eq!(c.venue_order_id, Some(venue_order_id));
7224 }
7225 other => panic!("(C) expected canceled event, was {other:?}"),
7226 }
7227
7228 let msg_d = make_order("2.560972", "1775074738038", PolymarketEventType::Update);
7230 dispatch_user_message(&UserWsMessage::Order(msg_d), &ctx, &mut state);
7231
7232 let evt = receiver.try_recv().expect("(D) canceled event");
7233 match &evt {
7234 ExecutionEvent::Order(OrderEventAny::Canceled(c)) => {
7235 assert_eq!(c.venue_order_id, Some(venue_order_id));
7236 }
7237 other => panic!("(D) expected canceled event, was {other:?}"),
7238 }
7239
7240 let msg_e = make_trade("trade-e", 1.341461, "1775074738036");
7242 dispatch_user_message(&UserWsMessage::Trade(msg_e), &ctx, &mut state);
7243
7244 let evt = receiver.try_recv().expect("(E) filled event");
7245 match &evt {
7246 ExecutionEvent::Order(OrderEventAny::Filled(f)) => {
7247 assert_eq!(f.venue_order_id, venue_order_id);
7248 }
7249 other => panic!("(E) expected filled event, was {other:?}"),
7250 }
7251
7252 let evt = receiver.try_recv().expect("(E) re-emitted cancel");
7254 match &evt {
7255 ExecutionEvent::Order(OrderEventAny::Canceled(c)) => {
7256 assert_eq!(c.venue_order_id, Some(venue_order_id));
7257 }
7258 other => panic!("(E) expected re-emitted cancel, was {other:?}"),
7259 }
7260
7261 assert!(
7263 receiver.try_recv().is_err(),
7264 "No further events expected after the sequence"
7265 );
7266 }
7267
7268 #[rstest]
7269 fn test_dispatch_taker_fill_bumps_qty_for_dust_overfill() {
7270 use crate::common::enums::{
7274 PolymarketEventType, PolymarketOrderSide, PolymarketOutcome, PolymarketTradeStatus,
7275 };
7276
7277 let instrument = bind_instrument(test_instrument(), "0xmarket", PolymarketOutcome::yes());
7278 let asset_id = instrument.id().symbol.inner();
7279 let token_instruments = AtomicMap::new();
7280 token_instruments.insert(asset_id, instrument.clone());
7281
7282 let fill_tracker = OrderFillTrackerMap::new();
7283 let venue_order_id = VenueOrderId::from("0xtaker-overfill");
7284 let submitted =
7285 Quantity::from_decimal_dp(dec!(7.5757), instrument.size_precision()).unwrap();
7286 fill_tracker.register(
7287 venue_order_id,
7288 submitted,
7289 OrderSide::Buy,
7290 instrument.id(),
7291 instrument.size_precision(),
7292 instrument.price_precision(),
7293 );
7294
7295 let pending_submits = PendingSubmitTracker::default();
7296 let order_contexts = OrderContextRegistry::default();
7297 register_context(
7298 &order_contexts,
7299 venue_order_id,
7300 instrument.id(),
7301 "O-OVERFILL",
7302 );
7303 order_contexts.mark_accepted(venue_order_id);
7304 let mut emitter = test_emitter();
7305 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
7306 emitter.set_sender(sender);
7307
7308 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
7309 settlement.mark_live();
7310 settlement.note_order_submitted(venue_order_id);
7311
7312 let ctx = WsDispatchContext {
7313 signer_type: PolymarketSignerType::Owner,
7314 token_instruments: &token_instruments,
7315 fill_tracker: &fill_tracker,
7316 settlement: &settlement,
7317 pending_submits: &pending_submits,
7318 order_contexts: &order_contexts,
7319 emitter: &emitter,
7320 account_id: AccountId::from("POLY-001"),
7321 clock: nautilus_core::time::get_atomic_clock_realtime(),
7322 user_address: "0xtest",
7323 user_api_key: "00000000-0000-0000-0000-000000000001",
7324 };
7325 let mut state = WsDispatchState::default();
7326
7327 let trade = PolymarketUserTrade {
7328 asset_id,
7329 bucket_index: 0,
7330 fee_rate_bps: "0".to_string(),
7331 id: "trade-overfill".to_string(),
7332 last_update: "1700000001".to_string(),
7333 maker_address: Ustr::from("0xmaker"),
7334 maker_orders: vec![],
7335 market: Ustr::from("0xmarket"),
7336 match_time: "1700000000".to_string(),
7337 outcome: PolymarketOutcome::yes(),
7338 owner: Ustr::from("00000000-0000-0000-0000-000000000001"),
7339 price: "0.66".to_string(),
7340 side: PolymarketOrderSide::Buy,
7341 size: "7.575758".to_string(),
7342 status: PolymarketTradeStatus::Confirmed,
7343 taker_order_id: venue_order_id.as_str().to_string(),
7344 timestamp: "1700000000000".to_string(),
7345 trade_owner: Ustr::from("00000000-0000-0000-0000-000000000001"),
7346 transaction_hash: None,
7347 trader_side: PolymarketLiquiditySide::Taker,
7348 event_type: PolymarketEventType::Trade,
7349 };
7350
7351 dispatch_user_message(&UserWsMessage::Trade(trade), &ctx, &mut state);
7352
7353 let venue_qty =
7354 Quantity::from_decimal_dp(dec!(7.575758), instrument.size_precision()).unwrap();
7355 let cumulative = fill_tracker.get_cumulative_filled(&venue_order_id);
7356 let updated = receiver.try_recv().expect("expected an updated event");
7357 let filled = receiver.try_recv().expect("expected a filled event");
7358
7359 assert_eq!(cumulative, Some(venue_qty));
7360 assert_eq!(fill_tracker.submitted_qty(&venue_order_id), Some(venue_qty));
7361
7362 match updated {
7363 ExecutionEvent::Order(OrderEventAny::Updated(updated)) => {
7364 assert_eq!(updated.quantity, venue_qty);
7365 assert_eq!(updated.venue_order_id, Some(venue_order_id));
7366 }
7367 other => panic!("expected updated event, was {other:?}"),
7368 }
7369
7370 match filled {
7371 ExecutionEvent::Order(OrderEventAny::Filled(filled)) => {
7372 assert_eq!(filled.last_qty, venue_qty);
7373 assert_eq!(filled.venue_order_id, venue_order_id);
7374 }
7375 other => panic!("expected filled event, was {other:?}"),
7376 }
7377
7378 assert!(receiver.try_recv().is_err());
7379 }
7380
7381 #[rstest]
7382 #[case(
7383 TimeInForce::Ioc,
7384 OrderType::Market,
7385 OrderSide::Buy,
7386 "5.202910",
7387 "5.202897",
7388 false,
7389 true
7390 )]
7391 #[case(
7392 TimeInForce::Fok,
7393 OrderType::Limit,
7394 OrderSide::Buy,
7395 "5.202910",
7396 "5.202897",
7397 true,
7398 false
7399 )]
7400 #[case(
7401 TimeInForce::Ioc,
7402 OrderType::Limit,
7403 OrderSide::Buy,
7404 "30",
7405 "20",
7406 false,
7407 true
7408 )]
7409 #[case(
7410 TimeInForce::Ioc,
7411 OrderType::Market,
7412 OrderSide::Sell,
7413 "5.202910",
7414 "5.202897",
7415 false,
7416 true
7417 )]
7418 #[case(
7419 TimeInForce::Gtc,
7420 OrderType::Limit,
7421 OrderSide::Buy,
7422 "5.202910",
7423 "5.202897",
7424 false,
7425 false
7426 )]
7427 fn test_taker_terminal_status_on_trade_confirm(
7428 #[case] time_in_force: TimeInForce,
7429 #[case] order_type: OrderType,
7430 #[case] order_side: OrderSide,
7431 #[case] submitted_qty: &str,
7432 #[case] fill_qty: &str,
7433 #[case] expect_normalization: bool,
7434 #[case] expect_cancel: bool,
7435 ) {
7436 use crate::common::enums::{
7440 PolymarketEventType, PolymarketOrderSide, PolymarketOutcome, PolymarketTradeStatus,
7441 };
7442
7443 let instrument = bind_instrument(test_instrument(), "0xmarket", PolymarketOutcome::yes());
7444 let asset_id = instrument.id().symbol.inner();
7445 let token_instruments = AtomicMap::new();
7446 token_instruments.insert(asset_id, instrument.clone());
7447
7448 let fill_tracker = OrderFillTrackerMap::new();
7449 let venue_order_id = VenueOrderId::from("0xtaker-one-shot-dust");
7450 let submitted = Quantity::from_decimal_dp(
7451 Decimal::from_str_exact(submitted_qty).unwrap(),
7452 instrument.size_precision(),
7453 )
7454 .unwrap();
7455 fill_tracker.register(
7456 venue_order_id,
7457 submitted,
7458 order_side,
7459 instrument.id(),
7460 instrument.size_precision(),
7461 instrument.price_precision(),
7462 );
7463
7464 let pending_submits = PendingSubmitTracker::default();
7465 let order_contexts = OrderContextRegistry::default();
7466 order_contexts.register_context(
7467 venue_order_id,
7468 OrderContext {
7469 identity: OrderIdentity {
7470 client_order_id: ClientOrderId::from("O-ONE-SHOT"),
7471 strategy_id: StrategyId::from("S-001"),
7472 instrument_id: instrument.id(),
7473 order_side,
7474 order_type,
7475 },
7476 quantity: submitted,
7477 price: Some(Price::from("0.50")),
7478 trigger_price: None,
7479 trigger_type: None,
7480 time_in_force,
7481 is_post_only: false,
7482 is_reduce_only: false,
7483 is_quote_quantity: false,
7484 },
7485 );
7486 order_contexts.mark_accepted(venue_order_id);
7487 let mut emitter = test_emitter();
7488 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
7489 emitter.set_sender(sender);
7490
7491 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
7492 settlement.mark_live();
7493 settlement.note_order_submitted(venue_order_id);
7494
7495 let ctx = WsDispatchContext {
7496 signer_type: PolymarketSignerType::Owner,
7497 token_instruments: &token_instruments,
7498 fill_tracker: &fill_tracker,
7499 settlement: &settlement,
7500 pending_submits: &pending_submits,
7501 order_contexts: &order_contexts,
7502 emitter: &emitter,
7503 account_id: AccountId::from("POLY-001"),
7504 clock: nautilus_core::time::get_atomic_clock_realtime(),
7505 user_address: "0xtest",
7506 user_api_key: "00000000-0000-0000-0000-000000000001",
7507 };
7508 let mut state = WsDispatchState::default();
7509
7510 let trade = PolymarketUserTrade {
7511 asset_id,
7512 bucket_index: 0,
7513 fee_rate_bps: "0".to_string(),
7514 id: "trade-one-shot-dust".to_string(),
7515 last_update: "1700000001".to_string(),
7516 maker_address: Ustr::from("0xmaker"),
7517 maker_orders: vec![],
7518 market: Ustr::from("0xmarket"),
7519 match_time: "1700000000".to_string(),
7520 outcome: PolymarketOutcome::yes(),
7521 owner: Ustr::from("00000000-0000-0000-0000-000000000001"),
7522 price: "0.963".to_string(),
7523 side: if order_side == OrderSide::Buy {
7524 PolymarketOrderSide::Buy
7525 } else {
7526 PolymarketOrderSide::Sell
7527 },
7528 size: fill_qty.to_string(),
7529 status: PolymarketTradeStatus::Confirmed,
7530 taker_order_id: venue_order_id.as_str().to_string(),
7531 timestamp: "1700000000000".to_string(),
7532 trade_owner: Ustr::from("00000000-0000-0000-0000-000000000001"),
7533 transaction_hash: None,
7534 trader_side: PolymarketLiquiditySide::Taker,
7535 event_type: PolymarketEventType::Trade,
7536 };
7537
7538 dispatch_user_message(&UserWsMessage::Trade(trade), &ctx, &mut state);
7539
7540 let event = receiver.try_recv().expect("expected the venue fill event");
7541 match event {
7542 ExecutionEvent::Order(OrderEventAny::Filled(filled)) => {
7543 assert_eq!(
7544 filled.last_qty,
7545 Quantity::from_decimal_dp(
7546 Decimal::from_str_exact(fill_qty).unwrap(),
7547 instrument.size_precision(),
7548 )
7549 .unwrap(),
7550 );
7551 }
7552 other => panic!("expected filled event, was {other:?}"),
7553 }
7554
7555 if expect_normalization {
7556 let event = receiver.try_recv().expect("expected quantity update");
7557 match event {
7558 ExecutionEvent::Order(OrderEventAny::Updated(updated)) => {
7559 assert_eq!(
7560 updated.quantity,
7561 Quantity::new(5.202897, instrument.size_precision()),
7562 );
7563 assert_eq!(updated.venue_order_id, Some(venue_order_id));
7564 assert!(updated.reconciliation);
7565 }
7566 other => panic!("expected updated event, was {other:?}"),
7567 }
7568 assert!(
7569 fill_tracker
7570 .get_cumulative_filled(&venue_order_id)
7571 .is_none(),
7572 "order must be settled and removed from the tracker",
7573 );
7574 } else if expect_cancel {
7575 let event = receiver.try_recv().expect("expected IOC cancellation");
7576 match event {
7577 ExecutionEvent::Order(OrderEventAny::Canceled(canceled)) => {
7578 assert_eq!(canceled.venue_order_id, Some(venue_order_id));
7579 }
7580 other => panic!("expected canceled event, was {other:?}"),
7581 }
7582 assert!(
7583 fill_tracker
7584 .get_cumulative_filled(&venue_order_id)
7585 .is_none(),
7586 "canceled IOC must be settled and removed from the tracker",
7587 );
7588 } else {
7589 assert!(
7590 receiver.try_recv().is_err(),
7591 "resting order must not receive a terminal event",
7592 );
7593 assert!(
7594 fill_tracker
7595 .get_cumulative_filled(&venue_order_id)
7596 .is_some(),
7597 "ineligible order must stay tracked with open leaves",
7598 );
7599 }
7600 }
7601
7602 #[rstest]
7603 fn test_dispatch_taker_fill_gross_overfill_raises_qty_then_fills() {
7604 use crate::common::enums::{
7608 PolymarketEventType, PolymarketOrderSide, PolymarketOutcome, PolymarketTradeStatus,
7609 };
7610
7611 let instrument = bind_instrument(test_instrument(), "0xmarket", PolymarketOutcome::yes());
7612 let asset_id = instrument.id().symbol.inner();
7613 let size_precision = instrument.size_precision();
7614 let token_instruments = AtomicMap::new();
7615 token_instruments.insert(asset_id, instrument.clone());
7616
7617 let fill_tracker = OrderFillTrackerMap::new();
7618 let venue_order_id = VenueOrderId::from("0xtaker-gross-overfill");
7619 let submitted = Quantity::new(30.0, size_precision);
7620 fill_tracker.register(
7621 venue_order_id,
7622 submitted,
7623 OrderSide::Buy,
7624 instrument.id(),
7625 size_precision,
7626 instrument.price_precision(),
7627 );
7628
7629 let pending_submits = PendingSubmitTracker::default();
7630 let order_contexts = OrderContextRegistry::default();
7631 register_context(
7632 &order_contexts,
7633 venue_order_id,
7634 instrument.id(),
7635 "O-GROSS-OVERFILL",
7636 );
7637 order_contexts.mark_accepted(venue_order_id);
7638 let mut emitter = test_emitter();
7639 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
7640 emitter.set_sender(sender);
7641
7642 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
7643 settlement.mark_live();
7644 settlement.note_order_submitted(venue_order_id);
7645
7646 let ctx = WsDispatchContext {
7647 signer_type: PolymarketSignerType::Owner,
7648 token_instruments: &token_instruments,
7649 fill_tracker: &fill_tracker,
7650 settlement: &settlement,
7651 pending_submits: &pending_submits,
7652 order_contexts: &order_contexts,
7653 emitter: &emitter,
7654 account_id: AccountId::from("POLY-001"),
7655 clock: nautilus_core::time::get_atomic_clock_realtime(),
7656 user_address: "0xtest",
7657 user_api_key: "00000000-0000-0000-0000-000000000001",
7658 };
7659 let mut state = WsDispatchState::default();
7660
7661 let trade = PolymarketUserTrade {
7663 asset_id,
7664 bucket_index: 0,
7665 fee_rate_bps: "0".to_string(),
7666 id: "trade-gross-overfill".to_string(),
7667 last_update: "1700000001".to_string(),
7668 maker_address: Ustr::from("0xmaker"),
7669 maker_orders: vec![],
7670 market: Ustr::from("0xmarket"),
7671 match_time: "1700000000".to_string(),
7672 outcome: PolymarketOutcome::yes(),
7673 owner: Ustr::from("00000000-0000-0000-0000-000000000001"),
7674 price: "0.014".to_string(),
7675 side: PolymarketOrderSide::Buy,
7676 size: "33.846152".to_string(),
7677 status: PolymarketTradeStatus::Confirmed,
7678 taker_order_id: venue_order_id.as_str().to_string(),
7679 timestamp: "1700000000000".to_string(),
7680 trade_owner: Ustr::from("00000000-0000-0000-0000-000000000001"),
7681 transaction_hash: None,
7682 trader_side: PolymarketLiquiditySide::Taker,
7683 event_type: PolymarketEventType::Trade,
7684 };
7685
7686 dispatch_user_message(&UserWsMessage::Trade(trade), &ctx, &mut state);
7687
7688 let expected_qty = Quantity::new(33.846152, size_precision);
7689
7690 match receiver.try_recv().expect("expected an updated event") {
7692 ExecutionEvent::Order(OrderEventAny::Updated(updated)) => {
7693 assert_eq!(updated.quantity, expected_qty);
7694 assert_eq!(updated.venue_order_id, Some(venue_order_id));
7695 }
7696 other => panic!("expected updated event raising qty to the fill, was {other:?}"),
7697 }
7698
7699 match receiver.try_recv().expect("expected a filled event") {
7700 ExecutionEvent::Order(OrderEventAny::Filled(filled)) => {
7701 assert_eq!(filled.last_qty, expected_qty);
7702 assert_eq!(filled.venue_order_id, venue_order_id);
7703 }
7704 other => panic!("expected filled event, was {other:?}"),
7705 }
7706 }
7707
7708 #[rstest]
7711 #[case(
7712 crate::common::enums::PolymarketOrderStatus::Unmatched,
7713 Some("invalid post-only order: order crosses book"),
7714 "Rejected"
7715 )]
7716 #[case(
7717 crate::common::enums::PolymarketOrderStatus::CanceledMarketResolved,
7718 None,
7719 "Expired"
7720 )]
7721 fn test_dispatch_order_terminal_status_emits_event(
7722 #[case] status: crate::common::enums::PolymarketOrderStatus,
7723 #[case] reason: Option<&str>,
7724 #[case] expected: &str,
7725 ) {
7726 use crate::common::enums::{
7727 PolymarketEventType, PolymarketOrderSide, PolymarketOrderType, PolymarketOutcome,
7728 };
7729
7730 let instrument = test_instrument();
7731 let asset_id = instrument.id().symbol.inner();
7732 let order_id = "0xterminal-order".to_string();
7733 let venue_order_id = VenueOrderId::from(order_id.as_str());
7734
7735 let token_instruments = AtomicMap::new();
7736 token_instruments.insert(asset_id, instrument.clone());
7737
7738 let fill_tracker = OrderFillTrackerMap::new();
7739 fill_tracker.register(
7740 venue_order_id,
7741 Quantity::from("10"),
7742 OrderSide::Buy,
7743 instrument.id(),
7744 instrument.size_precision(),
7745 instrument.price_precision(),
7746 );
7747
7748 let pending_submits = PendingSubmitTracker::default();
7749 let order_contexts = OrderContextRegistry::default();
7750 register_context(
7751 &order_contexts,
7752 venue_order_id,
7753 instrument.id(),
7754 "O-TERMINAL",
7755 );
7756 order_contexts.mark_accepted(venue_order_id);
7757 let mut emitter = test_emitter();
7758 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
7759 emitter.set_sender(sender);
7760
7761 let settlement = SettlementRegistry::new(AccountId::from("POLY-001"));
7762 settlement.mark_live();
7763 settlement.note_order_submitted(venue_order_id);
7764
7765 let ctx = WsDispatchContext {
7766 signer_type: PolymarketSignerType::Owner,
7767 token_instruments: &token_instruments,
7768 fill_tracker: &fill_tracker,
7769 settlement: &settlement,
7770 pending_submits: &pending_submits,
7771 order_contexts: &order_contexts,
7772 emitter: &emitter,
7773 account_id: AccountId::from("POLY-001"),
7774 clock: nautilus_core::time::get_atomic_clock_realtime(),
7775 user_address: "0xabc",
7776 user_api_key: "xxx",
7777 };
7778 let mut state = WsDispatchState::default();
7779
7780 let order = PolymarketUserOrder {
7781 asset_id,
7782 associate_trades: None,
7783 created_at: Some("1775074735".to_string()),
7784 expiration: Some("0".to_string()),
7785 id: order_id,
7786 maker_address: Some(Ustr::from("0xabc")),
7787 market: Ustr::from("0x4134"),
7788 order_owner: Some(Ustr::from("xxx")),
7789 order_type: Some(PolymarketOrderType::FOK),
7790 original_size: "10".to_string(),
7791 outcome: Some(PolymarketOutcome::yes()),
7792 owner: Ustr::from("xxx"),
7793 price: "0.50".to_string(),
7794 side: PolymarketOrderSide::Buy,
7795 size_matched: "0".to_string(),
7796 status: Some(PolymarketUserOrderStatus::new(status, reason)),
7797 timestamp: "1775074738031".to_string(),
7798 event_type: PolymarketEventType::Placement,
7799 };
7800
7801 dispatch_user_message(&UserWsMessage::Order(order), &ctx, &mut state);
7802
7803 let event = receiver.try_recv().expect("expected terminal order event");
7804 match event {
7805 ExecutionEvent::Order(order_event) => {
7806 assert!(
7807 format!("{order_event:?}").starts_with(expected),
7808 "expected {expected}, was {order_event:?}"
7809 );
7810 assert_eq!(
7811 order_event.client_order_id(),
7812 ClientOrderId::from("O-TERMINAL")
7813 );
7814
7815 if let OrderEventAny::Rejected(rejected) = order_event {
7816 assert_eq!(
7817 rejected.reason,
7818 "invalid post-only order: order crosses book"
7819 );
7820 assert!(rejected.due_post_only);
7821 }
7822 }
7823 other => panic!("expected order event, was {other:?}"),
7824 }
7825 }
7826}