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