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