Skip to main content

nautilus_execution/engine/
mod.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//! Provides a generic `ExecutionEngine` for all environments.
17//!
18//! The execution engines primary responsibility is to orchestrate interactions
19//! between the `ExecutionClient` instances, and the rest of the platform. This
20//! includes sending commands to, and receiving events from, the trading venue
21//! endpoints via its registered execution clients.
22
23pub mod config;
24pub mod position;
25pub mod settlement;
26pub mod stubs;
27
28use std::{
29    cell::{Cell, RefCell, RefMut},
30    collections::{HashMap, HashSet},
31    fmt::{Debug, Display},
32    rc::Rc,
33    time::SystemTime,
34};
35
36use ahash::{AHashMap, AHashSet};
37use config::ExecutionEngineConfig;
38use futures::future::join_all;
39use indexmap::{IndexMap, IndexSet};
40use nautilus_common::{
41    cache::{Cache, PositionRef},
42    clients::ExecutionClient,
43    clock::Clock,
44    enums::LogColor,
45    generators::position_id::PositionIdGenerator,
46    log_info,
47    logging::{CMD, EVT, RECV, SEND},
48    messages::{
49        ExecutionReport,
50        execution::{
51            BatchCancelOrders, BatchModifyOrders, CancelAllOrders, CancelOrder, ModifyOrder,
52            QueryAccount, QueryOrder, SubmitOrder, SubmitOrderList, TradingCommand,
53        },
54    },
55    msgbus::{
56        self, MStr, MessagingSwitchboard, Pattern, TypedHandler, TypedIntoHandler, get_message_bus,
57        switchboard::{self},
58        typed_handler::ShareableMessageHandler,
59    },
60    runner::{
61        TradingCommandMessage, capture_trading_cmd, trading_cmd_is_dispatching,
62        try_get_trading_cmd_sender,
63    },
64    timer::{TimeEvent, TimeEventCallback},
65};
66use nautilus_core::{
67    DurationNanos, UUID4, UnixNanos, WeakCell,
68    datetime::{mins_to_secs, secs_to_nanos},
69};
70use nautilus_model::{
71    accounts::Account,
72    data::InstrumentClose,
73    enums::{
74        AccountType, ContingencyType, OmsType, OrderStatus, OrderType, PositionSide, TimeInForce,
75    },
76    events::{
77        OrderAccepted, OrderDenied, OrderDeniedReason, OrderEvent, OrderEventAny, OrderFillVoided,
78        OrderFilled, OrderInitialized, OrderUpdated, PositionChanged, PositionClosed,
79        PositionEvent, PositionOpened,
80    },
81    identifiers::{
82        AccountId, ClientId, ClientOrderId, ExecAlgorithmId, InstrumentId, PositionId, StrategyId,
83        TradeId, Venue, VenueOrderId,
84    },
85    instruments::{Instrument, InstrumentAny},
86    orderbook::own::{OwnBookOrder, OwnOrderBook, should_handle_own_book_order},
87    orders::{Order, OrderAny, OrderError},
88    position::{Position, PositionReplayEvent},
89    reports::{ExecutionMassStatus, FillReport, OrderStatusReport, PositionStatusReport},
90    types::{Money, Quantity},
91};
92use position::CorrectedPosition;
93pub use position::{PositionStateSnapshot, SnapshotAnchorer};
94use rust_decimal::Decimal;
95
96use crate::{
97    client::ExecutionClientAdapter,
98    reconciliation::{
99        check_position_reconciliation, fill_precedes_snapshot_reconciled_position,
100        generate_external_order_status_events, generate_reconciliation_order_events,
101        generate_reconciliation_order_pre_fill_events,
102        generate_reconciliation_order_snapshot_events, reconcile_fill_report as reconcile_fill,
103    },
104};
105
106const TIMER_SNAPSHOT_POSITIONS: &str = "ExecEngine_SNAPSHOT_POSITIONS";
107const TIMER_PURGE_CLOSED_ORDERS: &str = "ExecEngine_PURGE_CLOSED_ORDERS";
108const TIMER_PURGE_CLOSED_POSITIONS: &str = "ExecEngine_PURGE_CLOSED_POSITIONS";
109const TIMER_PURGE_ACCOUNT_EVENTS: &str = "ExecEngine_PURGE_ACCOUNT_EVENTS";
110
111/// Central execution engine responsible for orchestrating order routing and execution.
112///
113/// The execution engine manages the entire order lifecycle from submission to completion,
114/// handling routing to appropriate execution clients, position management, and event
115/// processing. It supports multiple execution venues through registered clients and
116/// provides sophisticated order management capabilities.
117///
118/// An order dispatched to a registered or external execution client keeps its `Initialized` (or
119/// `Released`) status until the client's first status event is applied, so cached status alone
120/// cannot distinguish it from an order that has not been routed yet. The engine records each
121/// dispatch until that first status transition, and a later submit command naming the order is
122/// stale for it: the engine neither routes the order to a client again nor denies it when the
123/// later command fails validation. The record is process-local: a reset clears it, and it is
124/// absent after a restart, where a still-`Initialized` dispatched order is treated like any other
125/// unrouted order until it is resolved by the client's events or a venue cancel or expiry report.
126pub struct ExecutionEngine {
127    clock: Rc<RefCell<dyn Clock>>,
128    cache: Rc<RefCell<Cache>>,
129    clients: IndexMap<ClientId, ExecutionClientAdapter>,
130    default_client_id: Option<ClientId>,
131    routing_map: AHashMap<Venue, ClientId>,
132    instrument_venues: AHashSet<Venue>,
133    instrument_close_venues: AHashSet<Venue>,
134    oms_overrides: AHashMap<StrategyId, OmsType>,
135    external_clients: HashSet<ClientId>,
136    pos_id_generator: PositionIdGenerator,
137    config: ExecutionEngineConfig,
138    orders_dispatched: RefCell<AHashSet<ClientOrderId>>,
139    command_count: Cell<u64>,
140    event_count: u64,
141    report_count: u64,
142    filtered_unclaimed_external_order_count: u64,
143    snapshot_anchorer: Option<SnapshotAnchorer>,
144}
145
146impl Debug for ExecutionEngine {
147    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148        f.debug_struct(stringify!(ExecutionEngine))
149            .field("client_count", &self.clients.len())
150            .finish()
151    }
152}
153
154fn fill_affects_quantity(apply_position: bool, restatement: bool) -> bool {
155    apply_position && !restatement
156}
157
158fn position_accepts_price_restatement(position: &Position, fill: &OrderFilled) -> bool {
159    if !position.is_open()
160        || !position.trade_ids.contains(&fill.trade_id)
161        || fill.order_side != position.entry
162    {
163        return false;
164    }
165
166    let Some(existing) = position.events.iter().find(|event| {
167        event.trade_id == fill.trade_id && event.client_order_id == fill.client_order_id
168    }) else {
169        return false;
170    };
171
172    if existing.last_qty != fill.last_qty {
173        return false;
174    }
175
176    let mut latest = None;
177
178    for event in &position.replay_events {
179        let PositionReplayEvent::Filled(existing) = event else {
180            continue;
181        };
182
183        if existing.trade_id == fill.trade_id && existing.client_order_id == fill.client_order_id {
184            latest = Some(existing.last_px);
185        }
186    }
187
188    latest.is_some_and(|price| price != fill.last_px)
189}
190
191fn is_fill_price_restatement(order: &OrderAny, fill: &OrderFilled) -> bool {
192    order.events().iter().any(|event| {
193        matches!(
194            event,
195            OrderEventAny::Filled(existing)
196                if existing.trade_id == fill.trade_id && existing.last_px != fill.last_px
197        )
198    })
199}
200
201impl ExecutionEngine {
202    /// Creates a new [`ExecutionEngine`] instance.
203    pub fn new(
204        clock: Rc<RefCell<dyn Clock>>,
205        cache: Rc<RefCell<Cache>>,
206        config: Option<ExecutionEngineConfig>,
207    ) -> Self {
208        let trader_id = get_message_bus().borrow().trader_id;
209        let external_clients: HashSet<ClientId> = config
210            .as_ref()
211            .and_then(|c| c.external_clients.clone())
212            .unwrap_or_default()
213            .into_iter()
214            .collect();
215        {
216            let mut cache = cache.borrow_mut();
217
218            for client_id in &external_clients {
219                cache.add_external_client(*client_id);
220            }
221        }
222
223        Self {
224            clock: clock.clone(),
225            cache,
226            clients: IndexMap::new(),
227            default_client_id: None,
228            routing_map: AHashMap::new(),
229            instrument_venues: AHashSet::new(),
230            instrument_close_venues: AHashSet::new(),
231            oms_overrides: AHashMap::new(),
232            external_clients,
233            pos_id_generator: PositionIdGenerator::new(trader_id, clock),
234            config: config.unwrap_or_default(),
235            orders_dispatched: RefCell::new(AHashSet::new()),
236            command_count: Cell::new(0),
237            event_count: 0,
238            report_count: 0,
239            filtered_unclaimed_external_order_count: 0,
240            snapshot_anchorer: None,
241        }
242    }
243
244    /// Registers all message bus handlers for the execution engine.
245    pub fn register_msgbus_handlers(engine: &Rc<RefCell<Self>>) {
246        let weak = WeakCell::from(Rc::downgrade(engine));
247
248        let weak1 = weak.clone();
249        msgbus::register_trading_command_endpoint(
250            MessagingSwitchboard::exec_engine_execute(),
251            TypedIntoHandler::from(move |cmd: TradingCommand| {
252                if let Some(rc) = weak1.upgrade() {
253                    rc.borrow().execute(cmd);
254                }
255            }),
256        );
257
258        // Queued endpoint for deferred command execution (re-entrancy safe),
259        // with direct dispatch when no sender is installed.
260        msgbus::register_trading_command_endpoint(
261            MessagingSwitchboard::exec_engine_queue_execute(),
262            TypedIntoHandler::from(move |cmd: TradingCommand| {
263                let endpoint = MessagingSwitchboard::exec_engine_execute();
264                if trading_cmd_is_dispatching() {
265                    capture_trading_cmd(TradingCommandMessage::new(endpoint, cmd));
266                } else if let Some(sender) = try_get_trading_cmd_sender() {
267                    sender.execute(TradingCommandMessage::new(endpoint, cmd));
268                } else {
269                    msgbus::send_trading_command(endpoint, cmd);
270                }
271            }),
272        );
273
274        let weak2 = weak.clone();
275        msgbus::register_order_event_endpoint(
276            MessagingSwitchboard::exec_engine_process(),
277            TypedIntoHandler::from(move |event: OrderEventAny| {
278                if let Some(rc) = weak2.upgrade() {
279                    rc.borrow_mut().process(&event);
280                }
281            }),
282        );
283
284        let weak3 = weak;
285        msgbus::register_execution_report_endpoint(
286            MessagingSwitchboard::exec_engine_reconcile_execution_report(),
287            TypedIntoHandler::from(move |report: ExecutionReport| {
288                if let Some(rc) = weak3.upgrade() {
289                    rc.borrow_mut().reconcile_execution_report(&report);
290                }
291            }),
292        );
293    }
294
295    /// Returns the total count of trading commands received by the engine.
296    #[must_use]
297    pub fn command_count(&self) -> u64 {
298        self.command_count.get()
299    }
300
301    /// Returns the total count of order events received by the engine.
302    #[must_use]
303    pub const fn event_count(&self) -> u64 {
304        self.event_count
305    }
306
307    /// Returns the total count of execution reports received by the engine.
308    #[must_use]
309    pub const fn report_count(&self) -> u64 {
310        self.report_count
311    }
312
313    /// Returns the count of unclaimed external venue orders filtered by execution reconciliation.
314    #[must_use]
315    pub const fn filtered_unclaimed_external_order_count(&self) -> u64 {
316        self.filtered_unclaimed_external_order_count
317    }
318
319    /// Subscribes to instrument updates for a venue via the message bus.
320    ///
321    /// When instruments are published by the `DataEngine`, the handler routes
322    /// them to every client whose own venue matches, plus the client routed to that
323    /// venue, if any. Repeated subscriptions for the same venue are ignored.
324    pub fn subscribe_venue_instruments(engine: &Rc<RefCell<Self>>, venue: Venue) {
325        if !engine.borrow_mut().instrument_venues.insert(venue) {
326            return;
327        }
328
329        let weak = WeakCell::from(Rc::downgrade(engine));
330        let pattern = switchboard::get_instruments_pattern(venue);
331
332        let handler = TypedHandler::from(move |instrument: &InstrumentAny| {
333            if let Some(rc) = weak.upgrade() {
334                let venue = instrument.id().venue;
335                let mut engine = rc.borrow_mut();
336                let routed_client = engine.routing_map.get(&venue).copied();
337                for adapter in engine.clients.values_mut() {
338                    if adapter.venue == venue || Some(adapter.client_id) == routed_client {
339                        adapter.on_instrument(instrument.clone());
340                    }
341                }
342            }
343        });
344
345        msgbus::subscribe_instruments(pattern, handler, None);
346        log::info!("Subscribed to instrument updates for venue {venue}");
347    }
348
349    /// Subscribes to instrument closes for a venue via the message bus.
350    ///
351    /// A `ContractExpired` close for a binary option settles every open position in that
352    /// instrument at the close price, without an order or fill. The first close applied is
353    /// authoritative. After settlement, fills and fill voids for the instrument still update
354    /// their orders but no longer change positions. Repeated subscriptions for the same venue are
355    /// ignored.
356    pub fn subscribe_venue_instrument_closes(engine: &Rc<RefCell<Self>>, venue: Venue) {
357        if !engine.borrow_mut().instrument_close_venues.insert(venue) {
358            return;
359        }
360
361        let weak = WeakCell::from(Rc::downgrade(engine));
362        let pattern: MStr<Pattern> = format!("data.close.{venue}.*").into();
363
364        let handler = ShareableMessageHandler::from_typed(move |close: &InstrumentClose| {
365            if let Some(rc) = weak.upgrade() {
366                rc.borrow().settle_instrument_close(close);
367            }
368        });
369
370        msgbus::subscribe_instrument_close(pattern, handler, Some(10));
371        log::info!("Subscribed to instrument closes for venue {venue}");
372    }
373
374    #[must_use]
375    /// Returns the position ID count for the specified strategy.
376    pub fn position_id_count(&self, strategy_id: StrategyId) -> usize {
377        self.pos_id_generator.count(strategy_id)
378    }
379
380    #[must_use]
381    /// Returns a reference to the cache.
382    pub fn cache(&self) -> &Rc<RefCell<Cache>> {
383        &self.cache
384    }
385
386    #[must_use]
387    /// Returns a reference to the configuration.
388    pub const fn config(&self) -> &ExecutionEngineConfig {
389        &self.config
390    }
391
392    /// Sets the cache snapshot anchorer.
393    ///
394    /// The system event-store integration installs this while a run is open. Passing
395    /// `None` disables anchor recording for later cache snapshots.
396    pub fn set_snapshot_anchorer(&mut self, anchorer: Option<SnapshotAnchorer>) {
397        self.snapshot_anchorer = anchorer;
398    }
399
400    #[must_use]
401    /// Checks the integrity of cached execution data.
402    pub fn check_integrity(&self) -> bool {
403        self.cache.borrow_mut().check_integrity()
404    }
405
406    #[must_use]
407    /// Returns true if all registered execution clients are connected.
408    pub fn check_connected(&self) -> bool {
409        self.clients.values().all(|c| c.is_connected())
410    }
411
412    #[must_use]
413    /// Returns true if all registered execution clients are disconnected.
414    pub fn check_disconnected(&self) -> bool {
415        self.clients.values().all(|c| !c.is_connected())
416    }
417
418    /// Returns connection status for each registered client.
419    #[must_use]
420    pub fn client_connection_status(&self) -> Vec<(ClientId, bool)> {
421        self.clients
422            .values()
423            .map(|c| (c.client_id(), c.is_connected()))
424            .collect()
425    }
426
427    #[must_use]
428    /// Checks for residual positions and orders in the cache.
429    pub fn check_residuals(&self) -> bool {
430        self.cache.borrow().check_residuals()
431    }
432
433    #[must_use]
434    /// Returns the set of instruments that have external order claims.
435    pub fn get_external_order_claims_instruments(&self) -> HashSet<InstrumentId> {
436        self.cache
437            .borrow()
438            .external_order_claim_instrument_ids(None)
439            .into_iter()
440            .collect()
441    }
442
443    #[must_use]
444    /// Returns the configured external client IDs.
445    pub fn get_external_client_ids(&self) -> HashSet<ClientId> {
446        self.external_clients.clone()
447    }
448
449    #[must_use]
450    /// Returns any external order claim for the given instrument ID.
451    pub fn get_external_order_claim(&self, instrument_id: &InstrumentId) -> Option<StrategyId> {
452        self.cache.borrow().external_order_claim(instrument_id)
453    }
454
455    /// Registers a new execution client without assigning venue or default routing.
456    ///
457    /// Callers configure venue and fallback routing separately with
458    /// [`Self::register_venue_routing`] and [`Self::set_default_client`].
459    ///
460    /// # Errors
461    ///
462    /// Returns an error if a client with the same ID is already registered.
463    pub fn register_client(&mut self, client: Box<dyn ExecutionClient>) -> anyhow::Result<()> {
464        let client_id = client.client_id();
465
466        if self.clients.contains_key(&client_id) {
467            anyhow::bail!("Client already registered with ID {client_id}");
468        }
469
470        let adapter = ExecutionClientAdapter::new(client);
471        self.cache
472            .borrow_mut()
473            .add_client_account(client_id, adapter.account_id);
474
475        log::debug!("Registered client {client_id}");
476        self.clients.insert(client_id, adapter);
477        Ok(())
478    }
479
480    /// Registers a default execution client for fallback routing.
481    pub fn register_default_client(&mut self, client: Box<dyn ExecutionClient>) {
482        let client_id = client.client_id();
483        let adapter = ExecutionClientAdapter::new(client);
484        {
485            let mut cache = self.cache.borrow_mut();
486            cache.add_client_account(client_id, adapter.account_id);
487            cache.set_default_client(client_id);
488        }
489
490        self.clients.insert(client_id, adapter);
491        self.default_client_id = Some(client_id);
492        log::debug!("Registered default client {client_id}");
493    }
494
495    /// Marks an already-registered client as the default for fallback routing.
496    ///
497    /// # Errors
498    ///
499    /// Returns an error if no client is registered with the given ID, or a default
500    /// client has already been set.
501    pub fn set_default_client(&mut self, client_id: ClientId) -> anyhow::Result<()> {
502        if self.default_client_id.is_some() {
503            anyhow::bail!("default client already registered");
504        }
505
506        if !self.clients.contains_key(&client_id) {
507            anyhow::bail!("No client registered with ID {client_id}");
508        }
509
510        self.cache.borrow_mut().set_default_client(client_id);
511        self.default_client_id = Some(client_id);
512        log::debug!("Set client {client_id} as default");
513        Ok(())
514    }
515
516    #[must_use]
517    /// Returns a reference to the execution client registered with the given ID.
518    pub fn get_client(&self, client_id: &ClientId) -> Option<&dyn ExecutionClient> {
519        self.clients.get(client_id).map(|a| a.client.as_ref())
520    }
521
522    #[must_use]
523    /// Returns a mutable reference to the execution client adapter registered with the given ID.
524    pub fn get_client_adapter_mut(
525        &mut self,
526        client_id: &ClientId,
527    ) -> Option<&mut ExecutionClientAdapter> {
528        self.clients.get_mut(client_id)
529    }
530
531    /// Generates mass status for the given client.
532    ///
533    /// # Errors
534    ///
535    /// Returns an error if the client is not found or mass status generation fails.
536    pub async fn generate_mass_status(
537        &mut self,
538        client_id: &ClientId,
539        lookback_mins: Option<u64>,
540    ) -> anyhow::Result<Option<ExecutionMassStatus>> {
541        if let Some(client) = self.get_client_adapter_mut(client_id) {
542            client.generate_mass_status(lookback_mins).await
543        } else {
544            anyhow::bail!("Client {client_id} not found")
545        }
546    }
547
548    /// Registers an external order with the execution client for tracking.
549    ///
550    /// This is called after reconciliation creates an external order, allowing the
551    /// execution client to track it for subsequent events (e.g., cancellations).
552    pub fn register_external_order(
553        &self,
554        client_order_id: ClientOrderId,
555        venue_order_id: VenueOrderId,
556        instrument_id: InstrumentId,
557        strategy_id: StrategyId,
558        ts_init: UnixNanos,
559    ) {
560        let venue = instrument_id.venue;
561        // Prefer the cached origin over venue routing so tracking lands on the
562        // client whose stream materialized the order.
563        let client_id = self
564            .cache
565            .borrow()
566            .client_id(&client_order_id)
567            .copied()
568            .or_else(|| self.routing_map.get(&venue).copied())
569            .or(self.default_client_id);
570
571        if let Some(client_id) = client_id
572            && let Some(client) = self.clients.get(&client_id)
573        {
574            client.register_external_order(
575                client_order_id,
576                venue_order_id,
577                instrument_id,
578                strategy_id,
579                ts_init,
580            );
581        }
582    }
583
584    #[must_use]
585    /// Returns all registered execution client IDs.
586    pub fn client_ids(&self) -> Vec<ClientId> {
587        self.clients.keys().copied().collect()
588    }
589
590    #[must_use]
591    /// Returns mutable access to all registered execution clients.
592    pub fn get_clients_mut(&mut self) -> Vec<&mut ExecutionClientAdapter> {
593        self.clients.values_mut().collect()
594    }
595
596    /// Returns all registered execution clients.
597    #[must_use]
598    pub fn get_all_clients(&self) -> Vec<&dyn ExecutionClient> {
599        self.clients.values().map(|a| a.client.as_ref()).collect()
600    }
601
602    #[must_use]
603    /// Returns execution clients that would handle the given orders.
604    ///
605    /// This method first attempts to resolve each order's originating client from the cache,
606    /// then falls back to venue routing for any orders without a cached client.
607    pub fn get_clients_for_orders(&self, orders: &[OrderAny]) -> Vec<&dyn ExecutionClient> {
608        let mut client_ids: IndexSet<ClientId> = IndexSet::new();
609        let mut venues: IndexSet<Venue> = IndexSet::new();
610
611        // Collect client IDs from cache and venues for fallback
612        for order in orders {
613            venues.insert(order.instrument_id().venue);
614            if let Some(client_id) = self.cache.borrow().client_id(&order.client_order_id()) {
615                client_ids.insert(*client_id);
616            }
617        }
618
619        let mut clients: Vec<&dyn ExecutionClient> = Vec::new();
620
621        // Add clients for cached client IDs (orders go back to originating client)
622        for client_id in &client_ids {
623            if let Some(adapter) = self.clients.get(client_id)
624                && !clients.iter().any(|c| c.client_id() == adapter.client_id)
625            {
626                clients.push(adapter.client.as_ref());
627            }
628        }
629
630        // Add clients for venue routing (for orders not in cache)
631        for venue in &venues {
632            let resolved_id = self
633                .routing_map
634                .get(venue)
635                .copied()
636                .or(self.default_client_id);
637
638            if let Some(adapter) = resolved_id.and_then(|id| self.clients.get(&id))
639                && !clients.iter().any(|c| c.client_id() == adapter.client_id)
640            {
641                clients.push(adapter.client.as_ref());
642            }
643        }
644
645        clients
646    }
647
648    /// Sets routing for a specific venue to a given client ID.
649    ///
650    /// # Errors
651    ///
652    /// Returns an error if the client ID is not registered or the venue already
653    /// routes to a different client.
654    pub fn register_venue_routing(
655        &mut self,
656        client_id: ClientId,
657        venue: Venue,
658    ) -> anyhow::Result<()> {
659        if !self.clients.contains_key(&client_id) {
660            anyhow::bail!("No client registered with ID {client_id}");
661        }
662
663        if let Some(existing_client_id) = self.routing_map.get(&venue)
664            && *existing_client_id != client_id
665        {
666            anyhow::bail!(
667                "Venue {venue} already routed to {existing_client_id}, \
668                 cannot re-route to {client_id}"
669            );
670        }
671
672        self.cache.borrow_mut().add_client_route(client_id, venue);
673        self.routing_map.insert(venue, client_id);
674        log::info!("Set client {client_id} routing for {venue}");
675        Ok(())
676    }
677
678    /// Registers the OMS (Order Management System) type for a strategy.
679    ///
680    /// If an OMS type is already registered for this strategy, it will be overridden.
681    pub fn register_oms_type(&mut self, strategy_id: StrategyId, oms_type: OmsType) {
682        self.oms_overrides.insert(strategy_id, oms_type);
683        log::info!("Registered OMS::{oms_type:?} for {strategy_id}");
684    }
685
686    /// Registers external order claims for a strategy.
687    ///
688    /// Venue-sourced external orders, fills, and materialized reconciliation activity for matching
689    /// instruments will be associated with the strategy.
690    ///
691    /// This operation is atomic: either all instruments are registered or none are.
692    ///
693    /// # Errors
694    ///
695    /// Returns an error if any instrument already has a registered claim.
696    pub fn register_external_order_claims(
697        &mut self,
698        strategy_id: StrategyId,
699        instrument_ids: &HashSet<InstrumentId>,
700    ) -> anyhow::Result<()> {
701        let instrument_ids: Vec<_> = instrument_ids.iter().copied().collect();
702        self.cache
703            .borrow_mut()
704            .register_external_order_claims(strategy_id, &instrument_ids)?;
705
706        if !instrument_ids.is_empty() {
707            log::info!("Registered external order claims for {strategy_id}: {instrument_ids:?}");
708        }
709
710        Ok(())
711    }
712
713    /// Deregisters all external order claims owned by `strategy_id`.
714    ///
715    /// # Panics
716    ///
717    /// Panics if the shared cache is already borrowed.
718    pub fn deregister_external_order_claims(&mut self, strategy_id: StrategyId) {
719        self.cache
720            .borrow_mut()
721            .set_external_order_claims(strategy_id, &[])
722            .expect("clearing external order claims cannot fail");
723    }
724
725    /// # Errors
726    ///
727    /// Returns an error if no client is registered with the given ID.
728    pub fn deregister_client(&mut self, client_id: ClientId) -> anyhow::Result<()> {
729        if self.clients.shift_remove(&client_id).is_some() {
730            {
731                let mut cache = self.cache.borrow_mut();
732                cache.remove_client_account(&client_id);
733                cache.remove_client_routes(&client_id);
734            }
735
736            if self.default_client_id == Some(client_id) {
737                self.default_client_id = None;
738            }
739
740            // Remove from routing map if present
741            self.routing_map
742                .retain(|_, mapped_id| mapped_id != &client_id);
743            log::info!("Deregistered client {client_id}");
744            Ok(())
745        } else {
746            anyhow::bail!("No client registered with ID {client_id}")
747        }
748    }
749
750    /// Connects all registered execution clients concurrently.
751    ///
752    /// Connection failures are logged but do not prevent the node from running.
753    pub async fn connect(&mut self) {
754        let futures: Vec<_> = self
755            .get_clients_mut()
756            .into_iter()
757            .map(|client| async move {
758                let client_id = client.client_id;
759                (client_id, client.connect().await)
760            })
761            .collect();
762
763        let results = join_all(futures).await;
764
765        for (client_id, result) in results {
766            if result.is_err() {
767                log::error!("Failed to connect execution client {client_id}: connection failed");
768            }
769        }
770    }
771
772    /// Disconnects all registered execution clients concurrently.
773    ///
774    /// # Errors
775    ///
776    /// Returns an error if any client fails to disconnect.
777    pub async fn disconnect(&mut self) -> anyhow::Result<()> {
778        let futures: Vec<_> = self
779            .get_clients_mut()
780            .into_iter()
781            .map(|client| async move {
782                let client_id = client.client_id;
783                (client_id, client.disconnect().await)
784            })
785            .collect();
786
787        let results = join_all(futures).await;
788        let failed_clients: Vec<_> = results
789            .into_iter()
790            .filter_map(|(client_id, result)| result.err().map(|_| client_id.to_string()))
791            .collect();
792
793        if failed_clients.is_empty() {
794            Ok(())
795        } else {
796            anyhow::bail!(
797                "Failed to disconnect execution clients: {}",
798                failed_clients.join(", ")
799            )
800        }
801    }
802
803    /// Sets the `manage_own_order_books` configuration option.
804    pub fn set_manage_own_order_books(&mut self, value: bool) {
805        self.config.manage_own_order_books = value;
806    }
807
808    /// Starts the position snapshot timer if configured.
809    #[expect(
810        clippy::missing_panics_doc,
811        reason = "timer registration is not expected to fail"
812    )]
813    pub fn start_snapshot_timer(&mut self) {
814        if let Some(interval_secs) = self
815            .config
816            .snapshot_positions_interval_secs
817            .filter(|&secs| secs > 0.0)
818            && !self
819                .clock
820                .borrow()
821                .timer_names()
822                .contains(&TIMER_SNAPSHOT_POSITIONS)
823        {
824            let interval_ns = match secs_to_nanos(interval_secs) {
825                Ok(ns) => ns,
826                Err(e) => {
827                    log::error!("Cannot start position snapshots timer: {e}");
828                    return;
829                }
830            };
831            let clock = self.clock.clone();
832            let cache = self.cache.clone();
833            let debug = self.config.debug;
834
835            let callback_fn: Rc<dyn Fn(TimeEvent)> = Rc::new(move |_event| {
836                Self::snapshot_open_positions(&clock, &cache, debug);
837            });
838            let callback = TimeEventCallback::from(callback_fn);
839
840            log::info!("Starting position snapshots timer at {interval_secs} second intervals");
841            self.clock
842                .borrow_mut()
843                .set_timer_ns(
844                    TIMER_SNAPSHOT_POSITIONS,
845                    DurationNanos::new(interval_ns),
846                    None,
847                    None,
848                    Some(callback),
849                    None,
850                    None,
851                )
852                .expect("Failed to set position snapshots timer");
853        }
854    }
855
856    /// Stops the position snapshot timer if running.
857    pub fn stop_snapshot_timer(&mut self) {
858        let timer_registered = self
859            .clock
860            .borrow()
861            .timer_names()
862            .contains(&TIMER_SNAPSHOT_POSITIONS);
863
864        if timer_registered {
865            log::info!("Canceling position snapshots timer");
866            self.clock
867                .borrow_mut()
868                .cancel_timer(TIMER_SNAPSHOT_POSITIONS);
869        }
870    }
871
872    /// Starts the purge timers if configured.
873    pub fn start_purge_timers(&mut self) {
874        if let Some(interval_mins) = self
875            .config
876            .purge_closed_orders_interval_mins
877            .filter(|&m| m > 0)
878            && !self
879                .clock
880                .borrow()
881                .timer_names()
882                .contains(&TIMER_PURGE_CLOSED_ORDERS)
883        {
884            'purge_closed_orders: {
885                let Ok(interval_ns) = DurationNanos::try_from_mins(u64::from(interval_mins)) else {
886                    log::error!(
887                        "Invalid purge_closed_orders_interval_mins {interval_mins}: minutes to nanoseconds conversion overflow"
888                    );
889                    break 'purge_closed_orders;
890                };
891                let buffer_mins = self.config.purge_closed_orders_buffer_mins.unwrap_or(0);
892                let buffer_secs = mins_to_secs(u64::from(buffer_mins));
893                let cache = self.cache.clone();
894                let clock = self.clock.clone();
895
896                let callback_fn: Rc<dyn Fn(TimeEvent)> = Rc::new(move |_event| {
897                    let ts_now = clock.borrow().timestamp_ns();
898                    cache.borrow_mut().purge_closed_orders(ts_now, buffer_secs);
899                });
900                let callback = TimeEventCallback::from(callback_fn);
901
902                log::info!(
903                    "Starting purge closed orders timer at {interval_mins} minute intervals"
904                );
905
906                if let Err(e) = self.clock.borrow_mut().set_timer_ns(
907                    TIMER_PURGE_CLOSED_ORDERS,
908                    interval_ns,
909                    None,
910                    None,
911                    Some(callback),
912                    None,
913                    None,
914                ) {
915                    log::error!("Failed to set {TIMER_PURGE_CLOSED_ORDERS} timer: {e}");
916                }
917            }
918        }
919
920        if let Some(interval_mins) = self
921            .config
922            .purge_closed_positions_interval_mins
923            .filter(|&m| m > 0)
924            && !self
925                .clock
926                .borrow()
927                .timer_names()
928                .contains(&TIMER_PURGE_CLOSED_POSITIONS)
929        {
930            'purge_closed_positions: {
931                let Ok(interval_ns) = DurationNanos::try_from_mins(u64::from(interval_mins)) else {
932                    log::error!(
933                        "Invalid purge_closed_positions_interval_mins {interval_mins}: minutes to nanoseconds conversion overflow"
934                    );
935                    break 'purge_closed_positions;
936                };
937                let buffer_mins = self.config.purge_closed_positions_buffer_mins.unwrap_or(0);
938                let buffer_secs = mins_to_secs(u64::from(buffer_mins));
939                let cache = self.cache.clone();
940                let clock = self.clock.clone();
941
942                let callback_fn: Rc<dyn Fn(TimeEvent)> = Rc::new(move |_event| {
943                    let ts_now = clock.borrow().timestamp_ns();
944                    cache
945                        .borrow_mut()
946                        .purge_closed_positions(ts_now, buffer_secs);
947                });
948                let callback = TimeEventCallback::from(callback_fn);
949
950                log::info!(
951                    "Starting purge closed positions timer at {interval_mins} minute intervals"
952                );
953
954                if let Err(e) = self.clock.borrow_mut().set_timer_ns(
955                    TIMER_PURGE_CLOSED_POSITIONS,
956                    interval_ns,
957                    None,
958                    None,
959                    Some(callback),
960                    None,
961                    None,
962                ) {
963                    log::error!("Failed to set {TIMER_PURGE_CLOSED_POSITIONS} timer: {e}");
964                }
965            }
966        }
967
968        if let Some(interval_mins) = self
969            .config
970            .purge_account_events_interval_mins
971            .filter(|&m| m > 0)
972            && !self
973                .clock
974                .borrow()
975                .timer_names()
976                .contains(&TIMER_PURGE_ACCOUNT_EVENTS)
977        {
978            'purge_account_events: {
979                let Ok(interval_ns) = DurationNanos::try_from_mins(u64::from(interval_mins)) else {
980                    log::error!(
981                        "Invalid purge_account_events_interval_mins {interval_mins}: minutes to nanoseconds conversion overflow"
982                    );
983                    break 'purge_account_events;
984                };
985                let lookback_mins = self.config.purge_account_events_lookback_mins.unwrap_or(0);
986                let lookback_secs = mins_to_secs(u64::from(lookback_mins));
987                let cache = self.cache.clone();
988                let clock = self.clock.clone();
989
990                let callback_fn: Rc<dyn Fn(TimeEvent)> = Rc::new(move |_event| {
991                    let ts_now = clock.borrow().timestamp_ns();
992                    cache
993                        .borrow_mut()
994                        .purge_account_events(ts_now, lookback_secs);
995                });
996                let callback = TimeEventCallback::from(callback_fn);
997
998                log::info!(
999                    "Starting purge account events timer at {interval_mins} minute intervals"
1000                );
1001
1002                if let Err(e) = self.clock.borrow_mut().set_timer_ns(
1003                    TIMER_PURGE_ACCOUNT_EVENTS,
1004                    interval_ns,
1005                    None,
1006                    None,
1007                    Some(callback),
1008                    None,
1009                    None,
1010                ) {
1011                    log::error!("Failed to set {TIMER_PURGE_ACCOUNT_EVENTS} timer: {e}");
1012                }
1013            }
1014        }
1015    }
1016
1017    /// Stops the purge timers if running.
1018    pub fn stop_purge_timers(&mut self) {
1019        let timer_names: Vec<String> = self
1020            .clock
1021            .borrow()
1022            .timer_names()
1023            .into_iter()
1024            .map(String::from)
1025            .collect();
1026
1027        if timer_names.iter().any(|n| n == TIMER_PURGE_CLOSED_ORDERS) {
1028            log::info!("Canceling purge closed orders timer");
1029            self.clock
1030                .borrow_mut()
1031                .cancel_timer(TIMER_PURGE_CLOSED_ORDERS);
1032        }
1033
1034        if timer_names
1035            .iter()
1036            .any(|n| n == TIMER_PURGE_CLOSED_POSITIONS)
1037        {
1038            log::info!("Canceling purge closed positions timer");
1039            self.clock
1040                .borrow_mut()
1041                .cancel_timer(TIMER_PURGE_CLOSED_POSITIONS);
1042        }
1043
1044        if timer_names.iter().any(|n| n == TIMER_PURGE_ACCOUNT_EVENTS) {
1045            log::info!("Canceling purge account events timer");
1046            self.clock
1047                .borrow_mut()
1048                .cancel_timer(TIMER_PURGE_ACCOUNT_EVENTS);
1049        }
1050    }
1051
1052    /// Creates snapshots of all open positions.
1053    pub fn snapshot_open_position_states(&self) {
1054        Self::snapshot_open_positions(&self.clock, &self.cache, self.config.debug);
1055    }
1056
1057    fn snapshot_open_positions(
1058        clock: &Rc<RefCell<dyn Clock>>,
1059        cache: &Rc<RefCell<Cache>>,
1060        debug: bool,
1061    ) {
1062        let positions: Vec<Position> = cache
1063            .borrow()
1064            .positions_open(None, None, None, None, None)
1065            .into_iter()
1066            .map(|p| p.cloned())
1067            .collect();
1068
1069        for position in positions {
1070            Self::publish_position_state_snapshot(clock, cache, debug, &position, true);
1071        }
1072    }
1073
1074    #[expect(clippy::await_holding_refcell_ref)]
1075    /// Loads persistent state into cache and rebuilds indices.
1076    ///
1077    /// # Errors
1078    ///
1079    /// Returns an error if any cache operation fails.
1080    pub async fn load_cache(&mut self) -> anyhow::Result<()> {
1081        let ts = SystemTime::now(); // dst-ok: init-time log timing, not on DST state path
1082
1083        {
1084            let mut cache = self.cache.borrow_mut();
1085            cache.clear_index();
1086            cache.cache_general()?;
1087        }
1088
1089        self.cache.borrow_mut().cache_all().await?;
1090
1091        // Snapshot before iterating: `get_or_init_own_order_book` re-enters `self.cache.borrow_mut()`.
1092        let own_book_entries: Vec<(InstrumentId, OwnBookOrder)> = {
1093            let mut cache = self.cache.borrow_mut();
1094            cache.build_index();
1095            let _ = cache.check_integrity();
1096
1097            if self.config.manage_own_order_books {
1098                cache
1099                    .orders(None, None, None, None, None)
1100                    .into_iter()
1101                    .filter(|o| !o.is_closed() && should_handle_own_book_order(o))
1102                    .map(|o| (o.instrument_id(), o.to_own_book_order()))
1103                    .collect()
1104            } else {
1105                Vec::new()
1106            }
1107        };
1108
1109        for (instrument_id, own_order) in own_book_entries {
1110            let mut own_book = self.get_or_init_own_order_book(&instrument_id);
1111            own_book.add(own_order);
1112        }
1113
1114        self.set_position_id_counts();
1115
1116        log::info!(
1117            "Loaded cache in {}ms",
1118            SystemTime::now() // dst-ok: init-time log timing, not on DST state path
1119                .duration_since(ts)
1120                .map_err(|e| anyhow::anyhow!("Failed to calculate duration: {e}"))?
1121                .as_millis()
1122        );
1123
1124        Ok(())
1125    }
1126
1127    /// Flushes the cache database, permanently removing the data the backing owns.
1128    ///
1129    /// # Errors
1130    ///
1131    /// Returns an error if flushing the database fails.
1132    pub fn flush_db(&self) -> anyhow::Result<()> {
1133        self.cache.borrow_mut().flush_db()
1134    }
1135
1136    /// Reconciles an execution report.
1137    pub fn reconcile_execution_report(&mut self, report: &ExecutionReport) {
1138        if !matches!(report, ExecutionReport::MassStatus(_)) {
1139            self.report_count += 1;
1140        }
1141
1142        match report {
1143            ExecutionReport::Order(order_report) => {
1144                self.reconcile_order_status_report(order_report);
1145            }
1146            ExecutionReport::Fill(fill_report) => {
1147                self.reconcile_fill_report(fill_report);
1148            }
1149            ExecutionReport::OrderWithFills(order_report, fills) => {
1150                self.reconcile_order_with_fills(order_report, fills);
1151            }
1152            ExecutionReport::Position(position_report) => {
1153                self.reconcile_position_report(position_report);
1154            }
1155            ExecutionReport::MassStatus(mass_status) => {
1156                self.reconcile_execution_mass_status(mass_status);
1157            }
1158        }
1159    }
1160
1161    /// Reconciles an order status report received at runtime.
1162    ///
1163    /// Handles order status transitions by generating appropriate events when the venue
1164    /// reports a different status than our local state. Supports all order states including
1165    /// fills with inferred fill generation when instruments are available.
1166    ///
1167    /// When the order is not found in cache, creates an external order from the report.
1168    /// This handles exchange-generated orders (liquidation, ADL, settlement) that were
1169    /// not submitted locally.
1170    pub fn reconcile_order_status_report(&mut self, report: &OrderStatusReport) {
1171        self.handle_order_status_report(report, false);
1172    }
1173
1174    fn handle_order_status_report(&mut self, report: &OrderStatusReport, is_snapshot: bool) {
1175        msgbus::publish_any(
1176            MessagingSwitchboard::reconciliation_raw_order_status_report_topic(),
1177            report,
1178        );
1179
1180        let cache = self.cache.borrow();
1181
1182        let order = report
1183            .client_order_id
1184            .and_then(|id| cache.order(&id).map(|o| o.clone()))
1185            .or_else(|| {
1186                cache
1187                    .client_order_id(&report.venue_order_id)
1188                    .and_then(|cid| cache.order(cid).map(|o| o.clone()))
1189            });
1190
1191        let instrument = cache.instrument(&report.instrument_id).cloned();
1192
1193        drop(cache);
1194
1195        if let Some(order) = order {
1196            let ts_now = self.clock.borrow().timestamp_ns();
1197
1198            let events = if is_snapshot {
1199                generate_reconciliation_order_snapshot_events(
1200                    &order,
1201                    report,
1202                    instrument.as_ref(),
1203                    ts_now,
1204                )
1205            } else {
1206                generate_reconciliation_order_events(&order, report, instrument.as_ref(), ts_now)
1207            };
1208
1209            for event in &events {
1210                self.handle_event(event);
1211            }
1212        } else {
1213            self.create_external_order(report, instrument.as_ref());
1214        }
1215    }
1216
1217    fn create_external_order(
1218        &mut self,
1219        report: &OrderStatusReport,
1220        instrument: Option<&InstrumentAny>,
1221    ) {
1222        let Some(instrument) = instrument else {
1223            log::warn!(
1224                "Cannot create external order for venue_order_id={}: instrument {} not found",
1225                report.venue_order_id,
1226                report.instrument_id
1227            );
1228            return;
1229        };
1230
1231        let Some(order) = self.materialize_external_order_from_status(report) else {
1232            return;
1233        };
1234
1235        let ts_now = self.clock.borrow().timestamp_ns();
1236        let events = generate_external_order_status_events(
1237            &order,
1238            report,
1239            &report.account_id,
1240            instrument,
1241            ts_now,
1242        );
1243
1244        for event in &events {
1245            self.handle_event(event);
1246        }
1247    }
1248
1249    /// Builds and registers an external order from an [`OrderStatusReport`] without
1250    /// emitting status events. Returns the registered order.
1251    fn materialize_external_order_from_status(
1252        &mut self,
1253        report: &OrderStatusReport,
1254    ) -> Option<OrderAny> {
1255        let strategy_id = self.resolve_external_strategy(&report.instrument_id);
1256        if self.should_filter_unclaimed_external_order(strategy_id) {
1257            self.filtered_unclaimed_external_order_count += 1;
1258
1259            if self.filtered_unclaimed_external_order_count == 1 {
1260                let external_order_id = report
1261                    .client_order_id
1262                    .map_or_else(|| report.venue_order_id.to_string(), |id| id.to_string());
1263                log::info!(
1264                    "Filtering unclaimed external orders; first filtered order {} ({}) for {}",
1265                    external_order_id,
1266                    report.venue_order_id,
1267                    report.instrument_id,
1268                );
1269            } else {
1270                let external_order_id = report
1271                    .client_order_id
1272                    .map_or_else(|| report.venue_order_id.to_string(), |id| id.to_string());
1273                log::debug!(
1274                    "Filtered unclaimed external order {} ({}) for {}",
1275                    external_order_id,
1276                    report.venue_order_id,
1277                    report.instrument_id,
1278                );
1279            }
1280
1281            return None;
1282        }
1283
1284        self.materialize_external_order_from_status_with_strategy(report, strategy_id)
1285    }
1286
1287    fn materialize_external_order_from_status_with_strategy(
1288        &self,
1289        report: &OrderStatusReport,
1290        strategy_id: StrategyId,
1291    ) -> Option<OrderAny> {
1292        let client_order_id = report
1293            .client_order_id
1294            .unwrap_or_else(|| ClientOrderId::from(report.venue_order_id.as_str()));
1295
1296        let trader_id = get_message_bus().borrow().trader_id;
1297        let ts_now = self.clock.borrow().timestamp_ns();
1298        let Some(order_side) = report.order_side else {
1299            log::error!(
1300                "Skipping external order {} ({}) for {}: order side is not specified",
1301                client_order_id,
1302                report.venue_order_id,
1303                report.instrument_id,
1304            );
1305            return None;
1306        };
1307
1308        let initialized = match OrderInitialized::new_checked(
1309            trader_id,
1310            strategy_id,
1311            report.instrument_id,
1312            client_order_id,
1313            order_side,
1314            report.order_type,
1315            report.quantity,
1316            report.time_in_force,
1317            report.post_only,
1318            report.reduce_only,
1319            false, // quote_quantity
1320            true,  // reconciliation
1321            UUID4::new(),
1322            ts_now,
1323            ts_now,
1324            report.price,
1325            report.activation_price,
1326            report.trigger_price,
1327            report.trigger_type,
1328            report.limit_offset,
1329            report.trailing_offset,
1330            report.trailing_offset_type,
1331            report.expire_time,
1332            report.display_qty,
1333            None, // emulation_trigger
1334            None, // trigger_instrument_id
1335            report.contingency_type,
1336            report.order_list_id,
1337            report.linked_order_ids.clone(),
1338            report.parent_order_id,
1339            None, // exec_algorithm_id
1340            None, // exec_algorithm_params
1341            None, // exec_spawn_id
1342            None, // tags
1343        ) {
1344            Ok(initialized) => initialized,
1345            Err(e) => {
1346                log::error!("Failed to create external order from report: {e}");
1347                return None;
1348            }
1349        };
1350
1351        self.materialize_external_order(
1352            initialized,
1353            client_order_id,
1354            report.venue_order_id,
1355            report.instrument_id,
1356            strategy_id,
1357            ts_now,
1358            Some(report.order_status),
1359            self.source_client_id_for_account(report.account_id, &report.instrument_id),
1360        )
1361    }
1362
1363    /// Builds and registers an external order from a [`FillReport`] when no matching
1364    /// order exists in cache. The order is created with `OrderType::Market` and a
1365    /// quantity equal to the fill's `last_qty`, so the fill consumes the entire
1366    /// order on application.
1367    ///
1368    /// This handles venue-initiated fills (most commonly Hyperliquid liquidations)
1369    /// where the venue does not surface a user-level order on its order channel.
1370    fn materialize_external_order_from_fill(&mut self, report: &FillReport) -> Option<OrderAny> {
1371        let strategy_id = self.resolve_external_strategy(&report.instrument_id);
1372        if self.should_filter_unclaimed_external_order(strategy_id) {
1373            self.filtered_unclaimed_external_order_count += 1;
1374
1375            let external_order_id = report
1376                .client_order_id
1377                .map_or_else(|| report.venue_order_id.to_string(), |id| id.to_string());
1378
1379            if self.filtered_unclaimed_external_order_count == 1 {
1380                log::info!(
1381                    "Filtering unclaimed external orders; first filtered fill {} ({}) for {}",
1382                    external_order_id,
1383                    report.venue_order_id,
1384                    report.instrument_id,
1385                );
1386            } else {
1387                log::debug!(
1388                    "Filtered unclaimed external fill {} ({}) for {}",
1389                    external_order_id,
1390                    report.venue_order_id,
1391                    report.instrument_id,
1392                );
1393            }
1394
1395            return None;
1396        }
1397
1398        let client_order_id = report
1399            .client_order_id
1400            .unwrap_or_else(|| ClientOrderId::from(report.venue_order_id.as_str()));
1401
1402        let trader_id = get_message_bus().borrow().trader_id;
1403        let ts_now = self.clock.borrow().timestamp_ns();
1404
1405        let initialized = OrderInitialized::new(
1406            trader_id,
1407            strategy_id,
1408            report.instrument_id,
1409            client_order_id,
1410            report.order_side,
1411            OrderType::Market,
1412            report.last_qty,
1413            TimeInForce::Ioc,
1414            false, // post_only
1415            true,  // reduce_only: venue-initiated closes always reduce
1416            false, // quote_quantity
1417            true,  // reconciliation
1418            UUID4::new(),
1419            ts_now,
1420            ts_now,
1421            None, // price
1422            None, // activation_price
1423            None, // trigger_price
1424            None, // trigger_type
1425            None, // limit_offset
1426            None, // trailing_offset
1427            None,
1428            None, // expire_time
1429            None, // display_qty
1430            None, // emulation_trigger
1431            None, // trigger_instrument_id
1432            None,
1433            None, // order_list_id
1434            None, // linked_order_ids
1435            None, // parent_order_id
1436            None, // exec_algorithm_id
1437            None, // exec_algorithm_params
1438            None, // exec_spawn_id
1439            None, // tags
1440        );
1441
1442        self.materialize_external_order(
1443            initialized,
1444            client_order_id,
1445            report.venue_order_id,
1446            report.instrument_id,
1447            strategy_id,
1448            ts_now,
1449            None,
1450            self.source_client_id_for_account(report.account_id, &report.instrument_id),
1451        )
1452    }
1453
1454    fn resolve_external_strategy(&self, instrument_id: &InstrumentId) -> StrategyId {
1455        self.cache
1456            .borrow()
1457            .external_order_claim(instrument_id)
1458            .unwrap_or_else(StrategyId::external)
1459    }
1460
1461    fn should_filter_unclaimed_external_order(&self, strategy_id: StrategyId) -> bool {
1462        self.config.filter_unclaimed_external_orders && strategy_id.is_external()
1463    }
1464
1465    /// Adds an external order to the cache and registers it for adapter routing.
1466    /// Returns the registered order on success.
1467    #[allow(
1468        clippy::too_many_arguments,
1469        reason = "external order materialization threads several ids and a timestamp"
1470    )]
1471    fn materialize_external_order(
1472        &self,
1473        initialized: OrderInitialized,
1474        client_order_id: ClientOrderId,
1475        venue_order_id: VenueOrderId,
1476        instrument_id: InstrumentId,
1477        strategy_id: StrategyId,
1478        ts_now: UnixNanos,
1479        order_status: Option<OrderStatus>,
1480        source_client_id: Option<ClientId>,
1481    ) -> Option<OrderAny> {
1482        let initialized = OrderEventAny::Initialized(initialized);
1483        let order = match OrderAny::from_events(vec![initialized.clone()]) {
1484            Ok(order) => order,
1485            Err(e) => {
1486                log::error!("Failed to create external order from report: {e}");
1487                return None;
1488            }
1489        };
1490
1491        {
1492            let mut cache = self.cache.borrow_mut();
1493            if let Err(e) = cache.add_venue_order_id(&client_order_id, &venue_order_id, false) {
1494                log::warn!("Failed to claim venue order ID for external order: {e}");
1495                return None;
1496            }
1497
1498            if let Err(e) = cache.add_order(order.clone(), None, source_client_id, false) {
1499                log::error!("Failed to add external order to cache: {e}");
1500                return None;
1501            }
1502        }
1503
1504        self.publish_order_event(&initialized);
1505
1506        match order_status {
1507            Some(status) => log::info!(
1508                "Created external order {client_order_id} ({venue_order_id}) for {instrument_id} [{status}]",
1509            ),
1510            None => log::info!(
1511                "Created external order {client_order_id} ({venue_order_id}) for {instrument_id}",
1512            ),
1513        }
1514
1515        self.register_external_order(
1516            client_order_id,
1517            venue_order_id,
1518            instrument_id,
1519            strategy_id,
1520            ts_now,
1521        );
1522
1523        Some(order)
1524    }
1525
1526    /// Resolves the execution client origin for a live-stream report by matching
1527    /// the report account against registered clients. A unique match stamps the
1528    /// materialized order's client origin; no match or an ambiguous match keeps
1529    /// the order origin-free.
1530    fn source_client_id_for_account(
1531        &self,
1532        account_id: AccountId,
1533        instrument_id: &InstrumentId,
1534    ) -> Option<ClientId> {
1535        let mut matches = self
1536            .clients
1537            .values()
1538            .filter(|adapter| {
1539                adapter.account_id == account_id && adapter.handles_order_venue(instrument_id.venue)
1540            })
1541            .map(|adapter| adapter.client_id);
1542
1543        let first = matches.next()?;
1544
1545        matches.next().is_none().then_some(first)
1546    }
1547
1548    /// Reconciles a fill report received at runtime.
1549    ///
1550    /// Finds the associated order, validates the fill, and generates an `OrderFilled` event
1551    /// if the fill is not a duplicate and won't cause an overfill. When `allow_overfills` is
1552    /// disabled and the execution client allows a reconciliation overfill, the engine raises
1553    /// the order quantity to the filled quantity plus this fill before applying it. An already
1554    /// filled order is not raised again. When the order is not in cache, an external order is
1555    /// bootstrapped from the fill so that venue-initiated closures (e.g. Hyperliquid
1556    /// liquidations) that arrive without a companion order status report still update the
1557    /// local position.
1558    ///
1559    /// # Replayed Fills
1560    ///
1561    /// A fill updates its order but no position when an open position for its instrument and
1562    /// account:
1563    /// - Was opened by a synthetic reconciliation order.
1564    /// - Has a `ts_opened` later than the fill's `ts_event`.
1565    /// - Does not hold the fill's trade.
1566    ///
1567    /// The venue position report behind that position already includes the fill, so this stops
1568    /// a venue that replays executions after a restart from doubling the position.
1569    pub fn reconcile_fill_report(&mut self, report: &FillReport) {
1570        msgbus::publish_any(
1571            MessagingSwitchboard::reconciliation_raw_fill_report_topic(),
1572            report,
1573        );
1574
1575        if report.last_qty.is_zero() {
1576            log::warn!("Skipping zero-quantity fill report: {report}");
1577            return;
1578        }
1579
1580        let cache = self.cache.borrow();
1581
1582        let order = report
1583            .client_order_id
1584            .and_then(|id| cache.order(&id).map(|o| o.clone()))
1585            .or_else(|| {
1586                cache
1587                    .client_order_id(&report.venue_order_id)
1588                    .and_then(|cid| cache.order(cid).map(|o| o.clone()))
1589            });
1590
1591        let instrument = cache.instrument(&report.instrument_id).cloned();
1592
1593        drop(cache);
1594
1595        let Some(instrument) = instrument else {
1596            log::debug!(
1597                "Cannot reconcile fill report for venue_order_id={}: instrument {} not found",
1598                report.venue_order_id,
1599                report.instrument_id
1600            );
1601            return;
1602        };
1603
1604        let order = match order {
1605            Some(order) => order,
1606            None => {
1607                let Some(order) = self.materialize_external_order_from_fill(report) else {
1608                    return;
1609                };
1610                let ts_now = self.clock.borrow().timestamp_ns();
1611                let accepted = OrderAccepted::new(
1612                    order.trader_id(),
1613                    order.strategy_id(),
1614                    order.instrument_id(),
1615                    order.client_order_id(),
1616                    report.venue_order_id,
1617                    report.account_id,
1618                    UUID4::new(),
1619                    report.ts_event,
1620                    ts_now,
1621                    true, // reconciliation
1622                );
1623                self.handle_event(&OrderEventAny::Accepted(accepted));
1624                self.cache
1625                    .borrow()
1626                    .order(&order.client_order_id())
1627                    .map(|o| o.clone())
1628                    .unwrap_or(order)
1629            }
1630        };
1631
1632        let ts_now = self.clock.borrow().timestamp_ns();
1633        let order = self
1634            .raised_order_for_reconciliation_overfill(&order, report, ts_now)
1635            .unwrap_or(order);
1636
1637        if let Some(event) = reconcile_fill(
1638            &order,
1639            report,
1640            &instrument,
1641            ts_now,
1642            self.config.allow_overfills,
1643        ) {
1644            let apply_position =
1645                !fill_precedes_snapshot_reconciled_position(&self.cache.borrow(), report);
1646            self.handle_event_with_position_application(&event, apply_position);
1647        }
1648    }
1649
1650    fn raised_order_for_reconciliation_overfill(
1651        &mut self,
1652        order: &OrderAny,
1653        report: &FillReport,
1654        ts_now: UnixNanos,
1655    ) -> Option<OrderAny> {
1656        if self.config.allow_overfills
1657            || order.status() == OrderStatus::Filled
1658            || order.trade_ids().iter().any(|id| **id == report.trade_id)
1659        {
1660            return None;
1661        }
1662
1663        let raised_qty = order.filled_qty().checked_add(report.last_qty)?;
1664        if raised_qty <= order.quantity()
1665            || !self.client_allows_reconciliation_overfill(order, report)
1666        {
1667            return None;
1668        }
1669
1670        let updated = OrderUpdated::new(
1671            order.trader_id(),
1672            order.strategy_id(),
1673            order.instrument_id(),
1674            order.client_order_id(),
1675            raised_qty,
1676            UUID4::new(),
1677            report.ts_event,
1678            ts_now,
1679            true,
1680            order.venue_order_id(),
1681            order.account_id(),
1682            None,
1683            None,
1684            None,
1685            order.is_quote_quantity(),
1686        );
1687        self.handle_event(&OrderEventAny::Updated(updated));
1688        self.cache
1689            .borrow()
1690            .order(&order.client_order_id())
1691            .map(|order| order.clone())
1692    }
1693
1694    fn client_allows_reconciliation_overfill(&self, order: &OrderAny, report: &FillReport) -> bool {
1695        let client_id = self
1696            .cache
1697            .borrow()
1698            .client_id(&order.client_order_id())
1699            .copied()
1700            .or_else(|| self.routing_map.get(&order.instrument_id().venue).copied())
1701            .or(self.default_client_id);
1702        client_id
1703            .and_then(|client_id| self.clients.get(&client_id))
1704            .is_some_and(|client| client.allows_reconciliation_overfill(order, report))
1705    }
1706
1707    /// Reconciles an [`OrderStatusReport`] paired with companion [`FillReport`]s
1708    /// for the same venue event.
1709    ///
1710    /// Real fills supplied by the adapter are applied first so their `trade_id` and
1711    /// `commission` are preserved; any residual quantity not covered by the fills is
1712    /// then synthesized as an inferred fill from the status report's `avg_px`.
1713    /// Adapters use this to emit ADL / liquidation / settlement events without
1714    /// losing real fill metadata.
1715    pub fn reconcile_order_with_fills(&mut self, report: &OrderStatusReport, fills: &[FillReport]) {
1716        msgbus::publish_any(
1717            MessagingSwitchboard::reconciliation_raw_order_status_report_topic(),
1718            report,
1719        );
1720
1721        let fill_report_topic = MessagingSwitchboard::reconciliation_raw_fill_report_topic();
1722        for fill in fills {
1723            msgbus::publish_any(fill_report_topic, fill);
1724        }
1725
1726        let cache = self.cache.borrow();
1727        let order = report
1728            .client_order_id
1729            .and_then(|id| cache.order(&id).map(|o| o.clone()))
1730            .or_else(|| {
1731                cache
1732                    .client_order_id(&report.venue_order_id)
1733                    .and_then(|cid| cache.order(cid).map(|o| o.clone()))
1734            });
1735        let instrument = cache.instrument(&report.instrument_id).cloned();
1736        drop(cache);
1737
1738        let Some(instrument) = instrument else {
1739            log::debug!(
1740                "Cannot reconcile bundled report for venue_order_id={}: instrument {} not found",
1741                report.venue_order_id,
1742                report.instrument_id,
1743            );
1744
1745            if fills.is_empty()
1746                && let Some(order) = order
1747            {
1748                let ts_now = self.clock.borrow().timestamp_ns();
1749                let events =
1750                    generate_reconciliation_order_snapshot_events(&order, report, None, ts_now);
1751
1752                for event in &events {
1753                    self.handle_event(event);
1754                }
1755            }
1756            return;
1757        };
1758
1759        // Bootstrap the external order with only OrderAccepted; defer fill events to
1760        // the per-fill loop so real fill metadata is preserved.
1761        let mut order = match order {
1762            Some(order) => {
1763                let ts_now = self.clock.borrow().timestamp_ns();
1764                let events = generate_reconciliation_order_pre_fill_events(&order, report, ts_now);
1765                for event in &events {
1766                    self.handle_event(event);
1767                }
1768                self.cache
1769                    .borrow()
1770                    .order(&order.client_order_id())
1771                    .map(|o| o.clone())
1772                    .unwrap_or(order)
1773            }
1774            None => {
1775                let Some(order) = self.materialize_external_order_from_status(report) else {
1776                    return;
1777                };
1778                let ts_now = self.clock.borrow().timestamp_ns();
1779                let accepted = OrderAccepted::new(
1780                    order.trader_id(),
1781                    order.strategy_id(),
1782                    order.instrument_id(),
1783                    order.client_order_id(),
1784                    report.venue_order_id,
1785                    report.account_id,
1786                    UUID4::new(),
1787                    report.ts_accepted,
1788                    ts_now,
1789                    true, // reconciliation
1790                );
1791                self.handle_event(&OrderEventAny::Accepted(accepted));
1792                self.cache
1793                    .borrow()
1794                    .order(&order.client_order_id())
1795                    .map(|o| o.clone())
1796                    .unwrap_or(order)
1797            }
1798        };
1799
1800        let client_order_id = order.client_order_id();
1801
1802        for fill in fills {
1803            let ts_now = self.clock.borrow().timestamp_ns();
1804
1805            if let Some(event) = reconcile_fill(
1806                &order,
1807                fill,
1808                &instrument,
1809                ts_now,
1810                self.config.allow_overfills,
1811            ) {
1812                self.handle_event(&event);
1813            }
1814
1815            // Refresh order after fill to keep filled_qty accurate for the next iteration.
1816            if let Some(refreshed) = self
1817                .cache
1818                .borrow()
1819                .order(&client_order_id)
1820                .map(|o| o.clone())
1821            {
1822                order = refreshed;
1823            }
1824        }
1825
1826        let ts_now = self.clock.borrow().timestamp_ns();
1827        let events = generate_reconciliation_order_snapshot_events(
1828            &order,
1829            report,
1830            Some(&instrument),
1831            ts_now,
1832        );
1833
1834        for event in &events {
1835            self.handle_event(event);
1836        }
1837    }
1838
1839    /// Reconciles a position status report received at runtime.
1840    ///
1841    /// Compares the venue-reported position with cached positions and logs any discrepancies.
1842    /// Handles both hedging (with `venue_position_id`) and netting (without) modes.
1843    pub fn reconcile_position_report(&mut self, report: &PositionStatusReport) {
1844        msgbus::publish_any(
1845            MessagingSwitchboard::reconciliation_raw_position_status_report_topic(),
1846            report,
1847        );
1848
1849        let cache = self.cache.borrow();
1850
1851        let size_precision = cache
1852            .instrument(&report.instrument_id)
1853            .map(InstrumentAny::size_precision);
1854
1855        if report.venue_position_id.is_some() {
1856            self.reconcile_position_report_hedging(report, &cache);
1857        } else {
1858            self.reconcile_position_report_netting(report, &cache, size_precision);
1859        }
1860    }
1861
1862    fn reconcile_position_report_hedging(&self, report: &PositionStatusReport, cache: &Cache) {
1863        let venue_position_id = report.venue_position_id.as_ref().unwrap();
1864
1865        log::debug!(
1866            "Reconciling HEDGE position for {}, venue_position_id={}",
1867            report.instrument_id,
1868            venue_position_id
1869        );
1870
1871        let Some(position) = cache.position(venue_position_id) else {
1872            if report.signed_decimal_qty == Decimal::ZERO {
1873                return;
1874            }
1875
1876            log::error!("Cannot reconcile position: {venue_position_id} not found in cache");
1877            return;
1878        };
1879
1880        let cached_signed_qty = match position.side {
1881            PositionSide::Long => position.quantity.as_decimal(),
1882            PositionSide::Short => -position.quantity.as_decimal(),
1883            _ => Decimal::ZERO,
1884        };
1885        let venue_signed_qty = report.signed_decimal_qty;
1886
1887        if cached_signed_qty != venue_signed_qty {
1888            log::error!(
1889                "Position mismatch for {} {}: cached={}, venue={}",
1890                report.instrument_id,
1891                venue_position_id,
1892                cached_signed_qty,
1893                venue_signed_qty
1894            );
1895        }
1896    }
1897
1898    fn reconcile_position_report_netting(
1899        &self,
1900        report: &PositionStatusReport,
1901        cache: &Cache,
1902        size_precision: Option<u8>,
1903    ) {
1904        log::debug!("Reconciling NET position for {}", report.instrument_id);
1905
1906        let positions_open = Self::netting_positions_open_for_report(cache, report);
1907
1908        let position_refs = positions_open
1909            .iter()
1910            .map(|position| &**position)
1911            .collect::<Vec<_>>();
1912
1913        if let Some(message) =
1914            Self::netting_split_position_ownership_message(report, &position_refs)
1915        {
1916            log::warn!("{message}");
1917        }
1918
1919        // Sum up cached position quantities using domain types to avoid f64 precision loss
1920        let cached_signed_qty: Decimal = positions_open
1921            .iter()
1922            .map(|position| Self::position_signed_decimal_qty(position))
1923            .sum();
1924
1925        log::debug!(
1926            "Position report: venue_signed_qty={}, cached_signed_qty={}",
1927            report.signed_decimal_qty,
1928            cached_signed_qty
1929        );
1930
1931        let _ = check_position_reconciliation(report, cached_signed_qty, size_precision);
1932    }
1933
1934    fn netting_positions_open_for_report<'a>(
1935        cache: &'a Cache,
1936        report: &PositionStatusReport,
1937    ) -> Vec<PositionRef<'a>> {
1938        cache.positions_open(
1939            None,
1940            Some(&report.instrument_id),
1941            None,
1942            Some(&report.account_id),
1943            None,
1944        )
1945    }
1946
1947    fn netting_split_position_ownership_message(
1948        report: &PositionStatusReport,
1949        positions_open: &[&Position],
1950    ) -> Option<String> {
1951        let mut strategy_ids = positions_open
1952            .iter()
1953            .map(|position| position.strategy_id.to_string())
1954            .collect::<Vec<_>>();
1955        strategy_ids.sort();
1956        strategy_ids.dedup();
1957
1958        if strategy_ids.len() <= 1 {
1959            return None;
1960        }
1961
1962        let position_details = Self::position_details(positions_open.iter().copied());
1963
1964        Some(format!(
1965            "NETTING reconciliation found split ownership for account_id={}, instrument_id={}: \
1966             strategies=[{}], positions=[{}]",
1967            report.account_id,
1968            report.instrument_id,
1969            strategy_ids.join(", "),
1970            position_details
1971        ))
1972    }
1973
1974    /// Reconciles an execution mass status report.
1975    ///
1976    /// Processes all order reports, fill reports, and position reports contained
1977    /// in the mass status. Order reports are paired with their companion fills so
1978    /// real trade IDs and commissions are applied before any residual inferred fill.
1979    /// Filled-quantity decreases generate fill voids even when no companion fills are present.
1980    /// Order snapshots are skipped when cached fill activity is initialized at or after
1981    /// collection starts (`mass_status.ts_init`); companion trades still reconcile.
1982    pub fn reconcile_execution_mass_status(&mut self, mass_status: &ExecutionMassStatus) {
1983        self.report_count += 1;
1984
1985        log::info!(
1986            "Reconciling mass status for client={}, account={}, venue={}",
1987            mass_status.client_id,
1988            mass_status.account_id,
1989            mass_status.venue
1990        );
1991
1992        let order_reports = mass_status.order_reports();
1993        let fill_reports = mass_status.fill_reports();
1994        let mut paired_venue_ids = AHashSet::new();
1995
1996        for order_report in order_reports.values() {
1997            if self.is_order_snapshot_stale(order_report, mass_status.ts_init) {
1998                msgbus::publish_any(
1999                    MessagingSwitchboard::reconciliation_raw_order_status_report_topic(),
2000                    order_report,
2001                );
2002
2003                log::debug!(
2004                    "Skipping snapshot for {} after concurrent fill activity",
2005                    order_report.venue_order_id,
2006                );
2007                continue;
2008            }
2009
2010            if let Some(fills) = fill_reports.get(&order_report.venue_order_id)
2011                && !fills.is_empty()
2012            {
2013                self.reconcile_order_with_fills(order_report, fills);
2014                paired_venue_ids.insert(order_report.venue_order_id);
2015            } else {
2016                self.handle_order_status_report(order_report, true);
2017            }
2018        }
2019
2020        for fill_reports in fill_reports.values() {
2021            for fill_report in fill_reports {
2022                if paired_venue_ids.contains(&fill_report.venue_order_id) {
2023                    continue;
2024                }
2025
2026                self.reconcile_fill_report(fill_report);
2027            }
2028        }
2029
2030        for position_reports in mass_status.position_reports().values() {
2031            for position_report in position_reports {
2032                self.reconcile_position_report(position_report);
2033            }
2034        }
2035
2036        log::info!(
2037            "Mass status reconciliation complete: {} orders, {} fills, {} positions",
2038            mass_status.order_reports().len(),
2039            mass_status
2040                .fill_reports()
2041                .values()
2042                .map(Vec::len)
2043                .sum::<usize>(),
2044            mass_status
2045                .position_reports()
2046                .values()
2047                .map(Vec::len)
2048                .sum::<usize>()
2049        );
2050    }
2051
2052    fn is_order_snapshot_stale(&self, report: &OrderStatusReport, ts_snapshot: UnixNanos) -> bool {
2053        let cache = self.cache.borrow();
2054
2055        let order = report
2056            .client_order_id
2057            .and_then(|id| cache.order(&id))
2058            .or_else(|| {
2059                cache
2060                    .client_order_id(&report.venue_order_id)
2061                    .and_then(|id| cache.order(id))
2062            });
2063
2064        order.is_some_and(|order| {
2065            order.events().into_iter().any(|event| match event {
2066                OrderEventAny::Filled(fill) => fill.ts_init >= ts_snapshot,
2067                OrderEventAny::FillVoided(void) => void.ts_init >= ts_snapshot,
2068                _ => false,
2069            })
2070        })
2071    }
2072
2073    /// Executes a trading command by routing it to the appropriate execution client.
2074    pub fn execute(&self, command: TradingCommand) {
2075        self.execute_command(command);
2076    }
2077
2078    /// Processes an order event, updating internal state and routing as needed.
2079    pub fn process(&mut self, event: &OrderEventAny) {
2080        self.handle_event(event);
2081    }
2082
2083    /// Projects a reconciled fill onto its order without applying position or portfolio economics.
2084    pub fn project_reconciliation_fill(&mut self, fill: &OrderFilled) {
2085        self.handle_event_with_position_application(&OrderEventAny::Filled(fill.clone()), false);
2086    }
2087
2088    /// Starts the execution engine and all registered execution clients.
2089    pub fn start(&mut self) {
2090        for client in self.get_clients_mut() {
2091            if client.start().is_err() {
2092                log::error!("Failed to start execution client {}", client.client_id());
2093            }
2094        }
2095
2096        self.start_snapshot_timer();
2097        self.start_purge_timers();
2098
2099        log::info!("Started");
2100    }
2101
2102    /// Stops the execution engine and all registered execution clients.
2103    ///
2104    /// Adapters are expected to be idempotent on repeated `stop()` calls
2105    /// (e.g. via an internal `is_stopped` guard); the backtest teardown
2106    /// sequence calls `stop()` more than once per run.
2107    pub fn stop(&mut self) {
2108        for client in self.get_clients_mut() {
2109            if client.stop().is_err() {
2110                log::error!("Failed to stop execution client {}", client.client_id());
2111            }
2112        }
2113
2114        self.stop_snapshot_timer();
2115        self.stop_purge_timers();
2116
2117        log::info!("Stopped");
2118    }
2119
2120    /// Stops all registered execution clients without stopping the engine itself.
2121    pub fn stop_clients(&mut self) {
2122        for client in self.get_clients_mut() {
2123            if client.stop().is_err() {
2124                log::error!("Failed to stop execution client {}", client.client_id());
2125            }
2126        }
2127    }
2128
2129    /// Resets the execution engine and all registered execution clients to initial state.
2130    ///
2131    /// Cancels engine-owned timers (snapshot, purge) but leaves timers owned by
2132    /// other components on the shared clock untouched.
2133    pub fn reset(&mut self) {
2134        for client in self.get_clients_mut() {
2135            if client.reset().is_err() {
2136                log::error!("Failed to reset execution client {}", client.client_id());
2137            }
2138        }
2139
2140        self.cache.borrow_mut().reset();
2141        self.pos_id_generator.reset();
2142        self.orders_dispatched.borrow_mut().clear();
2143
2144        self.stop_snapshot_timer();
2145        self.stop_purge_timers();
2146
2147        self.command_count.set(0);
2148        self.event_count = 0;
2149        self.report_count = 0;
2150        self.filtered_unclaimed_external_order_count = 0;
2151        log::info!("Reset");
2152    }
2153
2154    /// Disposes of the execution engine, releasing resources from all clients and timers.
2155    ///
2156    /// Cancels engine-owned timers (snapshot, purge) but leaves timers owned by
2157    /// other components on the shared clock untouched.
2158    pub fn dispose(&mut self) {
2159        for client in self.get_clients_mut() {
2160            if client.dispose().is_err() {
2161                log::error!("Failed to dispose execution client {}", client.client_id());
2162            }
2163        }
2164
2165        self.stop_snapshot_timer();
2166        self.stop_purge_timers();
2167
2168        log::info!("Disposed");
2169    }
2170
2171    fn execute_command(&self, command: TradingCommand) {
2172        self.command_count.set(self.command_count.get() + 1);
2173
2174        if self.config.debug {
2175            log::debug!("{RECV}{CMD} {command}");
2176        }
2177
2178        match self.validate_submission(&command) {
2179            SubmissionValidationResult::Valid => {}
2180            SubmissionValidationResult::StaleOrder {
2181                client_order_id,
2182                status,
2183            } => {
2184                log::warn!(
2185                    "Skipping stale submit command for {client_order_id} in status {status}"
2186                );
2187                return;
2188            }
2189            SubmissionValidationResult::Dispatched { client_order_id } => {
2190                log::warn!(
2191                    "Skipping duplicate submit command for {client_order_id} already dispatched to an execution client"
2192                );
2193                return;
2194            }
2195            SubmissionValidationResult::Deny(reason) => {
2196                self.deny_submission(&command, &reason);
2197                return;
2198            }
2199        }
2200
2201        if let Some(cid) = command.client_id()
2202            && self.external_clients.contains(&cid)
2203        {
2204            let topic = format!("commands.trading.{cid}");
2205            msgbus::publish_any(topic.into(), &command);
2206
2207            // The external client now owns the submitted orders, exactly like a registered client
2208            match &command {
2209                TradingCommand::SubmitOrder(cmd) => {
2210                    self.orders_dispatched
2211                        .borrow_mut()
2212                        .insert(cmd.client_order_id);
2213                }
2214                TradingCommand::SubmitOrderList(cmd) => {
2215                    self.orders_dispatched
2216                        .borrow_mut()
2217                        .extend(cmd.order_list.client_order_ids.iter().copied());
2218                }
2219                _ => {}
2220            }
2221
2222            if self.config.debug {
2223                log::debug!("Skipping execution command for external client {cid}: {command}");
2224            }
2225            return;
2226        }
2227
2228        let client = if let Some(adapter) = self.find_client_for_command(&command) {
2229            adapter.client.as_ref()
2230        } else {
2231            let routing_context = Self::routing_context_for_command(&command);
2232
2233            log::error!(
2234                "No execution client found for command: client_id={:?}, {routing_context}, command={command}",
2235                command.client_id(),
2236            );
2237
2238            let reason = OrderDeniedReason::NoExecutionClient {
2239                client_id: command.client_id(),
2240                routing_context,
2241            }
2242            .to_string();
2243
2244            match command {
2245                TradingCommand::SubmitOrder(cmd) => {
2246                    let order = self
2247                        .cache
2248                        .borrow()
2249                        .order(&cmd.client_order_id)
2250                        .map(|o| o.clone());
2251
2252                    if let Some(order) = order {
2253                        self.deny_order(&order, &reason);
2254                    }
2255                }
2256                TradingCommand::SubmitOrderList(cmd) => {
2257                    let orders: Vec<OrderAny> = self
2258                        .cache
2259                        .borrow()
2260                        .orders_for_ids(&cmd.order_list.client_order_ids, &cmd);
2261
2262                    for order in &orders {
2263                        self.deny_order(order, &reason);
2264                    }
2265                }
2266                _ => {}
2267            }
2268
2269            return;
2270        };
2271
2272        match command {
2273            TradingCommand::SubmitOrder(cmd) => self.handle_submit_order(client, cmd),
2274            TradingCommand::SubmitOrderList(cmd) => self.handle_submit_order_list(client, cmd),
2275            TradingCommand::ModifyOrder(cmd) => self.handle_modify_order(client, cmd),
2276            TradingCommand::ModifyOrders(cmd) => self.handle_batch_modify_orders(client, cmd),
2277            TradingCommand::CancelOrder(cmd) => self.handle_cancel_order(client, cmd),
2278            TradingCommand::CancelOrders(cmd) => self.handle_batch_cancel_orders(client, cmd),
2279            TradingCommand::CancelAllOrders(cmd) => self.handle_cancel_all_orders(client, &cmd),
2280            TradingCommand::QueryOrder(cmd) => self.handle_query_order(client, cmd),
2281            TradingCommand::QueryAccount(cmd) => self.handle_query_account(client, cmd),
2282        }
2283    }
2284
2285    fn validate_submission(&self, command: &TradingCommand) -> SubmissionValidationResult {
2286        match command {
2287            TradingCommand::SubmitOrder(cmd) => {
2288                let client_order_id = cmd.client_order_id;
2289                let cache = self.cache.borrow();
2290
2291                if let Some(order) = cache.order(&client_order_id)
2292                    && !Self::has_submittable_status(&order)
2293                {
2294                    return SubmissionValidationResult::StaleOrder {
2295                        client_order_id,
2296                        status: order.status(),
2297                    };
2298                }
2299
2300                if self.is_dispatched(client_order_id) {
2301                    return SubmissionValidationResult::Dispatched { client_order_id };
2302                }
2303
2304                SubmissionValidationResult::Valid
2305            }
2306            TradingCommand::SubmitOrderList(cmd) => {
2307                let cache = self.cache.borrow();
2308                let client_order_ids = &cmd.order_list.client_order_ids;
2309                let has_ineligible_order = client_order_ids.iter().any(|client_order_id| {
2310                    self.is_dispatched(*client_order_id)
2311                        || cache
2312                            .order(client_order_id)
2313                            .is_some_and(|order| !Self::has_submittable_status(&order))
2314                });
2315
2316                if !has_ineligible_order {
2317                    return SubmissionValidationResult::Valid;
2318                }
2319
2320                SubmissionValidationResult::Deny(OrderDeniedReason::OrderListDenied {
2321                    order_list_id: cmd.order_list.id,
2322                })
2323            }
2324            _ => SubmissionValidationResult::Valid,
2325        }
2326    }
2327
2328    fn has_submittable_status(order: &OrderAny) -> bool {
2329        matches!(
2330            order.status(),
2331            OrderStatus::Initialized | OrderStatus::Released
2332        )
2333    }
2334
2335    // The dispatch record is checked independently of the cache because the command is dispatched
2336    // to an external client without the engine caching its orders (see the type-level documentation).
2337    fn is_dispatched(&self, client_order_id: ClientOrderId) -> bool {
2338        self.orders_dispatched.borrow().contains(&client_order_id)
2339    }
2340
2341    // A cached order accepts a submit command only while it has a submittable status and has not
2342    // already been dispatched to an execution client.
2343    fn is_eligible_for_submission(&self, order: &OrderAny) -> bool {
2344        Self::has_submittable_status(order) && !self.is_dispatched(order.client_order_id())
2345    }
2346
2347    fn deny_submission(&self, command: &TradingCommand, reason: &OrderDeniedReason) {
2348        let TradingCommand::SubmitOrderList(cmd) = command else {
2349            return;
2350        };
2351
2352        let cache = self.cache.borrow();
2353        let mut orders: Vec<OrderAny> = cmd
2354            .order_list
2355            .client_order_ids
2356            .iter()
2357            .filter_map(|client_order_id| cache.order_owned(client_order_id))
2358            .collect();
2359        drop(cache);
2360
2361        for client_order_id in &cmd.order_list.client_order_ids {
2362            if orders
2363                .iter()
2364                .any(|order| order.client_order_id() == *client_order_id)
2365            {
2366                continue;
2367            }
2368
2369            let Some(order_init) = cmd
2370                .order_inits
2371                .iter()
2372                .find(|init| init.client_order_id == *client_order_id)
2373            else {
2374                continue;
2375            };
2376
2377            if let Some(order) = self.add_order_from_init(order_init, cmd.position_id, cmd) {
2378                orders.push(order);
2379            }
2380        }
2381
2382        let mut eligible_orders = Vec::with_capacity(orders.len());
2383
2384        for order in &orders {
2385            if self.is_eligible_for_submission(order) {
2386                eligible_orders.push(order);
2387            } else if Self::has_submittable_status(order) {
2388                log::warn!(
2389                    "Preserving {} already dispatched to an execution client, not denying for order list {}",
2390                    order.client_order_id(),
2391                    cmd.order_list.id,
2392                );
2393            }
2394        }
2395
2396        if eligible_orders.is_empty() {
2397            log::warn!(
2398                "Skipping stale submit command for order list {}",
2399                cmd.order_list.id
2400            );
2401            return;
2402        }
2403
2404        let reason = reason.to_string();
2405        for order in eligible_orders {
2406            self.deny_order(order, &reason);
2407        }
2408    }
2409
2410    fn routing_context_for_command(command: &TradingCommand) -> String {
2411        match command {
2412            TradingCommand::SubmitOrder(cmd) => format!("venue={}", cmd.instrument_id.venue),
2413            TradingCommand::SubmitOrderList(cmd) => format!("venue={}", cmd.instrument_id.venue),
2414            TradingCommand::ModifyOrder(cmd) => format!("venue={}", cmd.instrument_id.venue),
2415            TradingCommand::ModifyOrders(cmd) => format!("venue={}", cmd.instrument_id.venue),
2416            TradingCommand::CancelOrder(cmd) => format!("venue={}", cmd.instrument_id.venue),
2417            TradingCommand::CancelOrders(cmd) => format!("venue={}", cmd.instrument_id.venue),
2418            TradingCommand::CancelAllOrders(cmd) => format!("venue={}", cmd.instrument_id.venue),
2419            TradingCommand::QueryOrder(cmd) => format!("venue={}", cmd.instrument_id.venue),
2420            TradingCommand::QueryAccount(cmd) => {
2421                let issuer = cmd.account_id.get_issuer();
2422                format!("account_id={}, issuer={issuer}", cmd.account_id)
2423            }
2424        }
2425    }
2426
2427    /// Returns the execution client selected by the command routing rules.
2428    pub fn find_client_for_command(
2429        &self,
2430        command: &TradingCommand,
2431    ) -> Option<&ExecutionClientAdapter> {
2432        if let Some(client_id) = command.client_id()
2433            && let Some(adapter) = self.clients.get(&client_id)
2434        {
2435            return Some(adapter);
2436        }
2437
2438        if let Some(account_id) = self.account_id_for_command(command) {
2439            let issuer = account_id.get_issuer();
2440            let issuer_client_id = ClientId::from(issuer.as_str());
2441
2442            if let Some(adapter) = self.clients.get(&issuer_client_id) {
2443                return Some(adapter);
2444            }
2445
2446            if let Some(client_id) = self.routing_map.get(&issuer)
2447                && let Some(adapter) = self.clients.get(client_id)
2448            {
2449                return Some(adapter);
2450            }
2451        }
2452
2453        if let Some(instrument_id) = Self::instrument_id_for_command(command)
2454            && let Some(client_id) = self.routing_map.get(&instrument_id.venue)
2455            && let Some(adapter) = self.clients.get(client_id)
2456        {
2457            return Some(adapter);
2458        }
2459
2460        self.default_client_id.and_then(|id| self.clients.get(&id))
2461    }
2462
2463    fn account_id_for_command(&self, command: &TradingCommand) -> Option<AccountId> {
2464        match command {
2465            TradingCommand::QueryAccount(cmd) => Some(cmd.account_id),
2466            TradingCommand::SubmitOrder(cmd) => self
2467                .cache
2468                .borrow()
2469                .order(&cmd.client_order_id)
2470                .and_then(|order| order.account_id()),
2471            TradingCommand::ModifyOrder(cmd) => self
2472                .cache
2473                .borrow()
2474                .order(&cmd.client_order_id)
2475                .and_then(|order| order.account_id()),
2476            TradingCommand::CancelOrder(cmd) => self
2477                .cache
2478                .borrow()
2479                .order(&cmd.client_order_id)
2480                .and_then(|order| order.account_id()),
2481            TradingCommand::SubmitOrderList(_)
2482            | TradingCommand::ModifyOrders(_)
2483            | TradingCommand::CancelOrders(_)
2484            | TradingCommand::CancelAllOrders(_)
2485            | TradingCommand::QueryOrder(_) => None,
2486        }
2487    }
2488
2489    const fn instrument_id_for_command(command: &TradingCommand) -> Option<InstrumentId> {
2490        match command {
2491            TradingCommand::SubmitOrder(cmd) => Some(cmd.instrument_id),
2492            TradingCommand::SubmitOrderList(cmd) => Some(cmd.instrument_id),
2493            TradingCommand::ModifyOrder(cmd) => Some(cmd.instrument_id),
2494            TradingCommand::ModifyOrders(cmd) => Some(cmd.instrument_id),
2495            TradingCommand::CancelOrder(cmd) => Some(cmd.instrument_id),
2496            TradingCommand::CancelOrders(cmd) => Some(cmd.instrument_id),
2497            TradingCommand::CancelAllOrders(cmd) => Some(cmd.instrument_id),
2498            TradingCommand::QueryOrder(cmd) => Some(cmd.instrument_id),
2499            TradingCommand::QueryAccount(_) => None,
2500        }
2501    }
2502
2503    fn handle_submit_order(&self, client: &dyn ExecutionClient, cmd: SubmitOrder) {
2504        let client_order_id = cmd.client_order_id;
2505        let cached_order = { self.cache.borrow().order_owned(&client_order_id) };
2506
2507        let (order, added_to_cache) = match cached_order {
2508            Some(order) => (order, false),
2509            None => {
2510                let Some(order) = self.add_order_from_init(&cmd.order_init, cmd.position_id, &cmd)
2511                else {
2512                    return;
2513                };
2514
2515                (order, true)
2516            }
2517        };
2518
2519        if added_to_cache && self.config.snapshot_orders {
2520            self.create_order_state_snapshot(&order);
2521        }
2522
2523        let order_venue = order.instrument_id().venue;
2524        let client_venue = client.venue();
2525        if !client.handles_order_venue(order_venue) {
2526            let client_id = client.client_id();
2527            let reason = OrderDeniedReason::ClientVenueMismatch {
2528                client_id,
2529                order_venue,
2530                client_venue,
2531            }
2532            .to_string();
2533            self.deny_order(&order, &reason);
2534            return;
2535        }
2536
2537        if let Some(reason) = self.check_position_id_against_oms(
2538            cmd.instrument_id,
2539            cmd.strategy_id,
2540            cmd.position_id,
2541            client,
2542        ) {
2543            self.deny_order(&order, &reason.to_string());
2544            return;
2545        }
2546
2547        let instrument_id = order.instrument_id();
2548
2549        if !added_to_cache && self.config.snapshot_orders {
2550            self.create_order_state_snapshot(&order);
2551        }
2552
2553        {
2554            let cache = self.cache.borrow();
2555            if cache.instrument(&instrument_id).is_none() {
2556                log::error!(
2557                    "Cannot handle submit order: no instrument found for {instrument_id}, {cmd}",
2558                );
2559                return;
2560            }
2561        }
2562
2563        let client_id = client.client_id();
2564        let claim_result = self
2565            .cache
2566            .borrow_mut()
2567            .claim_order_clients(&[(client_order_id, client_id)]);
2568
2569        if let Err(e) = claim_result {
2570            self.deny_order(
2571                &order,
2572                &OrderDeniedReason::ValidationFailed {
2573                    detail: format!(
2574                        "Failed to claim execution client {client_id} for {client_order_id}: {e}"
2575                    ),
2576                }
2577                .to_string(),
2578            );
2579            return;
2580        }
2581
2582        if self.config.manage_own_order_books && should_handle_own_book_order(&order) {
2583            let mut own_book = self.get_or_init_own_order_book(&order.instrument_id());
2584            own_book.add(order.to_own_book_order());
2585        }
2586
2587        log_info!("Submit {order}", color = LogColor::Blue);
2588
2589        if let Err(e) = client.submit_order(cmd) {
2590            self.deny_order(
2591                &order,
2592                &OrderDeniedReason::SubmitFailed {
2593                    detail: e.to_string(),
2594                }
2595                .to_string(),
2596            );
2597            return;
2598        }
2599
2600        self.orders_dispatched.borrow_mut().insert(client_order_id);
2601    }
2602
2603    fn handle_submit_order_list(&self, client: &dyn ExecutionClient, cmd: SubmitOrderList) {
2604        let mut orders = Vec::with_capacity(cmd.order_list.client_order_ids.len());
2605        let mut added_client_order_ids = AHashSet::new();
2606
2607        for client_order_id in &cmd.order_list.client_order_ids {
2608            let cached_order = { self.cache.borrow().order_owned(client_order_id) };
2609
2610            if let Some(order) = cached_order {
2611                orders.push(order);
2612                continue;
2613            }
2614
2615            let Some(order_init) = cmd
2616                .order_inits
2617                .iter()
2618                .find(|init| init.client_order_id == *client_order_id)
2619            else {
2620                log::error!(
2621                    "Cannot handle submit order list: order not found in cache and no initialization event for {client_order_id}, {cmd}"
2622                );
2623                continue;
2624            };
2625
2626            let Some(order) = self.add_order_from_init(order_init, cmd.position_id, &cmd) else {
2627                continue;
2628            };
2629
2630            added_client_order_ids.insert(order.client_order_id());
2631            orders.push(order);
2632        }
2633
2634        if self.config.snapshot_orders {
2635            for order in &orders {
2636                if added_client_order_ids.contains(&order.client_order_id()) {
2637                    self.create_order_state_snapshot(order);
2638                }
2639            }
2640        }
2641
2642        if orders.len() != cmd.order_list.client_order_ids.len() {
2643            let reason = OrderDeniedReason::OrderListIncomplete {
2644                order_list_id: cmd.order_list.id,
2645            }
2646            .to_string();
2647
2648            for order in &orders {
2649                self.deny_order(order, &reason);
2650            }
2651            return;
2652        }
2653
2654        let order_list_venue = cmd.instrument_id.venue;
2655        let client_venue = client.venue();
2656        if !client.handles_order_venue(order_list_venue) {
2657            let client_id = client.client_id();
2658            let reason = OrderDeniedReason::ClientVenueMismatch {
2659                client_id,
2660                order_venue: order_list_venue,
2661                client_venue,
2662            }
2663            .to_string();
2664
2665            for order in &orders {
2666                self.deny_order(order, &reason);
2667            }
2668            return;
2669        }
2670
2671        let is_uniform_instrument = orders
2672            .iter()
2673            .all(|o| o.instrument_id() == cmd.instrument_id);
2674
2675        if let Some(position_id) = cmd.position_id
2676            && !is_uniform_instrument
2677        {
2678            let reason = OrderDeniedReason::InvalidPositionId {
2679                position_id,
2680                detail: "not valid for a mixed-instrument order list; a position belongs to a single instrument"
2681                    .to_string(),
2682            }
2683            .to_string();
2684
2685            for order in &orders {
2686                self.deny_order(order, &reason);
2687            }
2688            return;
2689        }
2690
2691        if let Some(reason) = self.check_position_id_against_oms(
2692            cmd.instrument_id,
2693            cmd.strategy_id,
2694            cmd.position_id,
2695            client,
2696        ) {
2697            let reason = reason.to_string();
2698            for order in &orders {
2699                self.deny_order(order, &reason);
2700            }
2701            return;
2702        }
2703
2704        if self.config.snapshot_orders {
2705            for order in &orders {
2706                if !added_client_order_ids.contains(&order.client_order_id()) {
2707                    self.create_order_state_snapshot(order);
2708                }
2709            }
2710        }
2711
2712        {
2713            let cache = self.cache.borrow();
2714            if cache.instrument(&cmd.instrument_id).is_none() {
2715                log::error!(
2716                    "Cannot handle submit order list: no instrument found for {}, {cmd}",
2717                    cmd.instrument_id,
2718                );
2719                return;
2720            }
2721        }
2722
2723        let client_id = client.client_id();
2724        let claims = orders
2725            .iter()
2726            .map(|order| (order.client_order_id(), client_id))
2727            .collect::<Vec<_>>();
2728        let claim_result = self.cache.borrow_mut().claim_order_clients(&claims);
2729        if let Err(e) = claim_result {
2730            let reason = OrderDeniedReason::ValidationFailed {
2731                detail: format!(
2732                    "Failed to claim execution client {client_id} for order list {}: {e}",
2733                    cmd.order_list.id,
2734                ),
2735            }
2736            .to_string();
2737
2738            for order in &orders {
2739                self.deny_order(order, &reason);
2740            }
2741            return;
2742        }
2743
2744        if self.config.manage_own_order_books {
2745            for order in &orders {
2746                if should_handle_own_book_order(order) {
2747                    let mut own_book = self.get_or_init_own_order_book(&order.instrument_id());
2748                    own_book.add(order.to_own_book_order());
2749                }
2750            }
2751        }
2752
2753        log_info!("Submit {}", cmd.order_list, color = LogColor::Blue);
2754
2755        if let Err(e) = client.submit_order_list(cmd) {
2756            log::error!(
2757                "Error submitting order list to client {}",
2758                client.client_id()
2759            );
2760
2761            let reason = OrderDeniedReason::SubmitFailed {
2762                detail: e.to_string(),
2763            }
2764            .to_string();
2765
2766            for order in &orders {
2767                self.deny_order(order, &reason);
2768            }
2769            return;
2770        }
2771
2772        self.orders_dispatched
2773            .borrow_mut()
2774            .extend(orders.iter().map(Order::client_order_id));
2775    }
2776
2777    fn add_order_from_init(
2778        &self,
2779        order_init: &OrderInitialized,
2780        position_id: Option<PositionId>,
2781        context: &dyn Display,
2782    ) -> Option<OrderAny> {
2783        let client_order_id = order_init.client_order_id;
2784        let order = match OrderAny::from_events(vec![OrderEventAny::Initialized(
2785            order_init.clone(),
2786        )]) {
2787            Ok(order) => order,
2788            Err(e) => {
2789                log::error!(
2790                    "Cannot reconstruct order from initialization event for {client_order_id}: {e}, {context}"
2791                );
2792                return None;
2793            }
2794        };
2795
2796        if let Err(e) = self
2797            .cache
2798            .borrow_mut()
2799            .add_order(order.clone(), position_id, None, true)
2800        {
2801            log::error!(
2802                "Cannot add reconstructed order to cache for {client_order_id}: {e}, {context}"
2803            );
2804            return None;
2805        }
2806
2807        Some(order)
2808    }
2809
2810    fn handle_modify_order(&self, client: &dyn ExecutionClient, cmd: ModifyOrder) {
2811        let venue_str = cmd
2812            .venue_order_id
2813            .map_or_else(String::new, |venue_order_id| format!(" {venue_order_id}"));
2814
2815        log_info!(
2816            "Modify {}{venue_str}",
2817            cmd.client_order_id,
2818            color = LogColor::Blue
2819        );
2820
2821        let command_id = cmd.command_id;
2822        if client.modify_order(cmd).is_err() {
2823            log::error!(
2824                "Error modifying order: client {}, command {command_id}",
2825                client.client_id()
2826            );
2827        }
2828    }
2829
2830    fn handle_batch_modify_orders(&self, client: &dyn ExecutionClient, cmd: BatchModifyOrders) {
2831        let command_id = cmd.command_id;
2832        if client.batch_modify_orders(cmd).is_err() {
2833            log::error!(
2834                "Error batch modifying orders: client {}, command {command_id}",
2835                client.client_id()
2836            );
2837        }
2838    }
2839
2840    fn handle_cancel_order(&self, client: &dyn ExecutionClient, cmd: CancelOrder) {
2841        let venue_str = cmd
2842            .venue_order_id
2843            .map_or_else(String::new, |venue_order_id| format!(" {venue_order_id}"));
2844
2845        log_info!(
2846            "Cancel {}{venue_str}",
2847            cmd.client_order_id,
2848            color = LogColor::Blue
2849        );
2850
2851        let command_id = cmd.command_id;
2852        if client.cancel_order(cmd).is_err() {
2853            log::error!(
2854                "Error canceling order: client {}, command {command_id}",
2855                client.client_id()
2856            );
2857        }
2858    }
2859
2860    fn handle_cancel_all_orders(&self, client: &dyn ExecutionClient, command: &CancelAllOrders) {
2861        let client_id = client.client_id();
2862        let account_id = client.account_id();
2863        let algorithm_commands = self.plan_cancel_all_orders(command, client_id, account_id);
2864        let venue_command = Self::create_cancel_all_child(command, client_id);
2865        let emulator_command = Self::create_cancel_all_child(command, client_id);
2866        let side_str = command
2867            .order_side
2868            .map_or_else(|| " ".to_string(), |order_side| format!(" {order_side} "));
2869
2870        log_info!("Cancel all{side_str}orders", color = LogColor::Blue);
2871
2872        let command_id = command.command_id;
2873
2874        if client.cancel_all_orders(venue_command).is_err() {
2875            log::error!(
2876                "Error canceling all orders: client {}, command {command_id}",
2877                client.client_id()
2878            );
2879        }
2880
2881        msgbus::send_trading_command(
2882            MessagingSwitchboard::order_emulator_execute(),
2883            TradingCommand::CancelAllOrders(emulator_command),
2884        );
2885
2886        for (exec_algorithm_id, algorithm_command) in algorithm_commands {
2887            let endpoint = format!("{exec_algorithm_id}.execute");
2888            msgbus::send_any(
2889                endpoint.into(),
2890                &TradingCommand::CancelOrder(algorithm_command),
2891            );
2892        }
2893    }
2894
2895    fn plan_cancel_all_orders(
2896        &self,
2897        command: &CancelAllOrders,
2898        client_id: ClientId,
2899        account_id: AccountId,
2900    ) -> Vec<(ExecAlgorithmId, CancelOrder)> {
2901        let order_side = command.order_side;
2902        let candidates: Vec<(OrderAny, bool)> = {
2903            let cache = self.cache.borrow();
2904            cache
2905                .orders_active_local_refs(
2906                    None,
2907                    Some(&command.instrument_id),
2908                    None,
2909                    None,
2910                    order_side,
2911                )
2912                .into_iter()
2913                .filter_map(|order| {
2914                    if order
2915                        .account_id()
2916                        .is_some_and(|order_account_id| order_account_id != account_id)
2917                    {
2918                        return None;
2919                    }
2920
2921                    let cached_client_id = cache.client_id(&order.client_order_id()).copied();
2922                    let matches_client = match cached_client_id {
2923                        Some(order_client_id) => order_client_id == client_id,
2924                        None => command.client_id.is_none(),
2925                    };
2926
2927                    if !matches_client {
2928                        return None;
2929                    }
2930
2931                    let is_emulated = order.is_emulated() || order.emulation_trigger().is_some();
2932                    if !is_emulated && order.exec_algorithm_id().is_none() {
2933                        return None;
2934                    }
2935
2936                    Some((order.cloned(), cached_client_id.is_none()))
2937                })
2938                .collect()
2939        };
2940
2941        let claims: Vec<_> = candidates
2942            .iter()
2943            .filter_map(|(order, needs_claim)| {
2944                needs_claim.then_some((order.client_order_id(), client_id))
2945            })
2946            .collect();
2947        let claims_succeeded = claims.is_empty()
2948            || match self.cache.borrow_mut().claim_order_clients(&claims) {
2949                Ok(()) => true,
2950                Err(e) => {
2951                    log::error!(
2952                        "Cannot scope local cancel-all orders to execution client {client_id}: {e}"
2953                    );
2954                    false
2955                }
2956            };
2957        let correlation_id = command.correlation_id.or(Some(command.command_id));
2958        let mut algorithm_commands = Vec::new();
2959
2960        for (order, needs_claim) in candidates {
2961            if needs_claim && !claims_succeeded {
2962                continue;
2963            }
2964
2965            let is_emulated = order.is_emulated() || order.emulation_trigger().is_some();
2966            if is_emulated {
2967                continue;
2968            }
2969
2970            if let Some(exec_algorithm_id) = order.exec_algorithm_id() {
2971                let mut child = CancelOrder::new(
2972                    command.trader_id,
2973                    Some(client_id),
2974                    order.strategy_id(),
2975                    order.instrument_id(),
2976                    order.client_order_id(),
2977                    order.venue_order_id(),
2978                    UUID4::new(),
2979                    command.ts_init,
2980                    command.params.clone(),
2981                    correlation_id,
2982                );
2983                child.causation_id = Some(command.command_id);
2984                algorithm_commands.push((exec_algorithm_id, child));
2985            }
2986        }
2987
2988        algorithm_commands.sort_by_key(|(exec_algorithm_id, command)| {
2989            (*exec_algorithm_id, command.client_order_id)
2990        });
2991        algorithm_commands.dedup_by_key(|(_, command)| command.client_order_id);
2992
2993        algorithm_commands
2994    }
2995
2996    fn create_cancel_all_child(command: &CancelAllOrders, client_id: ClientId) -> CancelAllOrders {
2997        let mut child = CancelAllOrders::new(
2998            command.trader_id,
2999            Some(client_id),
3000            command.strategy_id,
3001            command.instrument_id,
3002            command.order_side,
3003            UUID4::new(),
3004            command.ts_init,
3005            command.params.clone(),
3006            command.correlation_id.or(Some(command.command_id)),
3007        );
3008        child.causation_id = Some(command.command_id);
3009        child
3010    }
3011
3012    fn handle_batch_cancel_orders(&self, client: &dyn ExecutionClient, cmd: BatchCancelOrders) {
3013        let client_order_ids: Vec<ClientOrderId> = cmd
3014            .cancels
3015            .iter()
3016            .map(|cancel| cancel.client_order_id)
3017            .collect();
3018
3019        log_info!(
3020            "Batch cancel orders {client_order_ids:?}",
3021            color = LogColor::Blue
3022        );
3023
3024        let command_id = cmd.command_id;
3025        if client.batch_cancel_orders(cmd).is_err() {
3026            log::error!(
3027                "Error batch canceling orders: client {}, command {command_id}",
3028                client.client_id()
3029            );
3030        }
3031    }
3032
3033    fn handle_query_account(&self, client: &dyn ExecutionClient, cmd: QueryAccount) {
3034        log_info!("Query {}", cmd.account_id, color = LogColor::Blue);
3035
3036        let command_id = cmd.command_id;
3037        if client.query_account(cmd).is_err() {
3038            log::warn!(
3039                "Error querying account: client {}, command {command_id}",
3040                client.client_id()
3041            );
3042        }
3043    }
3044
3045    fn handle_query_order(&self, client: &dyn ExecutionClient, cmd: QueryOrder) {
3046        log_info!("Query {}", cmd.client_order_id, color = LogColor::Blue);
3047
3048        let command_id = cmd.command_id;
3049        if client.query_order(cmd).is_err() {
3050            log::warn!(
3051                "Error querying order: client {}, command {command_id}",
3052                client.client_id()
3053            );
3054        }
3055    }
3056
3057    fn create_order_state_snapshot(&self, order: &OrderAny) {
3058        if self.config.debug {
3059            log::debug!("Creating order state snapshot for {order}");
3060        }
3061
3062        if self.cache.borrow().has_backing()
3063            && let Err(e) = self.cache.borrow().snapshot_order_state(order)
3064        {
3065            log::warn!("Failed to snapshot order state: {e}");
3066        }
3067    }
3068
3069    fn create_position_state_snapshot(&self, position: &Position, open_only: bool) {
3070        Self::publish_position_state_snapshot(
3071            &self.clock,
3072            &self.cache,
3073            self.config.debug,
3074            position,
3075            open_only,
3076        );
3077    }
3078
3079    fn publish_position_state_snapshot(
3080        clock: &Rc<RefCell<dyn Clock>>,
3081        cache: &Rc<RefCell<Cache>>,
3082        debug: bool,
3083        position: &Position,
3084        open_only: bool,
3085    ) {
3086        if debug {
3087            log::debug!("Creating position state snapshot for {position}");
3088        }
3089
3090        let ts_snapshot = clock.borrow().timestamp_ns();
3091        let unrealized_pnl = cache.borrow().calculate_unrealized_pnl(position);
3092
3093        let snapshot = PositionStateSnapshot {
3094            position: position.clone(),
3095            unrealized_pnl,
3096            ts_snapshot,
3097        };
3098
3099        let topic = switchboard::get_snapshot_position_topic(position.id);
3100        msgbus::publish_any(topic, &snapshot);
3101
3102        let has_backing = cache.borrow().has_backing();
3103        if has_backing
3104            && let Err(e) = cache.borrow_mut().snapshot_position_state(
3105                position,
3106                ts_snapshot,
3107                unrealized_pnl,
3108                Some(open_only),
3109            )
3110        {
3111            log::warn!("Failed to snapshot position state: {e}");
3112        }
3113    }
3114
3115    fn handle_event(&mut self, event: &OrderEventAny) {
3116        self.handle_event_with_position_application(event, true);
3117    }
3118
3119    fn handle_event_with_position_application(
3120        &mut self,
3121        event: &OrderEventAny,
3122        apply_position: bool,
3123    ) {
3124        let applied = self.apply_order_event(event, apply_position);
3125
3126        // Negative acknowledgment for clients awaiting application of an enqueued fill or void.
3127        // The declined event itself is the acknowledgment; each drop site logs its reason.
3128        if apply_position
3129            && !applied
3130            && matches!(
3131                event,
3132                OrderEventAny::Filled(_) | OrderEventAny::FillVoided(_)
3133            )
3134        {
3135            let topic = switchboard::get_order_fill_declined_topic(event.instrument_id());
3136            msgbus::publish_order_event(topic, event);
3137        }
3138    }
3139
3140    /// Applies an order event, returning whether it was applied and published.
3141    fn apply_order_event(&mut self, event: &OrderEventAny, apply_position: bool) -> bool {
3142        if let OrderEventAny::Filled(fill) = event
3143            && fill.last_qty.is_zero()
3144        {
3145            log::warn!("Skipping zero-quantity fill event: {fill}");
3146            return false;
3147        }
3148
3149        self.event_count += 1;
3150
3151        if self.config.debug {
3152            log::debug!("{RECV}{EVT} {event}");
3153        }
3154
3155        let event_client_order_id = event.client_order_id();
3156        let cache = self.cache.borrow();
3157        let client_order_id = if cache.order_exists(&event_client_order_id) {
3158            event_client_order_id
3159        } else {
3160            let is_leg_fill =
3161                matches!(event, OrderEventAny::Filled(fill) if self.is_leg_fill(fill));
3162            if !is_leg_fill {
3163                log::warn!(
3164                    "Order with {} not found in the cache to apply {}",
3165                    event.client_order_id(),
3166                    event
3167                );
3168            }
3169
3170            // Try to find order by venue order ID if available
3171            let venue_order_id = if let Some(id) = event.venue_order_id() {
3172                id
3173            } else {
3174                log::error!(
3175                    "Cannot apply event to any order: {} not found in the cache with no VenueOrderId",
3176                    event.client_order_id()
3177                );
3178                return false;
3179            };
3180
3181            // Look up client order ID from venue order ID
3182            let client_order_id = if let Some(id) = cache.client_order_id(&venue_order_id) {
3183                *id
3184            } else {
3185                if let OrderEventAny::Filled(fill) = event
3186                    && is_leg_fill
3187                {
3188                    log::info!(
3189                        "Processing leg fill without corresponding order: {} for instrument {}",
3190                        fill.client_order_id,
3191                        fill.instrument_id
3192                    );
3193                    drop(cache);
3194                    return self.handle_leg_fill_without_order(fill.clone());
3195                }
3196
3197                log::error!(
3198                    "Cannot apply event to any order: {} and {venue_order_id} not found in the cache",
3199                    event.client_order_id(),
3200                );
3201                return false;
3202            };
3203
3204            // Get order using found client order ID
3205            if cache.order_exists(&client_order_id) {
3206                log::info!("Order with {client_order_id} was found in the cache");
3207                client_order_id
3208            } else {
3209                if let OrderEventAny::Filled(fill) = event
3210                    && is_leg_fill
3211                {
3212                    log::info!(
3213                        "Processing leg fill without corresponding order: {} for instrument {}",
3214                        fill.client_order_id,
3215                        fill.instrument_id
3216                    );
3217                    drop(cache);
3218                    return self.handle_leg_fill_without_order(fill.clone());
3219                }
3220
3221                log::error!(
3222                    "Cannot apply event to any order: {client_order_id} and {venue_order_id} not found in cache",
3223                );
3224                return false;
3225            }
3226        };
3227        let order_before_fill = if matches!(event, OrderEventAny::Filled(_)) {
3228            cache.order(&client_order_id).map(|o| o.clone())
3229        } else {
3230            None
3231        };
3232
3233        drop(cache);
3234
3235        let event = if event_client_order_id == client_order_id {
3236            event.clone()
3237        } else {
3238            event.clone().with_client_order_id(client_order_id)
3239        };
3240
3241        match &event {
3242            OrderEventAny::Filled(fill) => {
3243                let Some(order_before_fill) = order_before_fill else {
3244                    log::error!(
3245                        "Cannot apply fill: order {} not found in the cache",
3246                        fill.client_order_id()
3247                    );
3248                    return false;
3249                };
3250                let configured_oms_type = self.determine_oms_type(fill);
3251                let Some(position_id) =
3252                    self.determine_position_id(fill, configured_oms_type, Some(&order_before_fill))
3253                else {
3254                    return false;
3255                };
3256                let oms_type = self
3257                    .cache
3258                    .borrow()
3259                    .oms_type(&position_id)
3260                    .unwrap_or(configured_oms_type);
3261
3262                let mut fill = fill.clone();
3263                fill.position_id = Some(position_id);
3264
3265                let restatement = is_fill_price_restatement(&order_before_fill, &fill);
3266                let validation = if apply_position {
3267                    self.validate_fill_for_order(&order_before_fill, &fill)
3268                } else {
3269                    self.validate_fill_for_order_projection(&order_before_fill, &fill)
3270                };
3271
3272                if validation.is_err() {
3273                    return false;
3274                }
3275
3276                let affects_quantity = fill_affects_quantity(apply_position, restatement);
3277                if affects_quantity
3278                    && !self.validate_fill_for_external_position(
3279                        &order_before_fill,
3280                        &fill,
3281                        oms_type,
3282                        position_id,
3283                    )
3284                {
3285                    return false;
3286                }
3287
3288                let event = OrderEventAny::Filled(fill.clone());
3289
3290                let Some(order) =
3291                    self.update_cached_order(client_order_id, &event, affects_quantity)
3292                else {
3293                    return false;
3294                };
3295
3296                let position_events = if apply_position {
3297                    self.handle_order_fill(&order, fill, oms_type, restatement)
3298                } else {
3299                    Vec::new()
3300                };
3301
3302                self.publish_order_event(&event);
3303                self.publish_position_events(position_events);
3304            }
3305            OrderEventAny::FillVoided(voided) => {
3306                let mut voided = voided.clone();
3307                let Some(order_before_void) = self
3308                    .cache
3309                    .borrow()
3310                    .order(&client_order_id)
3311                    .map(|order| order.clone())
3312                else {
3313                    log::error!("Cannot apply fill void: order {client_order_id} not found");
3314                    return false;
3315                };
3316                let original_fill = order_before_void
3317                    .events()
3318                    .into_iter()
3319                    .find_map(|candidate| match candidate {
3320                        OrderEventAny::Filled(fill) if fill.trade_id == voided.trade_id => {
3321                            Some(fill.clone())
3322                        }
3323                        _ => None,
3324                    });
3325
3326                if voided.position_id.is_none() {
3327                    voided.position_id = original_fill.as_ref().and_then(|fill| fill.position_id);
3328                }
3329                let event = OrderEventAny::FillVoided(voided.clone());
3330
3331                let mut validated_order = order_before_void.clone();
3332                match validated_order.apply(event.clone()) {
3333                    Ok(()) => {}
3334                    Err(OrderError::DuplicateFillVoid(trade_id)) => {
3335                        log::warn!(
3336                            "Duplicate fill void rejected at order level: trade_id={trade_id}"
3337                        );
3338                        return false;
3339                    }
3340                    Err(e) => {
3341                        log::error!("Cannot apply fill void to order: {e}");
3342                        return false;
3343                    }
3344                }
3345
3346                let corrected_positions = if apply_position
3347                    && original_fill
3348                        .as_ref()
3349                        .is_some_and(|fill| fill.position_id.is_some())
3350                    && !self.skips_settled_position_change(
3351                        "Fill void",
3352                        voided.instrument_id,
3353                        voided.trade_id,
3354                    ) {
3355                    match self.prepare_order_fill_void_positions(&order_before_void, &voided) {
3356                        Ok(positions) => positions,
3357                        Err(e) => {
3358                            log::error!("Cannot apply fill void to positions: {e}");
3359                            return false;
3360                        }
3361                    }
3362                } else {
3363                    Vec::new()
3364                };
3365
3366                let mut position_events = Vec::new();
3367
3368                for CorrectedPosition {
3369                    position,
3370                    corrected_qty,
3371                    absorbed_prior_cycles,
3372                    closed_cycles_pnl,
3373                } in corrected_positions
3374                {
3375                    if let Err(e) = self.cache.borrow_mut().update_position(&position) {
3376                        log::error!("Cannot apply fill void to position {}: {e}", position.id);
3377                        return false;
3378                    }
3379
3380                    if absorbed_prior_cycles {
3381                        log::info!(
3382                            "Settling archived NETTING cycles rebuilt by fill void {} for position {}: realized={closed_cycles_pnl:?}",
3383                            voided.trade_id,
3384                            position.id,
3385                        );
3386
3387                        self.cache
3388                            .borrow_mut()
3389                            .settle_position_snapshots(&position, closed_cycles_pnl);
3390                    }
3391
3392                    if self.config.snapshot_positions {
3393                        self.create_position_state_snapshot(&position, false);
3394                    }
3395
3396                    position_events.push(Self::create_fill_void_position_event(
3397                        &position,
3398                        &voided,
3399                        corrected_qty,
3400                    ));
3401                }
3402
3403                if self
3404                    .update_cached_order(client_order_id, &event, true)
3405                    .is_none()
3406                {
3407                    return false;
3408                }
3409
3410                if original_fill.is_some() {
3411                    let portfolio_endpoint = MessagingSwitchboard::portfolio_update_order();
3412                    msgbus::send_order_event(portfolio_endpoint, event.clone());
3413                }
3414                self.publish_order_event(&event);
3415                self.publish_position_events(position_events);
3416            }
3417            _ => {
3418                if self
3419                    .update_cached_order(client_order_id, &event, true)
3420                    .is_some()
3421                {
3422                    self.publish_order_event(&event);
3423                }
3424            }
3425        }
3426
3427        true
3428    }
3429
3430    fn handle_leg_fill_without_order(&mut self, mut fill: OrderFilled) -> bool {
3431        let instrument =
3432            if let Some(instrument) = self.cache.borrow().instrument(&fill.instrument_id) {
3433                instrument.clone()
3434            } else {
3435                log::error!(
3436                    "Cannot handle leg fill: no instrument found for {}, {fill}",
3437                    fill.instrument_id,
3438                );
3439                return false;
3440            };
3441
3442        if let Err(e) = self.cache.borrow().try_account(&fill.account_id) {
3443            log::error!("Cannot handle leg fill: {e}, {fill}");
3444            return false;
3445        }
3446
3447        let oms_type = self.determine_oms_type(&fill);
3448        let position_id = self.determine_leg_fill_position_id(&fill, oms_type);
3449        fill.position_id = Some(position_id);
3450
3451        if !self.validate_fill_for_position(position_id, &fill) {
3452            return false;
3453        }
3454
3455        let duplicate_position_fill = self.position_contains_trade_id(position_id, fill.trade_id);
3456
3457        let event = OrderEventAny::Filled(fill.clone());
3458
3459        if duplicate_position_fill {
3460            log::warn!(
3461                "Duplicate leg fill: {} trade_id={} already applied to position {}, skipping",
3462                fill.client_order_id,
3463                fill.trade_id,
3464                position_id
3465            );
3466            return false;
3467        }
3468
3469        let portfolio_endpoint = MessagingSwitchboard::portfolio_update_order();
3470        msgbus::send_order_event(portfolio_endpoint, event.clone());
3471        let position_events = self.handle_position_update(&instrument, fill, oms_type);
3472        self.publish_order_event(&event);
3473        self.publish_position_events(position_events);
3474
3475        true
3476    }
3477
3478    fn determine_leg_fill_position_id(
3479        &mut self,
3480        fill: &OrderFilled,
3481        oms_type: OmsType,
3482    ) -> PositionId {
3483        let cache = self.cache.borrow();
3484        let cached_position_id = cache.position_id(&fill.client_order_id()).copied();
3485        drop(cache);
3486
3487        if let Some(position_id) = cached_position_id {
3488            if let Some(fill_position_id) = fill.position_id
3489                && fill_position_id != position_id
3490            {
3491                log::warn!(
3492                    "Incorrect position ID assigned to leg fill: \
3493                     cached={position_id}, assigned={fill_position_id}; \
3494                     re-assigning from cache",
3495                );
3496            }
3497
3498            return position_id;
3499        }
3500
3501        match oms_type {
3502            OmsType::Hedging => self
3503                .orderless_hedging_leg_position_id(fill)
3504                .or(fill.position_id)
3505                .unwrap_or_else(|| self.pos_id_generator.generate(fill.strategy_id, false)),
3506            OmsType::Netting => self.determine_netting_position_id(fill, None),
3507            _ => self.determine_netting_position_id(fill, None),
3508        }
3509    }
3510
3511    fn orderless_hedging_leg_position_id(&self, fill: &OrderFilled) -> Option<PositionId> {
3512        if !self.is_leg_fill(fill) {
3513            return None;
3514        }
3515
3516        let cache = self.cache.borrow();
3517        if cache.order_exists(&fill.client_order_id()) {
3518            return None;
3519        }
3520
3521        let matching_positions: Vec<PositionId> = cache
3522            .positions_open(
3523                Some(&fill.instrument_id.venue),
3524                Some(&fill.instrument_id),
3525                Some(&fill.strategy_id),
3526                Some(&fill.account_id),
3527                None,
3528            )
3529            .iter()
3530            .filter(|position| position.opening_order_id == fill.client_order_id)
3531            .map(|position| position.id)
3532            .collect();
3533
3534        match matching_positions.as_slice() {
3535            [position_id] => Some(*position_id),
3536            [] => None,
3537            _ => {
3538                log::warn!(
3539                    "Cannot uniquely correlate HEDGING leg fill {} to an orderless position: \
3540                     found {} positions with opening_order_id={}",
3541                    fill.trade_id,
3542                    matching_positions.len(),
3543                    fill.client_order_id,
3544                );
3545                None
3546            }
3547        }
3548    }
3549
3550    fn is_leg_fill(&self, fill: &OrderFilled) -> bool {
3551        if !fill.client_order_id.as_str().contains("-LEG-")
3552            && !fill.venue_order_id.as_str().contains("-LEG-")
3553        {
3554            return false;
3555        }
3556
3557        self.cache
3558            .borrow()
3559            .instrument(&fill.instrument_id)
3560            .is_some_and(|instrument| !instrument.is_spread())
3561    }
3562
3563    fn determine_oms_type(&self, fill: &OrderFilled) -> OmsType {
3564        let client_id = self
3565            .cache
3566            .borrow()
3567            .client_id(&fill.client_order_id)
3568            .copied();
3569
3570        let client = client_id.and_then(|id| self.get_client(&id)).or_else(|| {
3571            self.source_client_id_for_account(fill.account_id, &fill.instrument_id)
3572                .and_then(|id| self.get_client(&id))
3573        });
3574
3575        self.resolve_oms_type_for_client(fill.strategy_id, client)
3576    }
3577
3578    fn resolve_oms_type_for_client(
3579        &self,
3580        strategy_id: StrategyId,
3581        client: Option<&dyn ExecutionClient>,
3582    ) -> OmsType {
3583        if let Some(oms_type) = self.oms_overrides.get(&strategy_id)
3584            && *oms_type != OmsType::Unspecified
3585        {
3586            return *oms_type;
3587        }
3588
3589        // Missing or ambiguous ownership retains the origin-free NETTING fallback
3590        client.map_or(OmsType::Netting, ExecutionClient::oms_type)
3591    }
3592
3593    fn check_position_id_against_oms(
3594        &self,
3595        instrument_id: InstrumentId,
3596        strategy_id: StrategyId,
3597        position_id: Option<PositionId>,
3598        client: &dyn ExecutionClient,
3599    ) -> Option<OrderDeniedReason> {
3600        let position_id = position_id?;
3601
3602        if self.resolve_oms_type_for_client(strategy_id, Some(client)) != OmsType::Netting {
3603            return None;
3604        }
3605
3606        let expected = format!("{instrument_id}-{strategy_id}");
3607        if position_id.as_str() == expected {
3608            return None;
3609        }
3610
3611        Some(OrderDeniedReason::InvalidPositionId {
3612            position_id,
3613            detail: format!(
3614                "not valid for NETTING OMS; expected '{expected}' (use HEDGING for custom position IDs)"
3615            ),
3616        })
3617    }
3618
3619    fn determine_position_id(
3620        &mut self,
3621        fill: &OrderFilled,
3622        oms_type: OmsType,
3623        order: Option<&OrderAny>,
3624    ) -> Option<PositionId> {
3625        let cache = self.cache.borrow();
3626
3627        let flip = match (oms_type == OmsType::Hedging)
3628            .then(|| cache.hedging_flip_position_ids(fill.client_order_id))
3629            .transpose()
3630        {
3631            Ok(flip) => flip.flatten(),
3632            Err(e) => {
3633                log::error!("Cannot route hedging fill {}: {e}", fill.trade_id);
3634                return None;
3635            }
3636        };
3637
3638        let cached_position_id = flip
3639            .map(|ids| ids[1])
3640            .or_else(|| cache.position_id(&fill.client_order_id()).copied());
3641        drop(cache);
3642
3643        if self.config.debug {
3644            log::debug!(
3645                "Determining position ID for {}, position_id={:?}",
3646                fill.client_order_id(),
3647                cached_position_id,
3648            );
3649        }
3650
3651        if let Some(cached_position_id) = cached_position_id
3652            && let Some(fill_position_id) = fill.position_id
3653            && cached_position_id != fill_position_id
3654        {
3655            if oms_type == OmsType::Hedging
3656                && !flip.is_some_and(|ids| ids.contains(&fill_position_id))
3657            {
3658                log::error!(
3659                    "Cannot apply hedging fill {} for {}: venue position ID {fill_position_id} conflicts with cached position ID {cached_position_id}",
3660                    fill.trade_id,
3661                    fill.client_order_id(),
3662                );
3663
3664                return None;
3665            }
3666
3667            if oms_type != OmsType::Hedging {
3668                log::warn!(
3669                    "Incorrect position ID assigned to fill: \
3670                     cached={cached_position_id}, assigned={fill_position_id}; \
3671                     re-assigning from cache",
3672                );
3673            }
3674        }
3675
3676        if let Some(position_id) = cached_position_id {
3677            if self.config.debug {
3678                log::debug!("Assigned {position_id} to {}", fill.client_order_id());
3679            }
3680
3681            if !self.validate_fill_for_position(position_id, fill) {
3682                return None;
3683            }
3684
3685            return Some(position_id);
3686        }
3687
3688        let position_id = match (oms_type, fill.position_id) {
3689            (OmsType::Hedging, Some(position_id)) => position_id,
3690            (OmsType::Hedging, None) => self.determine_hedging_position_id(fill, order),
3691            (OmsType::Netting, _) => self.determine_netting_position_id(fill, order),
3692            _ => self.determine_netting_position_id(fill, order),
3693        };
3694
3695        if !self.validate_fill_for_position(position_id, fill) {
3696            return None;
3697        }
3698
3699        self.index_exec_spawn_position(fill.client_order_id, order, position_id);
3700
3701        Some(position_id)
3702    }
3703
3704    fn index_exec_spawn_position(
3705        &self,
3706        client_order_id: ClientOrderId,
3707        order: Option<&OrderAny>,
3708        position_id: PositionId,
3709    ) {
3710        let order = if let Some(o) = order {
3711            o.clone()
3712        } else {
3713            let cache = self.cache.borrow();
3714            cache.order(&client_order_id).map_or_else(
3715                || panic!("Order for {client_order_id} not found to determine position ID"),
3716                |o| o.clone(),
3717            )
3718        };
3719
3720        if order.exec_algorithm_id().is_some()
3721            && let Some(exec_spawn_id) = order.exec_spawn_id()
3722        {
3723            let cache = self.cache.borrow();
3724            let primary = if let Some(p) = cache.order(&exec_spawn_id) {
3725                p.clone()
3726            } else {
3727                log::warn!(
3728                    "Primary exec spawn order {exec_spawn_id} not found, \
3729                     skipping position ID propagation"
3730                );
3731                return;
3732            };
3733            let primary_already_indexed = cache.position_id(&primary.client_order_id()).is_some();
3734            drop(cache);
3735
3736            if primary.position_id().is_none() && !primary_already_indexed {
3737                if let Some(mut primary_mut) = self.cache.borrow_mut().order_mut(&exec_spawn_id) {
3738                    primary_mut.set_position_id(Some(position_id));
3739                }
3740                let _ = self.cache.borrow_mut().add_position_id(
3741                    &position_id,
3742                    &primary.instrument_id().venue,
3743                    &primary.client_order_id(),
3744                    &primary.strategy_id(),
3745                );
3746                log::debug!("Assigned primary order {position_id}");
3747            }
3748        }
3749    }
3750
3751    /// Returns whether `fill` may be applied to the position assigned to `position_id`.
3752    ///
3753    /// Only `instrument_id` is compared. A position's instrument never changes, and a fill
3754    /// for another instrument would be priced with this position's precision, multiplier,
3755    /// currencies, and PnL rules.
3756    ///
3757    /// `account_id` and `strategy_id` are deliberately NOT compared, because each has a
3758    /// legitimate mismatch path. Netting position IDs are `{instrument_id}-{strategy_id}`,
3759    /// so two accounts trading one instrument under one strategy share a position ID.
3760    /// External order claims can be handed to a successor strategy while the predecessor's
3761    /// positions stay cached, so a later venue fill can carry the new strategy against them.
3762    fn validate_fill_for_position(&self, position_id: PositionId, fill: &OrderFilled) -> bool {
3763        let cache = self.cache.borrow();
3764        let Some(position) = cache.position_ref(&position_id) else {
3765            return true;
3766        };
3767
3768        if position.instrument_id != fill.instrument_id {
3769            log::error!(
3770                "Cannot apply fill {} to position {position_id}: instrument_id mismatch, expected={}, received={}",
3771                fill.trade_id,
3772                position.instrument_id,
3773                fill.instrument_id
3774            );
3775            return false;
3776        }
3777
3778        true
3779    }
3780
3781    fn validate_fill_for_external_position(
3782        &self,
3783        order: &OrderAny,
3784        fill: &OrderFilled,
3785        oms_type: OmsType,
3786        position_id: PositionId,
3787    ) -> bool {
3788        if oms_type != OmsType::Netting || !order.is_reduce_only() {
3789            return true;
3790        }
3791
3792        let cache = self.cache.borrow();
3793
3794        let Some(position) = cache.position_ref(&position_id) else {
3795            return true;
3796        };
3797
3798        // A settled position takes no further fills, so only the order applies this one
3799        if position.is_settled() {
3800            return true;
3801        }
3802
3803        if position.strategy_id.is_external()
3804            && position.strategy_id != fill.strategy_id
3805            && (position.account_id != fill.account_id
3806                || !position.is_opposite_side(fill.order_side)
3807                || fill.last_qty > position.quantity)
3808        {
3809            log::error!(
3810                "Cannot apply reduce-only fill {} to external NETTING position {position_id}: \
3811                 account, side, or quantity does not match the open position",
3812                fill.trade_id,
3813            );
3814            return false;
3815        }
3816
3817        true
3818    }
3819
3820    fn determine_hedging_position_id(
3821        &mut self,
3822        fill: &OrderFilled,
3823        order: Option<&OrderAny>,
3824    ) -> PositionId {
3825        let cache = self.cache.borrow();
3826
3827        let cached_order;
3828        let order: &OrderAny = if let Some(order) = order {
3829            order
3830        } else {
3831            cached_order = cache.order(&fill.client_order_id()).unwrap_or_else(|| {
3832                panic!(
3833                    "Order for {} not found to determine position ID",
3834                    fill.client_order_id()
3835                )
3836            });
3837            &cached_order
3838        };
3839
3840        // Check execution spawn orders
3841        if let Some(spawn_id) = order.exec_spawn_id() {
3842            let spawn_orders = cache.orders_for_exec_spawn(&spawn_id);
3843            for spawned_order in spawn_orders {
3844                if let Some(pos_id) = spawned_order.position_id() {
3845                    if self.config.debug {
3846                        log::debug!("Found spawned {} for {}", pos_id, fill.client_order_id());
3847                    }
3848                    return pos_id;
3849                }
3850            }
3851        }
3852
3853        if order.is_reduce_only() {
3854            let mut candidates = cache
3855                .positions_open(
3856                    None,
3857                    Some(&fill.instrument_id),
3858                    Some(&fill.strategy_id),
3859                    Some(&fill.account_id),
3860                    None,
3861                )
3862                .into_iter()
3863                .filter(|position| position.is_opposite_side(fill.order_side));
3864            let candidate = candidates.next();
3865
3866            if let Some(position) = candidate
3867                && candidates.next().is_none()
3868                && order.would_reduce_only(position.side, position.quantity)
3869            {
3870                if self.config.debug {
3871                    log::debug!(
3872                        "Assigned reduce-only fill {} to position {}",
3873                        fill.client_order_id(),
3874                        position.id
3875                    );
3876                }
3877                return position.id;
3878            }
3879        }
3880
3881        // Generate new position ID
3882        let position_id = self.pos_id_generator.generate(fill.strategy_id, false);
3883
3884        if self.config.debug {
3885            log::debug!("Generated {} for {}", position_id, fill.client_order_id());
3886        }
3887        position_id
3888    }
3889
3890    fn determine_netting_position_id(
3891        &self,
3892        fill: &OrderFilled,
3893        order: Option<&OrderAny>,
3894    ) -> PositionId {
3895        let position_id = PositionId::new(format!("{}-{}", fill.instrument_id, fill.strategy_id));
3896        let cache = self.cache.borrow();
3897        if order.is_none_or(|order| !order.is_reduce_only())
3898            || cache
3899                .position_ref(&position_id)
3900                .is_some_and(|position| position.is_open())
3901        {
3902            return position_id;
3903        }
3904
3905        let mut candidates = cache
3906            .positions_open(
3907                None,
3908                Some(&fill.instrument_id),
3909                Some(&StrategyId::external()),
3910                Some(&fill.account_id),
3911                None,
3912            )
3913            .into_iter()
3914            .filter(|position| {
3915                position.is_opposite_side(fill.order_side)
3916                    && cache.oms_type(&position.id) == Some(OmsType::Netting)
3917            });
3918
3919        let candidate = candidates.next();
3920
3921        if let Some(position) = candidate
3922            && candidates.next().is_none()
3923            && fill.last_qty <= position.quantity
3924        {
3925            return position.id;
3926        }
3927
3928        position_id
3929    }
3930
3931    fn validate_fill_for_order(&self, order: &OrderAny, fill: &OrderFilled) -> anyhow::Result<()> {
3932        if order.is_duplicate_fill(fill) {
3933            log::warn!(
3934                "Duplicate fill: {} trade_id={} already applied, skipping",
3935                order.client_order_id(),
3936                fill.trade_id
3937            );
3938            anyhow::bail!("Duplicate fill");
3939        }
3940
3941        if is_fill_price_restatement(order, fill) {
3942            return Ok(());
3943        }
3944
3945        if let Some(position_id) = fill.position_id
3946            && self.position_contains_trade_id(position_id, fill.trade_id)
3947        {
3948            log::warn!(
3949                "Duplicate fill: {} trade_id={} already applied to position {}, skipping",
3950                order.client_order_id(),
3951                fill.trade_id,
3952                position_id
3953            );
3954            anyhow::bail!("Duplicate position fill");
3955        }
3956
3957        self.check_overfill(order, fill)
3958    }
3959
3960    fn validate_fill_for_order_projection(
3961        &self,
3962        order: &OrderAny,
3963        fill: &OrderFilled,
3964    ) -> anyhow::Result<()> {
3965        if order.is_duplicate_fill(fill) {
3966            anyhow::bail!("Duplicate fill");
3967        }
3968
3969        if is_fill_price_restatement(order, fill) {
3970            return Ok(());
3971        }
3972
3973        self.check_overfill(order, fill)
3974    }
3975
3976    fn position_contains_trade_id(&self, position_id: PositionId, trade_id: TradeId) -> bool {
3977        self.cache
3978            .borrow()
3979            .position(&position_id)
3980            .is_some_and(|position| position.trade_ids.contains(&trade_id))
3981    }
3982
3983    fn update_cached_order(
3984        &self,
3985        client_order_id: ClientOrderId,
3986        event: &OrderEventAny,
3987        send_portfolio_update: bool,
3988    ) -> Option<OrderAny> {
3989        let result = { self.cache.borrow_mut().update_order(event) };
3990
3991        let order = match result {
3992            Ok(order) => order,
3993            Err(e) => {
3994                if matches!(
3995                    e.downcast_ref::<OrderError>(),
3996                    Some(OrderError::InvalidStateTransition)
3997                ) {
3998                    // A non-fill event that fails to apply to an already-closed order is an
3999                    // expected venue race (e.g. a place reject then a stream cancel for the same
4000                    // order), not an anomaly. A dropped fill stays at warn even on a closed order,
4001                    // since it represents real, possibly lost, execution.
4002                    let already_closed = self
4003                        .cache
4004                        .borrow()
4005                        .order(&client_order_id)
4006                        .is_some_and(|o| o.is_closed());
4007
4008                    if already_closed && !matches!(event, OrderEventAny::Filled(_)) {
4009                        log::debug!("InvalidStateTrigger: {e}, did not apply {event}");
4010                    } else {
4011                        log::warn!("InvalidStateTrigger: {e}, did not apply {event}");
4012                    }
4013                    return None;
4014                }
4015
4016                if let Some(OrderError::DuplicateFill(trade_id)) = e.downcast_ref::<OrderError>() {
4017                    log::warn!(
4018                        "Duplicate fill rejected at order level: trade_id={trade_id}, did not apply {event}"
4019                    );
4020                    return None;
4021                }
4022
4023                if let Some(OrderError::DuplicateFillVoid(trade_id)) =
4024                    e.downcast_ref::<OrderError>()
4025                {
4026                    log::warn!(
4027                        "Duplicate fill void rejected at order level: trade_id={trade_id}, did not apply {event}"
4028                    );
4029                    return None;
4030                }
4031
4032                log::error!("Error applying event: {e}, did not apply {event}");
4033
4034                // Failed updates leave the order unchanged, so only terminal events need cleanup
4035                if matches!(
4036                    event,
4037                    OrderEventAny::Denied(_)
4038                        | OrderEventAny::Rejected(_)
4039                        | OrderEventAny::Canceled(_)
4040                        | OrderEventAny::Expired(_)
4041                ) {
4042                    log::warn!(
4043                        "Terminal event {event} failed to apply to {client_order_id}, forcing cleanup from own book"
4044                    );
4045                    self.cache
4046                        .borrow_mut()
4047                        .force_remove_from_own_order_book(&client_order_id);
4048                }
4049
4050                return None;
4051            }
4052        };
4053
4054        // The client's first status transition closes the dispatch window
4055        if !Self::has_submittable_status(&order) {
4056            self.orders_dispatched.borrow_mut().remove(&client_order_id);
4057        }
4058
4059        if self.config.manage_own_order_books && should_handle_own_book_order(&order) {
4060            let needs_own_book = {
4061                self.cache
4062                    .borrow()
4063                    .own_order_book(&order.instrument_id())
4064                    .is_none()
4065            };
4066
4067            if needs_own_book {
4068                self.cache.borrow_mut().update_own_order_book(&order);
4069            }
4070        }
4071
4072        if self.config.debug {
4073            log::debug!("{SEND}{EVT} {event}");
4074        }
4075
4076        if self.config.snapshot_orders {
4077            self.create_order_state_snapshot(&order);
4078        }
4079
4080        if send_portfolio_update {
4081            self.send_order_update_to_portfolio(event);
4082        }
4083
4084        Some(order)
4085    }
4086
4087    fn send_order_update_to_portfolio(&self, event: &OrderEventAny) {
4088        let is_wallet = event.account_id().is_some_and(|account_id| {
4089            self.cache
4090                .borrow()
4091                .account(&account_id)
4092                .is_some_and(|account| account.account_type() == AccountType::Wallet)
4093        });
4094        let send_to_portfolio = match event {
4095            OrderEventAny::Filled(fill) => self
4096                .cache
4097                .borrow()
4098                .account(&fill.account_id)
4099                .is_none_or(|account| !account.is_margin_account()),
4100            OrderEventAny::Accepted(_)
4101            | OrderEventAny::Canceled(_)
4102            | OrderEventAny::Expired(_)
4103            | OrderEventAny::Rejected(_)
4104            | OrderEventAny::Updated(_) => true,
4105            OrderEventAny::Submitted(_)
4106            | OrderEventAny::Triggered(_)
4107            | OrderEventAny::PendingUpdate(_)
4108            | OrderEventAny::PendingCancel(_)
4109            | OrderEventAny::ModifyRejected(_)
4110            | OrderEventAny::CancelRejected(_)
4111            | OrderEventAny::FillVoided(_) => is_wallet,
4112            _ => false,
4113        };
4114
4115        if send_to_portfolio {
4116            let portfolio_endpoint = MessagingSwitchboard::portfolio_update_order();
4117            msgbus::send_order_event(portfolio_endpoint, event.clone());
4118        }
4119    }
4120
4121    fn publish_order_event(&self, event: &OrderEventAny) {
4122        let topic = switchboard::get_event_order_topic(event.strategy_id());
4123        msgbus::publish_order_event(topic, event);
4124
4125        #[rustfmt::skip]
4126        let topic = match event {
4127            OrderEventAny::Submitted(_) => switchboard::get_order_submitted_topic(event.instrument_id()),
4128            OrderEventAny::Rejected(_) => switchboard::get_order_rejected_topic(event.instrument_id()),
4129            OrderEventAny::PendingUpdate(_) => switchboard::get_order_pending_update_topic(event.instrument_id()),
4130            OrderEventAny::PendingCancel(_) => switchboard::get_order_pending_cancel_topic(event.instrument_id()),
4131            OrderEventAny::ModifyRejected(_) => switchboard::get_order_modify_rejected_topic(event.instrument_id()),
4132            OrderEventAny::CancelRejected(_) => switchboard::get_order_cancel_rejected_topic(event.instrument_id()),
4133            OrderEventAny::Canceled(_) => switchboard::get_order_canceled_topic(event.instrument_id()),
4134            OrderEventAny::FillVoided(_) => switchboard::get_order_fill_voided_topic(event.instrument_id()),
4135            // Keep Filled out of this generic fanout: handle_order_fill publishes the instrument
4136            // topic, while leg fills stay on the strategy topic.
4137            _ => return,
4138        };
4139
4140        msgbus::publish_order_event(topic, event);
4141    }
4142
4143    fn publish_position_events(&self, events: Vec<PositionEvent>) {
4144        for event in events {
4145            let strategy_id = match &event {
4146                PositionEvent::PositionOpened(event) => event.strategy_id,
4147                PositionEvent::PositionChanged(event) => event.strategy_id,
4148                PositionEvent::PositionClosed(event) => event.strategy_id,
4149                PositionEvent::PositionAdjusted(event) => event.strategy_id,
4150            };
4151            let topic = switchboard::get_event_position_topic(strategy_id);
4152            msgbus::publish_position_event(topic, &event);
4153        }
4154    }
4155
4156    fn check_overfill(&self, order: &OrderAny, fill: &OrderFilled) -> anyhow::Result<()> {
4157        let potential_overfill = order.calculate_overfill(fill.last_qty);
4158
4159        if potential_overfill.is_positive() {
4160            if self.config.allow_overfills {
4161                log::warn!(
4162                    "Order overfill detected: {} potential_overfill={}, current_filled={}, last_qty={}, quantity={}",
4163                    order.client_order_id(),
4164                    potential_overfill,
4165                    order.filled_qty(),
4166                    fill.last_qty,
4167                    order.quantity()
4168                );
4169            } else {
4170                let msg = format!(
4171                    "Order overfill rejected: {} potential_overfill={}, current_filled={}, last_qty={}, quantity={}. \
4172                Set `allow_overfills=true` in ExecutionEngineConfig to allow overfills.",
4173                    order.client_order_id(),
4174                    potential_overfill,
4175                    order.filled_qty(),
4176                    fill.last_qty,
4177                    order.quantity()
4178                );
4179                log::warn!("{msg}");
4180                anyhow::bail!("{msg}");
4181            }
4182        }
4183
4184        Ok(())
4185    }
4186
4187    fn handle_order_fill(
4188        &mut self,
4189        order: &OrderAny,
4190        fill: OrderFilled,
4191        oms_type: OmsType,
4192        restatement: bool,
4193    ) -> Vec<PositionEvent> {
4194        let instrument =
4195            if let Some(instrument) = self.cache.borrow().instrument(&fill.instrument_id) {
4196                instrument.clone()
4197            } else {
4198                log::error!(
4199                    "Cannot handle order fill: no instrument found for {}, {fill}",
4200                    fill.instrument_id,
4201                );
4202                return Vec::new();
4203            };
4204
4205        let is_margin_account = {
4206            let cache = self.cache.borrow();
4207            let account = match cache.try_account(&fill.account_id) {
4208                Ok(account) => account,
4209                Err(e) => {
4210                    log::error!("Cannot handle order fill: {e}, {fill}");
4211                    return Vec::new();
4212                }
4213            };
4214
4215            account.is_margin_account()
4216        };
4217
4218        // Skip portfolio position updates for combo fills (spread instruments)
4219        // Combo fills are only used for order management, not portfolio updates.
4220        // A price restatement does not change quantity, so it is not a cash fill.
4221        if !instrument.is_spread() && is_margin_account && !restatement {
4222            let portfolio_endpoint = MessagingSwitchboard::portfolio_update_order();
4223            msgbus::send_order_event(portfolio_endpoint, OrderEventAny::Filled(fill.clone()));
4224        }
4225
4226        let (position, position_events) = if instrument.is_spread() {
4227            (None, Vec::new())
4228        } else if restatement {
4229            let position_events = self.restate_open_position_price(&fill);
4230            let position_id = fill.position_id;
4231            (
4232                position_id.and_then(|position_id| {
4233                    self.cache
4234                        .borrow()
4235                        .position(&position_id)
4236                        .map(|position| position.clone_without_events())
4237                }),
4238                position_events,
4239            )
4240        } else {
4241            let position_events = self.handle_position_update(&instrument, fill.clone(), oms_type);
4242            let position_id = fill.position_id.unwrap();
4243            (
4244                self.cache
4245                    .borrow()
4246                    .position(&position_id)
4247                    .map(|position| position.clone_without_events()),
4248                position_events,
4249            )
4250        };
4251
4252        if !position_events.is_empty() {
4253            self.index_external_position_reduction(order, &fill, oms_type, position.as_ref());
4254        }
4255
4256        // Handle contingent orders for both spread and non-spread instruments
4257        // For spread instruments, contingent orders work without position linkage
4258        if matches!(order.contingency_type(), Some(ContingencyType::Oto)) {
4259            // For non-spread instruments, link to position if available
4260            if !instrument.is_spread()
4261                && let Some(ref pos) = position
4262                && pos.is_open()
4263            {
4264                let position_id = pos.id;
4265
4266                for client_order_id in order.linked_order_ids().unwrap_or_default() {
4267                    // Take a scoped write borrow on the contingent's cell. The borrow drops at
4268                    // the end of `and_then` so the subsequent `add_position_id` on the cache is
4269                    // free to take `&mut Cache`.
4270                    let link = self.cache.borrow_mut().order_mut(client_order_id).and_then(
4271                        |mut contingent_order| {
4272                            if contingent_order.position_id().is_none() {
4273                                contingent_order.set_position_id(Some(position_id));
4274                                Some((
4275                                    contingent_order.instrument_id().venue,
4276                                    contingent_order.client_order_id(),
4277                                    contingent_order.strategy_id(),
4278                                ))
4279                            } else {
4280                                None
4281                            }
4282                        },
4283                    );
4284
4285                    if let Some((venue, contingent_id, strategy_id)) = link
4286                        && let Err(e) = self.cache.borrow_mut().add_position_id(
4287                            &position_id,
4288                            &venue,
4289                            &contingent_id,
4290                            &strategy_id,
4291                        )
4292                    {
4293                        log::error!("Failed to add position ID: {e}");
4294                    }
4295                }
4296            }
4297            // For spread instruments, contingent orders can still be triggered
4298            // but without position linkage (since no position is created for spreads)
4299        }
4300
4301        let topic = switchboard::get_order_filled_topic(fill.instrument_id);
4302        let event = OrderEventAny::Filled(fill);
4303        msgbus::publish_order_event(topic, &event);
4304
4305        position_events
4306    }
4307
4308    fn index_external_position_reduction(
4309        &self,
4310        order: &OrderAny,
4311        fill: &OrderFilled,
4312        oms_type: OmsType,
4313        position: Option<&Position>,
4314    ) {
4315        if oms_type == OmsType::Netting
4316            && order.is_reduce_only()
4317            && let Some(position) = position
4318            && position.strategy_id.is_external()
4319            && let Err(e) = self.cache.borrow_mut().add_position_id(
4320                &position.id,
4321                &fill.instrument_id.venue,
4322                &fill.client_order_id,
4323                &position.strategy_id,
4324            )
4325        {
4326            log::error!("Failed to index external position for reducing order: {e}");
4327        }
4328    }
4329
4330    fn prepare_order_fill_void_positions(
4331        &self,
4332        order: &OrderAny,
4333        event: &OrderFillVoided,
4334    ) -> anyhow::Result<Vec<CorrectedPosition>> {
4335        let source_event_id = order
4336            .events()
4337            .into_iter()
4338            .find_map(|order_event| match order_event {
4339                OrderEventAny::Filled(fill) if fill.trade_id == event.trade_id => {
4340                    Some(fill.event_id)
4341                }
4342                _ => None,
4343            })
4344            .ok_or_else(|| anyhow::anyhow!("fill {} is not in order history", event.trade_id))?;
4345
4346        let positions: Vec<Position> = {
4347            let cache = self.cache.borrow();
4348
4349            let strategy_id = event
4350                .position_id
4351                .and_then(|id| cache.position_ref(&id))
4352                .filter(|position| {
4353                    order.is_reduce_only()
4354                        && position.strategy_id.is_external()
4355                        && cache.oms_type(&position.id) == Some(OmsType::Netting)
4356                })
4357                .map_or(event.strategy_id, |position| position.strategy_id);
4358
4359            cache
4360                .positions(
4361                    None,
4362                    Some(&event.instrument_id),
4363                    Some(&strategy_id),
4364                    Some(&event.account_id),
4365                    None,
4366                )
4367                .into_iter()
4368                .map(|position| position.cloned())
4369                .collect()
4370        };
4371        let mut fragments = Vec::new();
4372
4373        for position in &positions {
4374            for replay_event in &position.replay_events {
4375                let PositionReplayEvent::Filled(fill) = replay_event else {
4376                    continue;
4377                };
4378
4379                if fill.client_order_id != event.client_order_id || fill.trade_id != event.trade_id
4380                {
4381                    continue;
4382                }
4383                let split_rank = if fill.event_id == source_event_id {
4384                    0
4385                } else if fill.causation_id == Some(source_event_id) {
4386                    1
4387                } else {
4388                    continue;
4389                };
4390                fragments.push((position.id, split_rank, fill.last_qty, fill.commission));
4391            }
4392        }
4393        anyhow::ensure!(
4394            !fragments.is_empty(),
4395            "no position fragments found for fill {}",
4396            event.trade_id
4397        );
4398        fragments.sort_by_key(|(_, split_rank, _, _)| *split_rank);
4399
4400        let mut allocations = IndexMap::<PositionId, (Quantity, Option<Money>)>::new();
4401        let mut remaining_qty = event.voided_qty;
4402        for (position_id, _, quantity, _) in fragments.iter().rev() {
4403            if remaining_qty.is_zero() {
4404                break;
4405            }
4406            let removed = remaining_qty.min(*quantity);
4407            allocations
4408                .entry(*position_id)
4409                .and_modify(|allocation| allocation.0 = allocation.0 + removed)
4410                .or_insert((removed, None));
4411            remaining_qty = remaining_qty - removed;
4412        }
4413        anyhow::ensure!(
4414            remaining_qty.is_zero(),
4415            "position fragments do not cover voided quantity for fill {}",
4416            event.trade_id
4417        );
4418
4419        if let Some(mut remaining_commission) = event.commission_voided {
4420            for (position_id, _, _, commission) in fragments.iter().rev() {
4421                if remaining_commission.is_zero() {
4422                    break;
4423                }
4424                let Some(commission) = commission else {
4425                    continue;
4426                };
4427                anyhow::ensure!(
4428                    commission.currency == remaining_commission.currency,
4429                    "position commission currency differs for fill {}",
4430                    event.trade_id
4431                );
4432                let magnitude = remaining_commission.abs().min(commission.abs());
4433
4434                let removed = if remaining_commission.is_negative() {
4435                    -magnitude
4436                } else {
4437                    magnitude
4438                };
4439
4440                allocations
4441                    .entry(*position_id)
4442                    .and_modify(|allocation| {
4443                        allocation.1 = Some(
4444                            allocation
4445                                .1
4446                                .map_or(removed, |commission| commission + removed),
4447                        );
4448                    })
4449                    .or_insert((Quantity::zero(event.voided_qty.precision), Some(removed)));
4450                remaining_commission = remaining_commission - removed;
4451            }
4452            anyhow::ensure!(
4453                remaining_commission.is_zero(),
4454                "position fragments do not cover voided commission for fill {}",
4455                event.trade_id
4456            );
4457        }
4458
4459        let mut corrected_positions = Vec::new();
4460
4461        for (position_id, (voided_qty, commission_voided)) in allocations {
4462            if voided_qty.is_zero() {
4463                anyhow::bail!(
4464                    "commission-only position correction requires authoritative reconciliation for fill {}",
4465                    event.trade_id
4466                );
4467            }
4468            let mut position = self
4469                .cache
4470                .borrow()
4471                .position_owned(&position_id)
4472                .ok_or_else(|| anyhow::anyhow!("position {position_id} is not cached"))?;
4473            let previous = position
4474                .fill_voids
4475                .iter()
4476                .rev()
4477                .find(|record| {
4478                    record.event.client_order_id == event.client_order_id
4479                        && record.event.trade_id == event.trade_id
4480                })
4481                .map(|record| (record.voided_qty, record.commission_voided));
4482            if previous == Some((voided_qty, commission_voided)) {
4483                continue;
4484            }
4485            let corrected_qty = previous.map_or(voided_qty, |(prior_qty, _)| {
4486                voided_qty.saturating_sub(prior_qty)
4487            });
4488
4489            // `events` holds the fills since the position was last flat, because `apply_fill`
4490            // clears it when reopening from flat. A NETTING flip splits one fill across the
4491            // closing and reopening cycles under the same trade, so compare quantities rather
4492            // than presence: the correction reaches an earlier cycle once it exceeds what the
4493            // current cycle originally held. Earlier corrections have already shrunk the
4494            // fragments in `events` while `voided_qty` stays cumulative, so add back what this
4495            // position already voided. Read this before `apply_fill_void`, whose rebuild
4496            // re-derives `events` and can move that boundary.
4497            let previously_voided = previous
4498                .map_or(Quantity::zero(position.size_precision), |(prior_qty, _)| {
4499                    prior_qty
4500                });
4501            let current_cycle_qty = position
4502                .events
4503                .iter()
4504                .filter(|fill| {
4505                    fill.client_order_id == event.client_order_id && fill.trade_id == event.trade_id
4506                })
4507                .fold(previously_voided, |total, fill| total + fill.last_qty);
4508            let absorbed_prior_cycles = voided_qty > current_cycle_qty;
4509            let closed_cycles_pnl =
4510                position.apply_fill_void(event.clone(), voided_qty, commission_voided)?;
4511            corrected_positions.push(CorrectedPosition {
4512                position,
4513                corrected_qty,
4514                absorbed_prior_cycles,
4515                closed_cycles_pnl,
4516            });
4517        }
4518        Ok(corrected_positions)
4519    }
4520
4521    fn create_fill_void_position_event(
4522        position: &Position,
4523        fill_voided: &OrderFillVoided,
4524        corrected_qty: Quantity,
4525    ) -> PositionEvent {
4526        let event_id = UUID4::new();
4527        let ts_init = fill_voided.ts_init;
4528
4529        if position.is_closed() {
4530            PositionEvent::PositionClosed(PositionClosed {
4531                trader_id: position.trader_id,
4532                strategy_id: position.strategy_id,
4533                instrument_id: position.instrument_id,
4534                position_id: position.id,
4535                account_id: position.account_id,
4536                opening_order_id: position.opening_order_id,
4537                closing_order_id: position.closing_order_id,
4538                entry: position.entry,
4539                side: position.side,
4540                signed_qty: position.signed_qty,
4541                quantity: position.quantity,
4542                peak_quantity: position.peak_qty,
4543                last_qty: corrected_qty,
4544                last_px: fill_voided.last_px,
4545                currency: position.quote_currency,
4546                avg_px_open: position.avg_px_open,
4547                avg_px_close: position.avg_px_close,
4548                realized_return: position.realized_return,
4549                realized_pnl: position.realized_pnl,
4550                unrealized_pnl: Money::zero(position.quote_currency),
4551                duration: position.duration_ns,
4552                event_id,
4553                ts_opened: position.ts_opened,
4554                ts_closed: position.ts_closed,
4555                ts_event: fill_voided.ts_event,
4556                ts_init,
4557            })
4558        } else {
4559            PositionEvent::PositionChanged(PositionChanged {
4560                trader_id: position.trader_id,
4561                strategy_id: position.strategy_id,
4562                instrument_id: position.instrument_id,
4563                position_id: position.id,
4564                account_id: position.account_id,
4565                opening_order_id: position.opening_order_id,
4566                entry: position.entry,
4567                side: position.side,
4568                signed_qty: position.signed_qty,
4569                quantity: position.quantity,
4570                peak_quantity: position.peak_qty,
4571                last_qty: corrected_qty,
4572                last_px: fill_voided.last_px,
4573                currency: position.quote_currency,
4574                avg_px_open: position.avg_px_open,
4575                avg_px_close: position.avg_px_close,
4576                realized_return: position.realized_return,
4577                realized_pnl: position.realized_pnl,
4578                unrealized_pnl: Money::zero(position.quote_currency),
4579                event_id,
4580                ts_opened: position.ts_opened,
4581                ts_event: fill_voided.ts_event,
4582                ts_init,
4583            })
4584        }
4585    }
4586
4587    /// Handle position creation or update for a fill.
4588    ///
4589    /// This function mirrors the Python `_handle_position_update` method.
4590    fn handle_position_update(
4591        &mut self,
4592        instrument: &InstrumentAny,
4593        fill: OrderFilled,
4594        oms_type: OmsType,
4595    ) -> Vec<PositionEvent> {
4596        enum Action {
4597            Open,
4598            Reopen,
4599            Flip(Box<Position>),
4600            Update,
4601        }
4602
4603        let position_id = if let Some(position_id) = fill.position_id {
4604            position_id
4605        } else {
4606            log::error!("Cannot handle position update: no position ID found for fill {fill}");
4607            return Vec::new();
4608        };
4609
4610        if self.skips_settled_position_change("Fill", fill.instrument_id, fill.trade_id) {
4611            return Vec::new();
4612        }
4613
4614        let action = {
4615            let cache = self.cache.borrow();
4616
4617            match cache.position(&position_id) {
4618                None => Action::Open,
4619                Some(position) if position.is_closed() => Action::Reopen,
4620                Some(position) if self.will_flip_position(&position, &fill) => {
4621                    Action::Flip(Box::new(position.clone()))
4622                }
4623                Some(_) => Action::Update,
4624            }
4625        };
4626
4627        match action {
4628            Action::Open => {
4629                if self.reject_reduce_only_position_open(&fill, oms_type) {
4630                    return Vec::new();
4631                }
4632
4633                // Copy these before `fill` moves into `open_position`
4634                let instrument_id = fill.instrument_id;
4635                let trade_id = fill.trade_id;
4636
4637                self.open_position(instrument, None, true, fill, oms_type)
4638                    .unwrap_or_else(|e| {
4639                        log::error!(
4640                            "Failed to open position {position_id} for instrument \
4641                             {instrument_id} from fill {trade_id}: {e:?}"
4642                        );
4643                        Vec::new()
4644                    })
4645            }
4646            Action::Reopen => {
4647                if self.reject_reduce_only_position_open(&fill, oms_type) {
4648                    return Vec::new();
4649                }
4650
4651                let instrument_id = fill.instrument_id;
4652                let trade_id = fill.trade_id;
4653
4654                self.open_position(instrument, Some(position_id), true, fill, oms_type)
4655                    .unwrap_or_else(|e| {
4656                        log::error!(
4657                            "Failed to reopen position {position_id} for instrument \
4658                             {instrument_id} from fill {trade_id}: {e:?}"
4659                        );
4660                        Vec::new()
4661                    })
4662            }
4663            Action::Flip(mut position) => {
4664                self.flip_position(instrument, &mut position, &fill, oms_type)
4665            }
4666            Action::Update => self
4667                .update_position_from_fill(position_id, &fill)
4668                .into_iter()
4669                .collect(),
4670        }
4671    }
4672
4673    fn reject_reduce_only_position_open(&self, fill: &OrderFilled, oms_type: OmsType) -> bool {
4674        let cache = self.cache.borrow();
4675        let Some(order) = cache.order_owned(&fill.client_order_id) else {
4676            return false;
4677        };
4678
4679        if !order.is_reduce_only() {
4680            return false;
4681        }
4682
4683        let positions_open = cache.positions_open(
4684            None,
4685            Some(&fill.instrument_id),
4686            None,
4687            Some(&fill.account_id),
4688            None,
4689        );
4690        let position_id = fill
4691            .position_id
4692            .map_or_else(|| "None".to_string(), |position_id| position_id.to_string());
4693        let matching_position_details = Self::position_details(
4694            positions_open
4695                .iter()
4696                .filter(|position| position.is_opposite_side(fill.order_side))
4697                .map(|position| &**position),
4698        );
4699        let open_position_details =
4700            Self::position_details(positions_open.iter().map(|position| &**position));
4701
4702        log::error!(
4703            "Cannot open {oms_type} position {position_id} from reduce-only fill {} for {}; \
4704             matching_reduce_positions=[{}], open_positions=[{}]",
4705            fill.trade_id,
4706            fill.instrument_id,
4707            matching_position_details,
4708            open_position_details
4709        );
4710
4711        true
4712    }
4713
4714    #[allow(
4715        clippy::needless_pass_by_value,
4716        reason = "takes the opening fill by value to seed the new position"
4717    )]
4718    fn open_position(
4719        &self,
4720        instrument: &InstrumentAny,
4721        prior_position_id: Option<PositionId>,
4722        archive_prior: bool,
4723        fill: OrderFilled,
4724        oms_type: OmsType,
4725    ) -> anyhow::Result<Vec<PositionEvent>> {
4726        if let Some(position_id) = prior_position_id {
4727            let prior_snapshot = {
4728                let cache = self.cache.borrow();
4729                let position = cache
4730                    .position(&position_id)
4731                    .ok_or_else(|| anyhow::anyhow!("position {position_id} is not cached"))?;
4732
4733                if archive_prior && position.has_replay_trade_id(fill.trade_id) {
4734                    log::warn!(
4735                        "Ignoring duplicate fill {} for closed position {}; no position reopened (side={:?}, qty={}, px={})",
4736                        fill.trade_id,
4737                        position.id,
4738                        fill.order_side,
4739                        fill.last_qty,
4740                        fill.last_px
4741                    );
4742                    return Ok(Vec::new());
4743                }
4744
4745                archive_prior.then(|| position.clone_for_snapshot())
4746            };
4747
4748            if let Some(position) = prior_snapshot {
4749                self.reopen_position(&position, oms_type)?;
4750            }
4751        }
4752
4753        let position = Position::new(instrument, fill.clone());
4754        let is_orderless_leg = self.is_leg_fill(&fill)
4755            && !self.cache.borrow().order_exists(&position.opening_order_id);
4756        if let Some(position_id) = prior_position_id {
4757            debug_assert_eq!(position_id, position.id);
4758
4759            self.cache.borrow_mut().replace_position(
4760                &position,
4761                oms_type,
4762                !is_orderless_leg,
4763                self.config.carry_replay_events_on_reopen
4764                    || (oms_type == OmsType::Hedging && position_id.is_virtual()),
4765            )?;
4766        } else if is_orderless_leg {
4767            self.cache
4768                .borrow_mut()
4769                .add_position_without_order(&position, oms_type)?;
4770        } else {
4771            self.cache.borrow_mut().add_position(&position, oms_type)?;
4772        }
4773
4774        let (position, snapshot_position) = {
4775            let cache = self.cache.borrow();
4776            let position = cache
4777                .position(&fill.position_id.expect("Opening fill has no position ID"))
4778                .expect("Opened position is no longer cached");
4779            (
4780                position.clone_for_snapshot(),
4781                self.config.snapshot_positions.then(|| position.clone()),
4782            )
4783        };
4784
4785        if let Some(snapshot_position) = snapshot_position {
4786            self.create_position_state_snapshot(&snapshot_position, true);
4787        }
4788
4789        let ts_init = self.clock.borrow().timestamp_ns();
4790        let event = PositionOpened::create(&position, &fill, UUID4::new(), ts_init);
4791
4792        Ok(vec![PositionEvent::PositionOpened(event)])
4793    }
4794
4795    fn reopen_position(&self, position: &Position, oms_type: OmsType) -> anyhow::Result<()> {
4796        if oms_type == OmsType::Netting {
4797            if position.is_open() {
4798                anyhow::bail!(
4799                    "Cannot reopen position {} (oms_type=NETTING): reopening is only valid for closed positions in NETTING mode",
4800                    position.id
4801                );
4802            }
4803        } else {
4804            // HEDGING mode
4805            log::warn!(
4806                "Received fill for closed position {} in HEDGING mode; archiving closed cycle and creating new position",
4807                position.id
4808            );
4809        }
4810
4811        // Snapshot the closed cycle before its ID is reused: `add_position` replaces the
4812        // cached position, and realized PnL totals read closed cycles from the snapshots
4813        self.snapshot_position(position)?;
4814
4815        Ok(())
4816    }
4817
4818    /// Archives the closed `position` and anchors the frame when an anchorer is installed.
4819    ///
4820    /// An installed anchorer needs the encoded frame, so this takes the eager path. Without one
4821    /// the cache defers the encode unless a backing database has to persist the frame.
4822    fn snapshot_position(&self, position: &Position) -> anyhow::Result<()> {
4823        let mut cache = self.cache.borrow_mut();
4824
4825        let Some(anchorer) = &self.snapshot_anchorer else {
4826            return cache.snapshot_position(position);
4827        };
4828
4829        let snapshot_ref = cache.snapshot_position_encoded(position)?;
4830        drop(cache);
4831
4832        if let Err(e) = anchorer(snapshot_ref) {
4833            log::warn!("Failed to record cache snapshot anchor: {e}");
4834        }
4835
4836        Ok(())
4837    }
4838
4839    fn update_position(
4840        &self,
4841        position: &mut Position,
4842        fill: &OrderFilled,
4843    ) -> Option<PositionEvent> {
4844        // Apply the fill to the position
4845        position.apply(fill);
4846
4847        // Check if position is closed after applying the fill
4848        let is_closed = position.is_closed();
4849
4850        // Update position in cache - this should handle the closed state tracking
4851        if let Err(e) = self.cache.borrow_mut().update_position(position) {
4852            log::error!("Failed to update position: {e:?}");
4853            return None;
4854        }
4855
4856        // Verify cache state after update
4857        let cache = self.cache.borrow();
4858
4859        drop(cache);
4860
4861        // Create position state snapshot if enabled
4862        if self.config.snapshot_positions {
4863            self.create_position_state_snapshot(position, false);
4864        }
4865
4866        let ts_init = self.clock.borrow().timestamp_ns();
4867
4868        if is_closed {
4869            let event = PositionClosed::create(position, fill, UUID4::new(), ts_init);
4870            Some(PositionEvent::PositionClosed(event))
4871        } else {
4872            let event = PositionChanged::create(position, fill, UUID4::new(), ts_init);
4873            Some(PositionEvent::PositionChanged(event))
4874        }
4875    }
4876
4877    fn restate_open_position_price(&self, fill: &OrderFilled) -> Vec<PositionEvent> {
4878        let Some(position_id) = fill.position_id else {
4879            return Vec::new();
4880        };
4881
4882        let applicable = self
4883            .cache
4884            .borrow()
4885            .position(&position_id)
4886            .is_some_and(|position| position_accepts_price_restatement(&position, fill));
4887
4888        if !applicable {
4889            if let Some(position) = self.cache.borrow().position(&position_id) {
4890                log::warn!(
4891                    "Price restatement for {} trade_id={} was not applied to position {}: \
4892                     the trade is not an opening fill of the current cycle with a matching quantity",
4893                    fill.client_order_id,
4894                    fill.trade_id,
4895                    position.id,
4896                );
4897            }
4898
4899            return Vec::new();
4900        }
4901
4902        self.update_position_from_fill(position_id, fill)
4903            .into_iter()
4904            .collect()
4905    }
4906
4907    fn update_position_from_fill(
4908        &self,
4909        position_id: PositionId,
4910        fill: &OrderFilled,
4911    ) -> Option<PositionEvent> {
4912        let position = match self
4913            .cache
4914            .borrow_mut()
4915            .update_position_from_fill(position_id, fill)
4916        {
4917            Ok(position) => position,
4918            Err(e) => {
4919                log::error!("Failed to update position: {e:?}");
4920                return None;
4921            }
4922        };
4923
4924        if self.config.snapshot_positions {
4925            let position = self
4926                .cache
4927                .borrow()
4928                .position_owned(&position_id)
4929                .expect("Updated position is no longer cached");
4930            self.create_position_state_snapshot(&position, false);
4931        }
4932
4933        let ts_init = self.clock.borrow().timestamp_ns();
4934
4935        if position.is_closed() {
4936            let event = PositionClosed::create(&position, fill, UUID4::new(), ts_init);
4937            Some(PositionEvent::PositionClosed(event))
4938        } else {
4939            let event = PositionChanged::create(&position, fill, UUID4::new(), ts_init);
4940            Some(PositionEvent::PositionChanged(event))
4941        }
4942    }
4943
4944    fn settle_instrument_close(&self, close: &InstrumentClose) {
4945        let instrument_id = close.instrument_id;
4946
4947        let settled =
4948            match settlement::settle_instrument_close(&mut self.cache.borrow_mut(), *close) {
4949                Ok(settled) => settled,
4950                Err(e) => {
4951                    log::error!("Cannot settle {instrument_id}: {e}");
4952                    return;
4953                }
4954            };
4955
4956        let Some(close) = settlement::settlement_close(&self.cache.borrow(), &instrument_id) else {
4957            return;
4958        };
4959
4960        let mut position_events = Vec::with_capacity(settled.len());
4961
4962        for (position, last_qty) in settled {
4963            if self.config.snapshot_positions {
4964                let snapshot = self.cache.borrow().position_owned(&position.id);
4965                if let Some(snapshot) = snapshot {
4966                    self.create_position_state_snapshot(&snapshot, false);
4967                }
4968            }
4969
4970            log::info!(
4971                "Settled position {} at {} from contract expiration, realized_pnl={:?}",
4972                position.id,
4973                close.close_price,
4974                position.realized_pnl,
4975            );
4976
4977            let ts_init = self.clock.borrow().timestamp_ns();
4978            let event = PositionClosed::create_from_instrument_close(
4979                &position,
4980                &close,
4981                last_qty,
4982                UUID4::new(),
4983                ts_init,
4984            );
4985            position_events.push(PositionEvent::PositionClosed(event));
4986        }
4987
4988        self.publish_position_events(position_events);
4989    }
4990
4991    // Only venues this engine settles count, so a close cached without that subscription,
4992    // such as in a backtest, leaves fills applying to positions.
4993    fn skips_settled_position_change(
4994        &self,
4995        kind: &str,
4996        instrument_id: InstrumentId,
4997        trade_id: TradeId,
4998    ) -> bool {
4999        if !self.instrument_close_venues.contains(&instrument_id.venue) {
5000            return false;
5001        }
5002
5003        let is_settled =
5004            settlement::settlement_close(&self.cache.borrow(), &instrument_id).is_some();
5005
5006        if is_settled {
5007            log::warn!(
5008                "{kind} {trade_id} for {instrument_id} arrived after contract settlement; positions unchanged"
5009            );
5010        }
5011
5012        is_settled
5013    }
5014
5015    fn will_flip_position(&self, position: &Position, fill: &OrderFilled) -> bool {
5016        position.is_opposite_side(fill.order_side) && (fill.last_qty > position.quantity)
5017    }
5018
5019    fn position_signed_decimal_qty(position: &Position) -> Decimal {
5020        match position.side {
5021            PositionSide::Long => position.quantity.as_decimal(),
5022            PositionSide::Short => -position.quantity.as_decimal(),
5023            _ => Decimal::ZERO,
5024        }
5025    }
5026
5027    fn position_details<'a>(positions: impl IntoIterator<Item = &'a Position>) -> String {
5028        positions
5029            .into_iter()
5030            .map(|position| {
5031                format!(
5032                    "{} strategy_id={} signed_qty={}",
5033                    position.id,
5034                    position.strategy_id,
5035                    Self::position_signed_decimal_qty(position)
5036                )
5037            })
5038            .collect::<Vec<_>>()
5039            .join(", ")
5040    }
5041
5042    fn flip_position(
5043        &mut self,
5044        instrument: &InstrumentAny,
5045        position: &mut Position,
5046        fill: &OrderFilled,
5047        oms_type: OmsType,
5048    ) -> Vec<PositionEvent> {
5049        let mut position_events = Vec::new();
5050
5051        if fill.commission.is_none() {
5052            log::warn!(
5053                "Commission is not available for position flip, splitting with no commission"
5054            );
5055        }
5056
5057        let position_id_flip = if oms_type == OmsType::Hedging
5058            && let Some(position_id) = fill.position_id
5059            && position_id.is_virtual()
5060        {
5061            // Generate new position ID for flipped virtual position (Hedging OMS only)
5062            Some(self.pos_id_generator.generate(fill.strategy_id, true))
5063        } else {
5064            // Default: use the same position ID as the fill (Python behavior)
5065            fill.position_id
5066        };
5067
5068        let (fill_split1, fill_split2) = fill
5069            .split_for_position_flip(position.quantity, position_id_flip, UUID4::new())
5070            .expect("Invalid position flip split");
5071
5072        if let Some(position_event) = self.update_position(position, &fill_split1) {
5073            position_events.push(position_event);
5074        }
5075
5076        // Snapshot closed position before reusing ID (NETTING mode)
5077        if oms_type == OmsType::Netting
5078            && let Err(e) = self.snapshot_position(position)
5079        {
5080            log::warn!("Failed to snapshot position during flip: {e:?}");
5081        }
5082
5083        if oms_type == OmsType::Hedging
5084            && let Some(position_id) = fill.position_id
5085            && position_id.is_virtual()
5086        {
5087            log::warn!("Closing position {fill_split1}");
5088            log::warn!("Flipping position {fill_split2}");
5089        }
5090
5091        // Open flipped position
5092        let prior_position_id =
5093            (fill_split2.position_id == Some(position.id)).then_some(position.id);
5094
5095        match self.open_position(instrument, prior_position_id, false, fill_split2, oms_type) {
5096            Ok(opened_events) => position_events.extend(opened_events),
5097            Err(e) => log::error!("Failed to open flipped position: {e:?}"),
5098        }
5099
5100        position_events
5101    }
5102
5103    /// Sets the internal position ID generator counts based on existing cached positions.
5104    pub fn set_position_id_counts(&mut self) {
5105        let cache = self.cache.borrow();
5106        let positions = cache.positions(None, None, None, None, None);
5107
5108        // Count positions per instrument_id using a HashMap
5109        let mut counts: HashMap<StrategyId, usize> = HashMap::new();
5110
5111        for position in positions {
5112            *counts.entry(position.strategy_id).or_insert(0) += 1;
5113        }
5114
5115        self.pos_id_generator.reset();
5116
5117        for (strategy_id, count) in counts {
5118            self.pos_id_generator.set_count(count, strategy_id);
5119            log::info!("Set PositionId count for {strategy_id} to {count}");
5120        }
5121    }
5122
5123    fn deny_order(&self, order: &OrderAny, reason: &str) {
5124        let denied = OrderDenied::new(
5125            order.trader_id(),
5126            order.strategy_id(),
5127            order.instrument_id(),
5128            order.client_order_id(),
5129            reason.into(),
5130            UUID4::new(),
5131            self.clock.borrow().timestamp_ns(),
5132            self.clock.borrow().timestamp_ns(),
5133        );
5134
5135        let event = OrderEventAny::Denied(denied);
5136        let order = match self.cache.borrow_mut().update_order(&event) {
5137            Ok(order) => order,
5138            Err(e) => {
5139                log::error!("Failed to apply denied event to order: {e}");
5140                return;
5141            }
5142        };
5143
5144        let topic = switchboard::get_event_order_topic(order.strategy_id());
5145        msgbus::publish_order_event(topic, &event);
5146
5147        if self.config.snapshot_orders {
5148            self.create_order_state_snapshot(&order);
5149        }
5150    }
5151
5152    fn get_or_init_own_order_book(&self, instrument_id: &InstrumentId) -> RefMut<'_, OwnOrderBook> {
5153        let mut cache = self.cache.borrow_mut();
5154        if cache.own_order_book_mut(instrument_id).is_none() {
5155            let own_book = OwnOrderBook::new(*instrument_id);
5156            cache
5157                .add_own_order_book(own_book)
5158                .expect("adding an own order book cannot fail");
5159        }
5160
5161        RefMut::map(cache, |c| {
5162            c.own_order_book_mut(instrument_id)
5163                .expect("own order book was ensured above")
5164        })
5165    }
5166}
5167
5168enum SubmissionValidationResult {
5169    Valid,
5170    StaleOrder {
5171        client_order_id: ClientOrderId,
5172        status: OrderStatus,
5173    },
5174    Dispatched {
5175        client_order_id: ClientOrderId,
5176    },
5177    Deny(OrderDeniedReason),
5178}
5179
5180#[cfg(test)]
5181mod tests {
5182    use nautilus_common::{
5183        clock::VirtualClock,
5184        msgbus::{MessageBus, set_message_bus},
5185    };
5186    use nautilus_model::{
5187        accounts::CashAccount,
5188        enums::{LiquiditySide, OrderSide, OrderType, PositionSide},
5189        events::{
5190            OrderFillVoided,
5191            order::spec::{OrderCanceledSpec, OrderFillVoidedSpec, OrderFilledSpec},
5192        },
5193        identifiers::{AccountId, ClientOrderId, TradeId, VenueOrderId},
5194        instruments::{InstrumentAny, stubs::audusd_sim},
5195        orders::{builder::OrderTestBuilder, stubs::TestOrderEventStubs},
5196        position::PositionFillVoid,
5197        types::Price,
5198    };
5199    use rstest::*;
5200
5201    use super::*;
5202
5203    #[rstest]
5204    fn netting_positions_open_for_report_scopes_positions_by_account() {
5205        let instrument = InstrumentAny::CurrencyPair(audusd_sim());
5206        let account1_id = AccountId::from("SIM-001");
5207        let account2_id = AccountId::from("SIM-002");
5208        let position1 = position_for_account(
5209            &instrument,
5210            account1_id,
5211            StrategyId::from("S-001"),
5212            PositionId::from("P-ACC-1"),
5213            OrderSide::Buy,
5214            Quantity::from(1_000),
5215        );
5216        let position2 = position_for_account(
5217            &instrument,
5218            account2_id,
5219            StrategyId::from("S-002"),
5220            PositionId::from("P-ACC-2"),
5221            OrderSide::Buy,
5222            Quantity::from(2_000),
5223        );
5224        let mut cache = Cache::default();
5225        cache.add_position(&position1, OmsType::Netting).unwrap();
5226        cache.add_position(&position2, OmsType::Netting).unwrap();
5227
5228        let report = PositionStatusReport::new(
5229            account1_id,
5230            instrument.id(),
5231            PositionSide::Long,
5232            Quantity::from(1_000),
5233            UnixNanos::from(1_000_000),
5234            UnixNanos::from(1_000_000),
5235            None,
5236            None,
5237            None,
5238        );
5239
5240        let positions_open = ExecutionEngine::netting_positions_open_for_report(&cache, &report);
5241        let signed_qty: Decimal = positions_open
5242            .iter()
5243            .map(|position| ExecutionEngine::position_signed_decimal_qty(position))
5244            .sum();
5245
5246        assert_eq!(positions_open.len(), 1);
5247        assert_eq!(positions_open[0].id, position1.id);
5248        assert_eq!(signed_qty, Decimal::from(1_000));
5249    }
5250
5251    #[rstest]
5252    fn netting_split_position_ownership_message_reports_only_split_ownership() {
5253        let instrument = InstrumentAny::CurrencyPair(audusd_sim());
5254        let account_id = AccountId::from("SIM-001");
5255        let external_position = position_for_account(
5256            &instrument,
5257            account_id,
5258            StrategyId::from("EXTERNAL"),
5259            PositionId::from("P-EXTERNAL"),
5260            OrderSide::Buy,
5261            Quantity::from(1_000),
5262        );
5263        let strategy_position = position_for_account(
5264            &instrument,
5265            account_id,
5266            StrategyId::from("S-001"),
5267            PositionId::from("P-STRATEGY"),
5268            OrderSide::Buy,
5269            Quantity::from(500),
5270        );
5271        let same_strategy_position = position_for_account(
5272            &instrument,
5273            account_id,
5274            StrategyId::from("EXTERNAL"),
5275            PositionId::from("P-EXTERNAL-2"),
5276            OrderSide::Buy,
5277            Quantity::from(250),
5278        );
5279        let report = PositionStatusReport::new(
5280            account_id,
5281            instrument.id(),
5282            PositionSide::Long,
5283            Quantity::from(1_500),
5284            UnixNanos::from(1_000_000),
5285            UnixNanos::from(1_000_000),
5286            None,
5287            None,
5288            None,
5289        );
5290
5291        let message = ExecutionEngine::netting_split_position_ownership_message(
5292            &report,
5293            &[&external_position, &strategy_position],
5294        )
5295        .expect("split ownership should produce a warning message");
5296
5297        assert!(message.contains("account_id=SIM-001"));
5298        assert!(message.contains(&format!("instrument_id={}", instrument.id())));
5299        assert!(message.contains("EXTERNAL"));
5300        assert!(message.contains("S-001"));
5301        assert!(message.contains("P-EXTERNAL"));
5302        assert!(message.contains("P-STRATEGY"));
5303        assert!(message.contains("signed_qty=1000"));
5304        assert!(message.contains("signed_qty=500"));
5305        assert!(
5306            ExecutionEngine::netting_split_position_ownership_message(
5307                &report,
5308                &[&external_position, &same_strategy_position],
5309            )
5310            .is_none()
5311        );
5312    }
5313
5314    #[rstest]
5315    fn materialize_external_order_rejects_venue_id_owned_by_another_order() {
5316        let cache = Rc::new(RefCell::new(Cache::default()));
5317        let venue_order_id = VenueOrderId::from("V-SHARED");
5318        let owner_id = ClientOrderId::from("O-OWNER");
5319        cache
5320            .borrow_mut()
5321            .add_venue_order_id(&owner_id, &venue_order_id, false)
5322            .unwrap();
5323        let engine = ExecutionEngine::new(
5324            Rc::new(RefCell::new(VirtualClock::new())),
5325            Rc::clone(&cache),
5326            None,
5327        );
5328        let instrument = InstrumentAny::CurrencyPair(audusd_sim());
5329        let claimant_id = ClientOrderId::from("O-CLAIMANT");
5330        let order = OrderTestBuilder::new(OrderType::Limit)
5331            .instrument_id(instrument.id())
5332            .client_order_id(claimant_id)
5333            .side(OrderSide::Buy)
5334            .quantity(Quantity::from(100_000))
5335            .price(Price::from("1.00000"))
5336            .build();
5337        let OrderEventAny::Initialized(initialized) = order.last_event().clone() else {
5338            panic!("Expected initialized order");
5339        };
5340
5341        let result = engine.materialize_external_order(
5342            initialized,
5343            claimant_id,
5344            venue_order_id,
5345            instrument.id(),
5346            order.strategy_id(),
5347            UnixNanos::default(),
5348            None,
5349            None,
5350        );
5351
5352        assert!(result.is_none());
5353        assert!(!cache.borrow().order_exists(&claimant_id));
5354        assert_eq!(
5355            cache.borrow().client_order_id(&venue_order_id),
5356            Some(&owner_id)
5357        );
5358        assert_eq!(cache.borrow().venue_order_id(&claimant_id), None);
5359    }
5360
5361    #[rstest]
5362    fn carry_replay_reopen_transfers_history_allocations() {
5363        let instrument = InstrumentAny::CurrencyPair(audusd_sim());
5364        let position_id = PositionId::from("P-REOPEN-TRANSFER");
5365        let opening = position_fill(
5366            &instrument,
5367            position_id,
5368            "O-OPEN",
5369            "T-OPEN",
5370            OrderSide::Buy,
5371            10,
5372            1,
5373        );
5374        let closing = position_fill(
5375            &instrument,
5376            position_id,
5377            "O-CLOSE",
5378            "T-CLOSE",
5379            OrderSide::Sell,
5380            10,
5381            2,
5382        );
5383        let reopening = position_fill(
5384            &instrument,
5385            position_id,
5386            "O-REOPEN",
5387            "T-REOPEN",
5388            OrderSide::Buy,
5389            10,
5390            3,
5391        );
5392        let mut position = Position::new(&instrument, opening.clone());
5393        position.apply(&closing);
5394
5395        let fill_voided = OrderFillVoided::new(
5396            opening.trader_id,
5397            opening.strategy_id,
5398            opening.instrument_id,
5399            opening.client_order_id,
5400            opening.venue_order_id,
5401            opening.account_id,
5402            "C-TRANSFER".into(),
5403            opening.trade_id,
5404            Quantity::from(1),
5405            None,
5406            opening.order_side,
5407            opening.order_type,
5408            opening.last_px,
5409            opening.currency,
5410            opening.liquidity_side,
5411            opening.position_id,
5412            None,
5413            None,
5414            UUID4::new(),
5415            UnixNanos::from(4),
5416            UnixNanos::from(4),
5417            false,
5418            false,
5419        );
5420        position.fill_voids.push(PositionFillVoid {
5421            event: fill_voided,
5422            voided_qty: Quantity::from(1),
5423            commission_voided: None,
5424        });
5425
5426        let cache = Rc::new(RefCell::new(Cache::default()));
5427        cache
5428            .borrow_mut()
5429            .add_position_without_order(&position, OmsType::Netting)
5430            .unwrap();
5431        {
5432            let mut cached = cache.borrow_mut();
5433            let mut position = cached.position_mut(&position_id).unwrap();
5434            position.replay_events.reserve(8);
5435            position.fill_voids.reserve(4);
5436        }
5437        let (replay_ptr, void_ptr, replay_len, void_len, replay_before, voids_before) = {
5438            let cache = cache.borrow();
5439            let position = cache.position(&position_id).unwrap();
5440            (
5441                position.replay_events.as_ptr(),
5442                position.fill_voids.as_ptr(),
5443                position.replay_events.len(),
5444                position.fill_voids.len(),
5445                serde_json::to_value(&position.replay_events).unwrap(),
5446                serde_json::to_value(&position.fill_voids).unwrap(),
5447            )
5448        };
5449        let config = ExecutionEngineConfig::builder()
5450            .carry_replay_events_on_reopen(true)
5451            .build()
5452            .unwrap();
5453        let mut engine = ExecutionEngine::new(
5454            Rc::new(RefCell::new(VirtualClock::new())),
5455            Rc::clone(&cache),
5456            Some(config),
5457        );
5458
5459        let events = engine.handle_position_update(&instrument, reopening, OmsType::Netting);
5460
5461        assert_eq!(events.len(), 1);
5462        let cache = cache.borrow();
5463        let position = cache.position(&position_id).unwrap();
5464        assert_eq!(position.replay_events.as_ptr(), replay_ptr);
5465        assert_eq!(position.fill_voids.as_ptr(), void_ptr);
5466        assert_eq!(position.replay_events.len(), replay_len + 1);
5467        assert_eq!(position.fill_voids.len(), void_len);
5468        assert_eq!(
5469            serde_json::to_value(&position.replay_events[..replay_len]).unwrap(),
5470            replay_before,
5471        );
5472        assert_eq!(
5473            serde_json::to_value(&position.fill_voids).unwrap(),
5474            voids_before
5475        );
5476        assert!(matches!(
5477            &position.replay_events[0],
5478            PositionReplayEvent::Filled(fill) if fill.event_id == opening.event_id
5479        ));
5480    }
5481
5482    #[rstest]
5483    fn position_opened_uses_state_captured_before_snapshot_subscriber() {
5484        let instrument = InstrumentAny::CurrencyPair(audusd_sim());
5485        let position_id = PositionId::from("P-SNAPSHOT-SUBSCRIBER");
5486        let fill = position_fill(
5487            &instrument,
5488            position_id,
5489            "O-SNAPSHOT-SUBSCRIBER",
5490            "T-SNAPSHOT-SUBSCRIBER",
5491            OrderSide::Buy,
5492            10,
5493            1,
5494        );
5495        let cache = Rc::new(RefCell::new(Cache::default()));
5496        let config = ExecutionEngineConfig::builder()
5497            .snapshot_positions(true)
5498            .build()
5499            .unwrap();
5500        let engine = ExecutionEngine::new(
5501            Rc::new(RefCell::new(VirtualClock::new())),
5502            Rc::clone(&cache),
5503            Some(config),
5504        );
5505        set_message_bus(Rc::new(RefCell::new(MessageBus::default())));
5506        let subscriber_cache = Rc::clone(&cache);
5507        let topic = switchboard::get_snapshot_position_topic(position_id);
5508        msgbus::subscribe_any(
5509            topic.as_str().into(),
5510            TypedHandler::from_typed::<PositionStateSnapshot, _>(move |_| {
5511                subscriber_cache
5512                    .borrow_mut()
5513                    .position_mut(&position_id)
5514                    .unwrap()
5515                    .quantity = Quantity::from(99);
5516            }),
5517            None,
5518        );
5519
5520        let events = engine
5521            .open_position(&instrument, None, true, fill.clone(), OmsType::Netting)
5522            .unwrap();
5523
5524        assert_eq!(
5525            cache.borrow().position(&position_id).unwrap().quantity,
5526            Quantity::from(99)
5527        );
5528        let [PositionEvent::PositionOpened(opened)] = events.as_slice() else {
5529            panic!("Expected one position-opened event");
5530        };
5531        assert_eq!(opened.position_id, position_id);
5532        assert_eq!(opened.opening_order_id, fill.client_order_id);
5533        assert_eq!(opened.quantity, fill.last_qty);
5534        assert_eq!(opened.last_px, fill.last_px);
5535    }
5536
5537    fn position_fill(
5538        instrument: &InstrumentAny,
5539        position_id: PositionId,
5540        order_id: &str,
5541        trade_id: &str,
5542        order_side: OrderSide,
5543        quantity: u32,
5544        ts_event: u64,
5545    ) -> OrderFilled {
5546        OrderFilledSpec::builder()
5547            .instrument_id(instrument.id())
5548            .client_order_id(ClientOrderId::from(order_id))
5549            .venue_order_id(VenueOrderId::from(order_id))
5550            .trade_id(TradeId::from(trade_id))
5551            .order_side(order_side)
5552            .last_qty(Quantity::from(quantity))
5553            .last_px(Price::from("1.00000"))
5554            .currency(instrument.quote_currency())
5555            .liquidity_side(LiquiditySide::Maker)
5556            .position_id(position_id)
5557            .ts_event(UnixNanos::from(ts_event))
5558            .build()
5559    }
5560
5561    fn position_for_account(
5562        instrument: &InstrumentAny,
5563        account_id: AccountId,
5564        strategy_id: StrategyId,
5565        position_id: PositionId,
5566        order_side: OrderSide,
5567        quantity: Quantity,
5568    ) -> Position {
5569        let client_order_id = ClientOrderId::from(format!("O-{position_id}"));
5570        let fill = OrderFilledSpec::builder()
5571            .strategy_id(strategy_id)
5572            .instrument_id(instrument.id())
5573            .client_order_id(client_order_id)
5574            .venue_order_id(VenueOrderId::from(format!("V-{position_id}")))
5575            .account_id(account_id)
5576            .trade_id(TradeId::new(format!("T-{position_id}")))
5577            .order_side(order_side)
5578            .last_qty(quantity)
5579            .last_px(Price::from("1.0"))
5580            .currency(instrument.quote_currency())
5581            .liquidity_side(LiquiditySide::Maker)
5582            .position_id(position_id)
5583            .commission(Money::from("2 USD"))
5584            .build();
5585
5586        Position::new(instrument, fill)
5587    }
5588
5589    #[rstest]
5590    fn declined_fill_and_void_publish_negative_acknowledgment() {
5591        let (mut engine, instrument, nacks) = nack_engine();
5592        let overfill = nack_order_fill(&instrument, "T-OVERFILL", Quantity::from(200));
5593        let valid = nack_order_fill(&instrument, "T-VALID", Quantity::from(50));
5594        let zero_void = nack_order_fill_void(&valid, Quantity::from(0));
5595
5596        engine.process(&OrderEventAny::Filled(overfill.clone()));
5597        engine.process(&OrderEventAny::Filled(valid.clone()));
5598        // A declined reconciliation projection is not an enqueued client event, so no NACK
5599        engine.project_reconciliation_fill(&valid);
5600        engine.process(&OrderEventAny::FillVoided(zero_void.clone()));
5601
5602        assert_eq!(
5603            *nacks.borrow(),
5604            vec![
5605                OrderEventAny::Filled(overfill),
5606                OrderEventAny::FillVoided(zero_void),
5607            ]
5608        );
5609    }
5610
5611    #[derive(Debug, Clone, Copy)]
5612    enum DeclineCase {
5613        ZeroQuantityFill,
5614        DuplicateFill,
5615        DuplicateVoid,
5616        UnknownOrderFill,
5617        UnknownOrderCancel,
5618    }
5619
5620    #[rstest]
5621    #[case::zero_quantity_fill(DeclineCase::ZeroQuantityFill)]
5622    #[case::duplicate_fill(DeclineCase::DuplicateFill)]
5623    #[case::duplicate_void(DeclineCase::DuplicateVoid)]
5624    #[case::unknown_order_fill(DeclineCase::UnknownOrderFill)]
5625    #[case::unknown_order_cancel(DeclineCase::UnknownOrderCancel)]
5626    fn declined_order_event_negative_acknowledgment(#[case] case: DeclineCase) {
5627        let (mut engine, instrument, nacks) = nack_engine();
5628        let fill = nack_order_fill(&instrument, "T-CASE", Quantity::from(50));
5629
5630        let (events, expected) = match case {
5631            DeclineCase::ZeroQuantityFill => {
5632                let zero = OrderEventAny::Filled(nack_order_fill(
5633                    &instrument,
5634                    "T-ZERO",
5635                    Quantity::from(0),
5636                ));
5637                (vec![zero.clone()], vec![zero])
5638            }
5639            DeclineCase::DuplicateFill => {
5640                let filled = OrderEventAny::Filled(fill);
5641                (vec![filled.clone(), filled.clone()], vec![filled])
5642            }
5643            DeclineCase::DuplicateVoid => {
5644                let voided = OrderEventAny::FillVoided(nack_order_fill_void(&fill, fill.last_qty));
5645                (
5646                    vec![OrderEventAny::Filled(fill), voided.clone(), voided.clone()],
5647                    vec![voided],
5648                )
5649            }
5650            DeclineCase::UnknownOrderFill => {
5651                let mut unknown = fill;
5652                unknown.client_order_id = ClientOrderId::from("O-UNKNOWN");
5653                unknown.venue_order_id = VenueOrderId::from("V-UNKNOWN");
5654                let filled = OrderEventAny::Filled(unknown);
5655                (vec![filled.clone()], vec![filled])
5656            }
5657            // Only fills and voids are awaited by clients, so other declined events never NACK
5658            DeclineCase::UnknownOrderCancel => {
5659                let canceled = OrderCanceledSpec::builder()
5660                    .instrument_id(instrument.id())
5661                    .client_order_id(ClientOrderId::from("O-UNKNOWN"))
5662                    .venue_order_id(VenueOrderId::from("V-UNKNOWN"))
5663                    .account_id(fill.account_id)
5664                    .build();
5665                (vec![OrderEventAny::Canceled(canceled)], Vec::new())
5666            }
5667        };
5668
5669        for event in &events {
5670            engine.process(event);
5671        }
5672
5673        assert_eq!(*nacks.borrow(), expected);
5674    }
5675
5676    // Engine holding one accepted BUY 100 limit order O-NACK (venue V-NACK), with a recorder of
5677    // every event republished on the fill-declined topics
5678    fn nack_engine() -> (
5679        ExecutionEngine,
5680        InstrumentAny,
5681        Rc<RefCell<Vec<OrderEventAny>>>,
5682    ) {
5683        let instrument = InstrumentAny::CurrencyPair(audusd_sim());
5684        let cache = Rc::new(RefCell::new(Cache::default()));
5685        cache
5686            .borrow_mut()
5687            .add_instrument(instrument.clone())
5688            .unwrap();
5689        cache
5690            .borrow_mut()
5691            .add_account(CashAccount::default().into())
5692            .unwrap();
5693
5694        let engine = ExecutionEngine::new(
5695            Rc::new(RefCell::new(VirtualClock::new())),
5696            Rc::clone(&cache),
5697            None,
5698        );
5699        set_message_bus(Rc::new(RefCell::new(MessageBus::default())));
5700
5701        let order = OrderTestBuilder::new(OrderType::Limit)
5702            .instrument_id(instrument.id())
5703            .client_order_id(ClientOrderId::from("O-NACK"))
5704            .side(OrderSide::Buy)
5705            .quantity(Quantity::from(100))
5706            .price(Price::from("1.00000"))
5707            .build();
5708        cache
5709            .borrow_mut()
5710            .add_order(order.clone(), None, None, false)
5711            .unwrap();
5712        let account_id = AccountId::from("SIM-001");
5713        let submitted = TestOrderEventStubs::submitted(&order, account_id);
5714        cache.borrow_mut().update_order(&submitted).unwrap();
5715        let accepted =
5716            TestOrderEventStubs::accepted(&order, account_id, VenueOrderId::from("V-NACK"));
5717        cache.borrow_mut().update_order(&accepted).unwrap();
5718
5719        let nacks: Rc<RefCell<Vec<OrderEventAny>>> = Rc::new(RefCell::new(Vec::new()));
5720        let capture = Rc::clone(&nacks);
5721        msgbus::subscribe_order_events(
5722            "events.order_fill_declined.*".into(),
5723            TypedHandler::from(move |event: &OrderEventAny| {
5724                capture.borrow_mut().push(event.clone());
5725            }),
5726            None,
5727        );
5728
5729        (engine, instrument, nacks)
5730    }
5731
5732    fn nack_order_fill(
5733        instrument: &InstrumentAny,
5734        trade_id: &str,
5735        last_qty: Quantity,
5736    ) -> OrderFilled {
5737        OrderFilledSpec::builder()
5738            .instrument_id(instrument.id())
5739            .client_order_id(ClientOrderId::from("O-NACK"))
5740            .venue_order_id(VenueOrderId::from("V-NACK"))
5741            .account_id(AccountId::from("SIM-001"))
5742            .trade_id(TradeId::from(trade_id))
5743            .order_side(OrderSide::Buy)
5744            .order_type(OrderType::Limit)
5745            .last_qty(last_qty)
5746            .last_px(Price::from("1.00000"))
5747            .currency(instrument.quote_currency())
5748            .liquidity_side(LiquiditySide::Taker)
5749            .build()
5750    }
5751
5752    fn nack_order_fill_void(fill: &OrderFilled, voided_qty: Quantity) -> OrderFillVoided {
5753        OrderFillVoidedSpec::builder()
5754            .trader_id(fill.trader_id)
5755            .strategy_id(fill.strategy_id)
5756            .instrument_id(fill.instrument_id)
5757            .client_order_id(fill.client_order_id)
5758            .venue_order_id(fill.venue_order_id)
5759            .account_id(fill.account_id)
5760            .trade_id(fill.trade_id)
5761            .voided_qty(voided_qty)
5762            .order_side(fill.order_side)
5763            .order_type(fill.order_type)
5764            .last_px(fill.last_px)
5765            .currency(fill.currency)
5766            .liquidity_side(fill.liquidity_side)
5767            .build()
5768    }
5769}