Skip to main content

nautilus_polymarket/websocket/
dispatch.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! WebSocket message dispatch for the Polymarket execution client.
17//!
18//! Routes user-channel WS messages (order updates and trades) for orders submitted through this
19//! client into Nautilus order events (`OrderAccepted` / `OrderFilled` / `OrderFillVoided` /
20//! `OrderCanceled` / `OrderRejected` / `OrderExpired`), building them from the context captured at
21//! submit (`OrderContextRegistry`). Order-channel messages drive lifecycle events; trade-channel
22//! messages drive fills, and acceptance is synthesized before a fill or cancel that races ahead.
23//! Messages are emitted once the order is known (accepted, or with a submit in flight), otherwise
24//! buffered until acceptance. Reports are reserved for the `generate_*` query and reconciliation
25//! methods.
26//!
27//! Trade evidence is admitted through the shared settlement boundary and governed by the
28//! settlement registry: provisional and confirmed stream statuses apply legs once per
29//! uninterrupted session, failed or conflicting stream evidence quarantines the trade for
30//! targeted terminal REST resolution, and only a REST-established `FAILED` outcome voids
31//! applied fills or tombstones absent legs.
32//!
33//! Execution-client and dispatch replacement confirmations share one promotion under the dispatch
34//! lock. Execution-client confirmation uses logical-order context and local event time. Dispatch
35//! confirmation uses old-leg context and stream or REST evidence time, with local time as fallback
36//! for invalid order-update timestamps. Promotion publishes `OrderUpdated` before registering and
37//! releasing fills.
38//!
39//! Execution-client confirmation drains the returned activity immediately; dispatch handlers retain
40//! their own emission order. Execution-client confirmation sets drained fill `ts_init` at emission;
41//! dispatch preserves buffered `ts_init`.
42
43use 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/// Signal returned when a finalized trade requires an async account refresh.
100#[derive(Debug)]
101pub(crate) struct AccountRefreshRequest;
102
103/// Mutable state retained across user WebSocket stream generations.
104///
105/// Terminal cancel reports are re-emitted after fills to restore terminal state when fills race
106/// ahead of or arrive after cancel messages.
107#[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    /// Promotes the winning replacement and releases its buffered activity after `OrderUpdated`.
202    ///
203    /// Source context resolution follows the consuming claim. Event time is resolved after
204    /// context registration, while initialization time uses the caller's local clock.
205    ///
206    /// # Errors
207    ///
208    /// Returns `None` if no pending replacement matches or its source context cannot be resolved.
209    /// Failed context resolution still consumes the claim.
210    #[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/// Buffered activity released by a replacement promotion after its update is published.
568#[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
581/// Immutable context borrowed from the async block's owned values.
582pub(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
626/// Top-level router: synchronous, returns signal for async account refresh.
627pub(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    // Order updates can race ahead of trade messages, so cap filled_qty
716    // to what the fill tracker has recorded to prevent duplicate inferred fills
717    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    // Track cancel reports so we can re-emit them after late-arriving fills.
729    // Saved regardless of acceptance state so that cancels arriving during
730    // the HTTP round-trip are available once the order is later accepted.
731    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    // Tracked own orders route through order events; externally-managed orders
746    // (no captured context) buffer until accepted or fall back to reports.
747    let context = ctx.order_contexts.get(&venue_order_id);
748
749    // Emit fills first: a terminal status would otherwise close the order ahead of them
750    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        // Registered between the early accepted-check and here: emit rather than buffer
789        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
809/// Takes the buffered fills released by an order status update and reports whether the order is
810/// accepted; a known own order (submit in flight or outcome unknown) self-registers on its first
811/// update.
812fn 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
947/// Routes a buffered fill for an order without captured context through the report path, if
948/// its trade still permits application.
949fn 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
1006/// Emits the terminal order event for a taker order once its trade confirms.
1007///
1008/// Taker fills receive no order-channel `MATCHED` update. FOK is atomic, so a sub-cent quantity
1009/// difference can be normalized. IOC maps to FAK, so every positive remainder was killed by the
1010/// venue and must close as `Canceled` without changing the venue-reported fill quantity. A hard
1011/// fault withdraws that authority and does not consume the tracker's one emission.
1012fn 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    // Quantity normalization and taker terminal statuses key off trade confirmation,
1087    // and a hard fault keeps the confirmed outcome while withdrawing that bookkeeping.
1088    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
1099/// Executes the effects a registry transition authorized, in order.
1100pub(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
1130/// Applies a targeted REST trade result through the settlement registry, then runs the same
1131/// confirmation bookkeeping as a stream confirmation once the trade is confirmed.
1132///
1133/// Evidence that fails admission leaves the trade pending for the next targeted read.
1134pub(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
1155/// Applies REST evidence for an order whose submit outcome was unknown, returning `true` once
1156/// its venue state is applied.
1157///
1158/// Terminal trades pass through the settlement registry first, so their fills queue behind the
1159/// order's acceptance; the order status then registers and accepts the order and releases them.
1160/// The status is withheld while any trade of the order is still provisional, or while confirmed
1161/// trades do not yet cover the venue's matched quantity, so the order never reports a state
1162/// ahead of its fills. Locally closed orders and replaced-leg cancellations still apply trades
1163/// but do not emit the order status.
1164pub(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    // Venue `Matched` is not a cancel, and the parsed `Canceled` status is the IOC remainder rule
1216    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    // A filled taker order reaches the terminal normalization a stream confirmation would apply,
1226    // and this check also covers a faulted trade in this slice whose order was not stored.
1227    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
1254/// Applies REST trades for an order that was live while the user stream was disconnected,
1255/// returning `true` once they cover the venue's matched quantity.
1256///
1257/// Only trades the settlement registry has never seen are applied, so each missed fill applies
1258/// once and trades it already holds keep their established resolution. The order status is not
1259/// applied: the stream and reconciliation keep driving the lifecycle of an accepted order.
1260pub(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
1302/// Returns the captured context and instrument of a submitted order, or `None` when they are
1303/// unknown or its REST order evidence contradicts them.
1304fn 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
1330/// Admits the REST trades touching an order and passes each to `apply`, returning `true` once
1331/// every trade is terminal and the confirmed trades cover the venue's matched quantity.
1332///
1333/// Nothing is applied while any trade is still provisional.
1334fn 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    // REST can list the order as matched before its trades appear
1381    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
1393/// Returns the distinct REST trades touching `venue_order_id`, or `None` when pagination repeats
1394/// a trade with contradictory evidence.
1395fn 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
1424/// Converts an admitted leg into a fill report and routes it through the established emission
1425/// machinery: modify promotion first, then tracked-order emission or buffered delivery, with
1426/// report fallback for orders without captured context.
1427fn 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    // Already delivered through a reconciliation fill report
1437    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
1490/// Confirmation bookkeeping beyond the settlement registry: terminal quantity normalization
1491/// and taker terminal statuses once a trade confirms through stream or REST evidence.
1492fn 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
1545/// Re-emits a saved cancel report after a fill to restore terminal state.
1546///
1547/// When fills race ahead of (or arrive after) cancel messages, the order can
1548/// get stuck in `PartiallyFilled`. This re-emission ensures the execution
1549/// engine transitions the order back to `Canceled`.
1550///
1551/// Skips re-emission when the fill tracker shows the order is fully filled,
1552/// because `Filled` is already terminal and a spurious cancel would fail
1553/// the `Filled -> Canceled` state transition.
1554fn 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    // Unfilled FOK cancellations carry an empty size_matched in captured venue messages
1628    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
1674/// Maps a venue `CANCELED` on a GTD order to `Expired` once the cancel is in the expiry window.
1675///
1676/// Polymarket reports both a user cancel and a GTD expiry as `CANCELED`, and expires a GTD
1677/// order one minute before its stated expiration. A cancel earlier than that window stays
1678/// `Canceled`.
1679fn 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
1703/// Converts a venue-reported `original_size` on a user-channel order message into shares.
1704///
1705/// The venue echoes the signed `makerAmount`, which for a BUY is the pUSD budget rather than a
1706/// share count (see `compute_maker_taker_amounts`). Dividing by the order price recovers the
1707/// signed `takerAmount`, which is the share quantity the client submitted.
1708///
1709/// This is confirmed for the market order types (`FAK` and `FOK`), where a BUY at 0.01 for 100
1710/// shares reports `1`. A SELL signs shares as its maker amount and needs no conversion. Resting
1711/// types pass through unchanged: their denomination is unconfirmed, and converting a
1712/// share-denominated size would misreport every externally-managed resting order.
1713fn 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
1733/// Emits order events for a tracked own-order status update.
1734///
1735/// Order-channel messages drive lifecycle events only; fills arrive separately on the trade
1736/// channel as `OrderFilled`. `PartiallyFilled` / `Filled` statuses therefore emit no fill here,
1737/// they only ensure acceptance has been emitted so the order lifecycle stays well-formed.
1738fn 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
1770/// Emits `OrderAccepted` at most once for a tracked order across submit and stream evidence.
1771///
1772/// Locally closed orders consume the acceptance marker without emitting an event, so late fills
1773/// cannot replay acceptance even if the order subsequently reopens.
1774fn 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
1802/// Builds and emits an `OrderFilled` event for a tracked order, synthesizing acceptance first.
1803///
1804/// `info` carries the venue fill metadata (the raw trade fields) for trade-sourced fills, and is
1805/// `None` for order-path fills that have no originating trade payload.
1806fn 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    // Pending before sending, so a decline observed during delivery finds the leg in flight
1821    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
1855/// Emits the one required `OrderFillVoided` for an applied leg of a REST-established `FAILED`
1856/// trade, constructed from the canonical applied fill published by core.
1857pub(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
1896/// Flattens a REST trade report into a string map of venue fill metadata for
1897/// `OrderFilled.info`.
1898pub(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
1903/// Flattens a user trade into a string map of venue fill metadata for `OrderFilled.info`.
1904///
1905/// Mirrors the v1 adapter, which attaches the full raw trade to each fill it generates. Scalar
1906/// fields map to their string form; nested fields (such as `maker_orders`) become their JSON text.
1907fn 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
1925/// Emits an `OrderUpdated` raising the order quantity to the actual BUY fill, before the fill.
1926///
1927/// A Polymarket BUY is bounded by the USDC it spends, so a marketable fill below the limit price
1928/// returns more shares than the nominal quantity. The engine rejects a fill past the order
1929/// quantity, so the quantity is raised first. The price is left unchanged (`None`).
1930fn 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
1958/// Emits an order-only reconciliation update which cannot change strategy position.
1959fn 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    /// Registers a tracked-order context so the dispatch routes the order through events.
2088    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        // A resting BUY already reports shares, so its size passes through unconverted
2829        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    // A market-order-type BUY reports the signed pUSD maker amount, so shares come from
2993    // dividing by the price. A SELL and the resting types already report shares.
2994    #[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    // A non-terminating division still rounds to the instrument's size precision
3050    #[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        // No registration: the submit response has not landed, so the order update registers it
3093        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        // The venue reported 1.01 pUSD for the 101 shares submitted at 0.01
3130        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        // No context registered, so the order surfaces as a report for reconciliation
3159        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        // The venue match time, not the message timestamp or the local clock
3245        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        // Every raw trade field is captured (mirrors v1 info=msg.to_dict()).
3446        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        // Numeric fields flatten to their string form.
3454        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        // The `type` serde-rename key is preserved.
3461        assert_eq!(info[&Ustr::from("type")], Ustr::from("TRADE"));
3462        // Nested fields become their JSON text.
3463        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        // Order not registered in fill_tracker, so should be buffered
3508        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        // The tracked own order emits an OrderAccepted event carrying the client order ID.
3606        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        // First dispatch processes the trade
3812        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        // Second dispatch should be deduped, no additional fill
3816        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        // Matching evidence after quarantine remains REST-gated: no emission.
3888        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        // A fill rebuilt from the cache at a snapped quantity that REST would contradict
5009        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        // MATCHED applies once for a current-session order.
5128        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        // The applied fill resolves the pending application through observation.
5141        settlement.observe_fill_applied(&filled);
5142
5143        // A stream FAILED update quarantines; it never voids directly.
5144        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        // A targeted terminal REST FAILED result voids the observed fill once.
5158        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        // A stale MATCHED update after the terminal REST outcome is absorbed as a tombstone.
5199        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        // Register order so it is "accepted" but with no fills tracked
5373        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        // No context registered, so the order surfaces as a report (the external/reconciliation
5384        // fallback), where filled_qty is capped to tracked fills.
5385        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        // Register and record a partial fill (50 of 100)
5435        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        // No context registered, so the order surfaces as a report (the external/reconciliation
5447        // fallback), where filled_qty is capped to tracked fills.
5448        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        // Register order as accepted with original qty=100
5656        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        // Step 1: Dispatch cancel (simulates message A from the bug)
5693        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        // Step 2: Dispatch trade fill (simulates trade arriving after cancel)
5703        dispatch_user_message(&UserWsMessage::Trade(trade), &ctx, &mut state);
5704
5705        // Should get: filled event, then re-emitted canceled event
5706        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        // The old venue order filled 20 of 120, so the replacement carries the other 100
6356        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        // The reconciliation report already delivered the leg, so it does not block reports
6778        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        // Register with qty=25 matching the trade size so the fill completes the order
6794        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        // Cancel then fill that completes the order
6836        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        // Channel should be empty: no re-emitted cancel for a fully-filled order
6843        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        // Fill tracker has NO registration (simulates HTTP still in-flight)
6859        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 cancel while order is not yet accepted
6886        dispatch_user_message(&UserWsMessage::Order(cancel_order), &ctx, &mut state);
6887
6888        // Cancel should be buffered (not emitted) AND saved to terminal_cancel_reports
6889        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    // A trade landing before the submit response buffers its fill, so the order update that
6910    // registers the order must emit that fill before its own terminal status
6911    #[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        // No registration: the submit response has not landed
6933        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        // The trade arrives first and buffers its fill
6974        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        // The order update registers the order and drains the buffered fill
6997        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        // The engine's state machine is what proves the order actually closes
7042        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    /// Replays the exact 5-message WS sequence from issue #3797.
7060    ///
7061    /// Messages in arrival order:
7062    ///   (A) Order Canceled, size_matched=0
7063    ///   (B) Trade fill 1.219511 (maker side)
7064    ///   (C) Order Canceled, size_matched=1.219511
7065    ///   (D) Order Canceled, size_matched=2.560972 (capped to tracked)
7066    ///   (E) Trade fill 1.341461 (maker side)
7067    ///
7068    /// Without the fix, the order ends in PartiallyFilled after (E).
7069    /// With the fix, a re-emitted cancel after (E) restores Canceled.
7070    #[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        // (A) Cancel with size_matched=0
7185        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        // (B) Trade fill 1.219511
7197        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        // Re-emitted cancel after fill (B)
7208        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        // (C) Cancel with size_matched=1.219511
7217        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        // (D) Cancel with size_matched=2.560972 (capped to tracked 1.219511)
7229        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        // (E) Trade fill 1.341461
7241        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        // The fix: re-emitted cancel after (E) restores terminal state
7253        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        // No more events
7262        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        // Captured V2 market BUY: 5 pUSD at 0.66 signs 7.5757 shares, and the on-chain
7271        // `takerAmountFilled` was 7.575758 (`chain_order_filled_taker_fees.json`). The
7272        // order quantity rises to the fill so the engine accepts the venue quantity.
7273        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        // Takers receive no MATCHED order update. FOK is atomic, so a dust
7437        // difference normalizes the registered quantity. IOC maps to FAK, so
7438        // a positive remainder closes as Canceled without changing the fill.
7439        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        // A marketable BUY filled below its limit returns more shares than the nominal qty (a
7605        // gross overfill, beyond the dust band). The dispatcher must raise the order qty via
7606        // OrderUpdated before the OrderFilled, or the engine drops the fill as an overfill.
7607        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        // 33.846152 shares against a nominal 30: a marketable fill below the limit price.
7662        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        // The raise must precede the fill so the engine accepts the larger quantity.
7691        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    // Unmatched -> Rejected (placement never became live); CanceledMarketResolved -> Expired
7709    // (market settled). Both are tracked own-order terminal states emitted as order events.
7710    #[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}