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 stubs;
26
27use std::{
28    cell::{Cell, RefCell, RefMut},
29    collections::{HashMap, HashSet},
30    fmt::{Debug, Display},
31    rc::Rc,
32    time::SystemTime,
33};
34
35use ahash::AHashSet;
36use config::ExecutionEngineConfig;
37use futures::future::join_all;
38use indexmap::{IndexMap, IndexSet};
39use nautilus_common::{
40    cache::{Cache, PositionRef},
41    clients::{ExecutionClient, SocketReconnectLookup},
42    clock::Clock,
43    enums::LogColor,
44    generators::position_id::PositionIdGenerator,
45    log_info,
46    logging::{CMD, EVT, RECV, SEND},
47    messages::{
48        ExecutionReport,
49        execution::{
50            BatchCancelOrders, BatchModifyOrders, CancelAllOrders, CancelOrder, ModifyOrder,
51            QueryAccount, QueryOrder, SubmitOrder, SubmitOrderList, TradingCommand,
52        },
53    },
54    msgbus::{
55        self, MessagingSwitchboard, TypedHandler, TypedIntoHandler, get_message_bus,
56        switchboard::{self},
57    },
58    runner::{
59        TradingCommandMessage, capture_trading_cmd, trading_cmd_is_dispatching,
60        try_get_trading_cmd_sender,
61    },
62    timer::{TimeEvent, TimeEventCallback},
63};
64use nautilus_core::{
65    UUID4, UnixNanos, WeakCell,
66    datetime::{checked_mins_to_nanos, mins_to_secs, secs_to_nanos},
67};
68use nautilus_model::{
69    accounts::Account,
70    enums::{
71        AccountType, ContingencyType, OmsType, OrderSide, OrderStatus, OrderType, PositionSide,
72        TimeInForce, TrailingOffsetType,
73    },
74    events::{
75        OrderAccepted, OrderDenied, OrderDeniedReason, OrderEvent, OrderEventAny, OrderFillVoided,
76        OrderFilled, OrderInitialized, PositionChanged, PositionClosed, PositionEvent,
77        PositionOpened,
78    },
79    identifiers::{
80        AccountId, ClientId, ClientOrderId, InstrumentId, PositionId, StrategyId, TradeId, Venue,
81        VenueOrderId,
82    },
83    instruments::{Instrument, InstrumentAny},
84    orderbook::own::{OwnBookOrder, OwnOrderBook, should_handle_own_book_order},
85    orders::{Order, OrderAny, OrderError},
86    position::{Position, PositionReplayEvent},
87    reports::{ExecutionMassStatus, FillReport, OrderStatusReport, PositionStatusReport},
88    types::{Money, Quantity},
89};
90use position::CorrectedPosition;
91pub use position::{PositionStateSnapshot, SnapshotAnchorer};
92use rust_decimal::Decimal;
93use ustr::Ustr;
94
95use crate::{
96    client::ExecutionClientAdapter,
97    reconciliation::{
98        check_position_reconciliation, generate_external_order_status_events,
99        generate_reconciliation_order_events, generate_reconciliation_order_pre_fill_events,
100        generate_reconciliation_order_snapshot_events, reconcile_fill_report as reconcile_fill,
101    },
102};
103
104const TIMER_SNAPSHOT_POSITIONS: &str = "ExecEngine_SNAPSHOT_POSITIONS";
105const TIMER_PURGE_CLOSED_ORDERS: &str = "ExecEngine_PURGE_CLOSED_ORDERS";
106const TIMER_PURGE_CLOSED_POSITIONS: &str = "ExecEngine_PURGE_CLOSED_POSITIONS";
107const TIMER_PURGE_ACCOUNT_EVENTS: &str = "ExecEngine_PURGE_ACCOUNT_EVENTS";
108
109/// Central execution engine responsible for orchestrating order routing and execution.
110///
111/// The execution engine manages the entire order lifecycle from submission to completion,
112/// handling routing to appropriate execution clients, position management, and event
113/// processing. It supports multiple execution venues through registered clients and
114/// provides sophisticated order management capabilities.
115pub struct ExecutionEngine {
116    clock: Rc<RefCell<dyn Clock>>,
117    cache: Rc<RefCell<Cache>>,
118    clients: IndexMap<ClientId, ExecutionClientAdapter>,
119    default_client_id: Option<ClientId>,
120    routing_map: HashMap<Venue, ClientId>,
121    oms_overrides: HashMap<StrategyId, OmsType>,
122    external_order_claims: HashMap<InstrumentId, StrategyId>,
123    external_clients: HashSet<ClientId>,
124    pos_id_generator: PositionIdGenerator,
125    config: ExecutionEngineConfig,
126    command_count: Cell<u64>,
127    event_count: u64,
128    report_count: u64,
129    filtered_unclaimed_external_order_count: u64,
130    snapshot_anchorer: Option<SnapshotAnchorer>,
131}
132
133impl Debug for ExecutionEngine {
134    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135        f.debug_struct(stringify!(ExecutionEngine))
136            .field("client_count", &self.clients.len())
137            .finish()
138    }
139}
140
141impl ExecutionEngine {
142    /// Creates a new [`ExecutionEngine`] instance.
143    pub fn new(
144        clock: Rc<RefCell<dyn Clock>>,
145        cache: Rc<RefCell<Cache>>,
146        config: Option<ExecutionEngineConfig>,
147    ) -> Self {
148        let trader_id = get_message_bus().borrow().trader_id;
149        Self {
150            clock: clock.clone(),
151            cache,
152            clients: IndexMap::new(),
153            default_client_id: None,
154            routing_map: HashMap::new(),
155            oms_overrides: HashMap::new(),
156            external_order_claims: HashMap::new(),
157            external_clients: config
158                .as_ref()
159                .and_then(|c| c.external_clients.clone())
160                .unwrap_or_default()
161                .into_iter()
162                .collect(),
163            pos_id_generator: PositionIdGenerator::new(trader_id, clock),
164            config: config.unwrap_or_default(),
165            command_count: Cell::new(0),
166            event_count: 0,
167            report_count: 0,
168            filtered_unclaimed_external_order_count: 0,
169            snapshot_anchorer: None,
170        }
171    }
172
173    /// Registers all message bus handlers for the execution engine.
174    pub fn register_msgbus_handlers(engine: &Rc<RefCell<Self>>) {
175        let weak = WeakCell::from(Rc::downgrade(engine));
176
177        let weak1 = weak.clone();
178        msgbus::register_trading_command_endpoint(
179            MessagingSwitchboard::exec_engine_execute(),
180            TypedIntoHandler::from(move |cmd: TradingCommand| {
181                if let Some(rc) = weak1.upgrade() {
182                    rc.borrow().execute(cmd);
183                }
184            }),
185        );
186
187        // Queued endpoint for deferred command execution (re-entrancy safe),
188        // with direct dispatch when no sender is installed.
189        msgbus::register_trading_command_endpoint(
190            MessagingSwitchboard::exec_engine_queue_execute(),
191            TypedIntoHandler::from(move |cmd: TradingCommand| {
192                let endpoint = MessagingSwitchboard::exec_engine_execute();
193                if trading_cmd_is_dispatching() {
194                    capture_trading_cmd(TradingCommandMessage::new(endpoint, cmd));
195                } else if let Some(sender) = try_get_trading_cmd_sender() {
196                    sender.execute(TradingCommandMessage::new(endpoint, cmd));
197                } else {
198                    msgbus::send_trading_command(endpoint, cmd);
199                }
200            }),
201        );
202
203        let weak2 = weak.clone();
204        msgbus::register_order_event_endpoint(
205            MessagingSwitchboard::exec_engine_process(),
206            TypedIntoHandler::from(move |event: OrderEventAny| {
207                if let Some(rc) = weak2.upgrade() {
208                    rc.borrow_mut().process(&event);
209                }
210            }),
211        );
212
213        let weak3 = weak;
214        msgbus::register_execution_report_endpoint(
215            MessagingSwitchboard::exec_engine_reconcile_execution_report(),
216            TypedIntoHandler::from(move |report: ExecutionReport| {
217                if let Some(rc) = weak3.upgrade() {
218                    rc.borrow_mut().reconcile_execution_report(&report);
219                }
220            }),
221        );
222    }
223
224    /// Returns the total count of trading commands received by the engine.
225    #[must_use]
226    pub fn command_count(&self) -> u64 {
227        self.command_count.get()
228    }
229
230    /// Returns the total count of order events received by the engine.
231    #[must_use]
232    pub const fn event_count(&self) -> u64 {
233        self.event_count
234    }
235
236    /// Returns the total count of execution reports received by the engine.
237    #[must_use]
238    pub const fn report_count(&self) -> u64 {
239        self.report_count
240    }
241
242    /// Returns the count of unclaimed external venue orders filtered by execution reconciliation.
243    #[must_use]
244    pub const fn filtered_unclaimed_external_order_count(&self) -> u64 {
245        self.filtered_unclaimed_external_order_count
246    }
247
248    /// Subscribes to instrument updates for a venue via the message bus.
249    ///
250    /// When instruments are published by the `DataEngine`, the handler routes
251    /// them to the execution client registered for that venue.
252    pub fn subscribe_venue_instruments(engine: &Rc<RefCell<Self>>, venue: Venue) {
253        let weak = WeakCell::from(Rc::downgrade(engine));
254        let pattern = switchboard::get_instruments_pattern(venue);
255
256        let handler = TypedHandler::from(move |instrument: &InstrumentAny| {
257            if let Some(rc) = weak.upgrade() {
258                let venue = instrument.id().venue;
259                let client_id = rc.borrow().routing_map.get(&venue).copied();
260                if let Some(client_id) = client_id {
261                    let mut engine = rc.borrow_mut();
262                    if let Some(adapter) = engine.get_client_adapter_mut(&client_id) {
263                        adapter.on_instrument(instrument.clone());
264                    }
265                }
266            }
267        });
268
269        msgbus::subscribe_instruments(pattern, handler, None);
270        log::info!("Subscribed to instrument updates for venue {venue}");
271    }
272
273    #[must_use]
274    /// Returns the position ID count for the specified strategy.
275    pub fn position_id_count(&self, strategy_id: StrategyId) -> usize {
276        self.pos_id_generator.count(strategy_id)
277    }
278
279    #[must_use]
280    /// Returns a reference to the cache.
281    pub fn cache(&self) -> &Rc<RefCell<Cache>> {
282        &self.cache
283    }
284
285    #[must_use]
286    /// Returns a reference to the configuration.
287    pub const fn config(&self) -> &ExecutionEngineConfig {
288        &self.config
289    }
290
291    /// Sets the cache snapshot anchorer.
292    ///
293    /// The system event-store integration installs this while a run is open. Passing
294    /// `None` disables anchor recording for later cache snapshots.
295    pub fn set_snapshot_anchorer(&mut self, anchorer: Option<SnapshotAnchorer>) {
296        self.snapshot_anchorer = anchorer;
297    }
298
299    #[must_use]
300    /// Checks the integrity of cached execution data.
301    pub fn check_integrity(&self) -> bool {
302        self.cache.borrow_mut().check_integrity()
303    }
304
305    #[must_use]
306    /// Returns true if all registered execution clients are connected.
307    pub fn check_connected(&self) -> bool {
308        self.clients.values().all(|c| c.is_connected())
309    }
310
311    #[must_use]
312    /// Returns true if all registered execution clients are disconnected.
313    pub fn check_disconnected(&self) -> bool {
314        self.clients.values().all(|c| !c.is_connected())
315    }
316
317    /// Returns connection status for each registered client.
318    #[must_use]
319    pub fn client_connection_status(&self) -> Vec<(ClientId, bool)> {
320        self.clients
321            .values()
322            .map(|c| (c.client_id(), c.is_connected()))
323            .collect()
324    }
325
326    #[must_use]
327    /// Checks for residual positions and orders in the cache.
328    pub fn check_residuals(&self) -> bool {
329        self.cache.borrow().check_residuals()
330    }
331
332    #[must_use]
333    /// Returns the set of instruments that have external order claims.
334    pub fn get_external_order_claims_instruments(&self) -> HashSet<InstrumentId> {
335        self.external_order_claims.keys().copied().collect()
336    }
337
338    #[must_use]
339    /// Returns the configured external client IDs.
340    pub fn get_external_client_ids(&self) -> HashSet<ClientId> {
341        self.external_clients.clone()
342    }
343
344    #[must_use]
345    /// Returns any external order claim for the given instrument ID.
346    pub fn get_external_order_claim(&self, instrument_id: &InstrumentId) -> Option<StrategyId> {
347        self.external_order_claims.get(instrument_id).copied()
348    }
349
350    /// Returns the instruments with external order claims owned by `strategy_id`.
351    #[must_use]
352    pub fn get_external_order_claims_for_strategy(
353        &self,
354        strategy_id: StrategyId,
355    ) -> HashSet<InstrumentId> {
356        self.external_order_claims
357            .iter()
358            .filter_map(|(instrument_id, owner)| (*owner == strategy_id).then_some(*instrument_id))
359            .collect()
360    }
361
362    /// Registers a new execution client.
363    ///
364    /// # Errors
365    ///
366    /// Returns an error if a client with the same ID is already registered.
367    pub fn register_client(&mut self, client: Box<dyn ExecutionClient>) -> anyhow::Result<()> {
368        let client_id = client.client_id();
369        let venue = client.venue();
370
371        if self.clients.contains_key(&client_id) {
372            anyhow::bail!("Client already registered with ID {client_id}");
373        }
374
375        let adapter = ExecutionClientAdapter::new(client);
376
377        if let Some(existing_client_id) = self.routing_map.get(&venue) {
378            anyhow::bail!(
379                "Venue {venue} already routed to {existing_client_id}, \
380                 cannot register {client_id} for the same venue"
381            );
382        }
383
384        self.routing_map.insert(venue, client_id);
385        log::debug!("Registered client {client_id}");
386        self.clients.insert(client_id, adapter);
387        Ok(())
388    }
389
390    /// Registers a default execution client for fallback routing.
391    pub fn register_default_client(&mut self, client: Box<dyn ExecutionClient>) {
392        let client_id = client.client_id();
393        let adapter = ExecutionClientAdapter::new(client);
394
395        self.clients.insert(client_id, adapter);
396        self.default_client_id = Some(client_id);
397        log::debug!("Registered default client {client_id}");
398    }
399
400    /// Marks an already-registered client as the default for fallback routing.
401    ///
402    /// # Errors
403    ///
404    /// Returns an error if no client is registered with the given ID, or a default
405    /// client has already been set.
406    pub fn set_default_client(&mut self, client_id: ClientId) -> anyhow::Result<()> {
407        if self.default_client_id.is_some() {
408            anyhow::bail!("default client already registered");
409        }
410
411        if !self.clients.contains_key(&client_id) {
412            anyhow::bail!("No client registered with ID {client_id}");
413        }
414        self.default_client_id = Some(client_id);
415        log::debug!("Set client {client_id} as default");
416        Ok(())
417    }
418
419    #[must_use]
420    /// Returns a reference to the execution client registered with the given ID.
421    pub fn get_client(&self, client_id: &ClientId) -> Option<&dyn ExecutionClient> {
422        self.clients.get(client_id).map(|a| a.client.as_ref())
423    }
424
425    /// Resolves a reconnectable socket endpoint owned by a registered execution client.
426    #[must_use]
427    pub fn socket_reconnect_lookup(
428        &self,
429        client_id: &ClientId,
430        endpoint: Ustr,
431    ) -> SocketReconnectLookup {
432        let Some(client) = self.get_client(client_id) else {
433            return SocketReconnectLookup::ClientNotFound;
434        };
435        let Some(registry) = client.socket_reconnect_registry() else {
436            return SocketReconnectLookup::Unsupported;
437        };
438
439        registry.get(endpoint).map_or(
440            SocketReconnectLookup::EndpointNotFound,
441            SocketReconnectLookup::Handle,
442        )
443    }
444
445    #[must_use]
446    /// Returns a mutable reference to the execution client adapter registered with the given ID.
447    pub fn get_client_adapter_mut(
448        &mut self,
449        client_id: &ClientId,
450    ) -> Option<&mut ExecutionClientAdapter> {
451        self.clients.get_mut(client_id)
452    }
453
454    /// Generates mass status for the given client.
455    ///
456    /// # Errors
457    ///
458    /// Returns an error if the client is not found or mass status generation fails.
459    pub async fn generate_mass_status(
460        &mut self,
461        client_id: &ClientId,
462        lookback_mins: Option<u64>,
463    ) -> anyhow::Result<Option<ExecutionMassStatus>> {
464        if let Some(client) = self.get_client_adapter_mut(client_id) {
465            client.generate_mass_status(lookback_mins).await
466        } else {
467            anyhow::bail!("Client {client_id} not found")
468        }
469    }
470
471    /// Registers an external order with the execution client for tracking.
472    ///
473    /// This is called after reconciliation creates an external order, allowing the
474    /// execution client to track it for subsequent events (e.g., cancellations).
475    pub fn register_external_order(
476        &self,
477        client_order_id: ClientOrderId,
478        venue_order_id: VenueOrderId,
479        instrument_id: InstrumentId,
480        strategy_id: StrategyId,
481        ts_init: UnixNanos,
482    ) {
483        let venue = instrument_id.venue;
484        // Prefer the cached origin over venue routing so tracking lands on the
485        // client whose stream materialized the order.
486        let client_id = self
487            .cache
488            .borrow()
489            .client_id(&client_order_id)
490            .copied()
491            .or_else(|| self.routing_map.get(&venue).copied())
492            .or(self.default_client_id);
493
494        if let Some(client_id) = client_id
495            && let Some(client) = self.clients.get(&client_id)
496        {
497            client.register_external_order(
498                client_order_id,
499                venue_order_id,
500                instrument_id,
501                strategy_id,
502                ts_init,
503            );
504        }
505    }
506
507    #[must_use]
508    /// Returns all registered execution client IDs.
509    pub fn client_ids(&self) -> Vec<ClientId> {
510        self.clients.keys().copied().collect()
511    }
512
513    #[must_use]
514    /// Returns mutable access to all registered execution clients.
515    pub fn get_clients_mut(&mut self) -> Vec<&mut ExecutionClientAdapter> {
516        self.clients.values_mut().collect()
517    }
518
519    /// Returns all registered execution clients.
520    #[must_use]
521    pub fn get_all_clients(&self) -> Vec<&dyn ExecutionClient> {
522        self.clients.values().map(|a| a.client.as_ref()).collect()
523    }
524
525    #[must_use]
526    /// Returns execution clients that would handle the given orders.
527    ///
528    /// This method first attempts to resolve each order's originating client from the cache,
529    /// then falls back to venue routing for any orders without a cached client.
530    pub fn get_clients_for_orders(&self, orders: &[OrderAny]) -> Vec<&dyn ExecutionClient> {
531        let mut client_ids: IndexSet<ClientId> = IndexSet::new();
532        let mut venues: IndexSet<Venue> = IndexSet::new();
533
534        // Collect client IDs from cache and venues for fallback
535        for order in orders {
536            venues.insert(order.instrument_id().venue);
537            if let Some(client_id) = self.cache.borrow().client_id(&order.client_order_id()) {
538                client_ids.insert(*client_id);
539            }
540        }
541
542        let mut clients: Vec<&dyn ExecutionClient> = Vec::new();
543
544        // Add clients for cached client IDs (orders go back to originating client)
545        for client_id in &client_ids {
546            if let Some(adapter) = self.clients.get(client_id)
547                && !clients.iter().any(|c| c.client_id() == adapter.client_id)
548            {
549                clients.push(adapter.client.as_ref());
550            }
551        }
552
553        // Add clients for venue routing (for orders not in cache)
554        for venue in &venues {
555            let resolved_id = self
556                .routing_map
557                .get(venue)
558                .copied()
559                .or(self.default_client_id);
560
561            if let Some(adapter) = resolved_id.and_then(|id| self.clients.get(&id))
562                && !clients.iter().any(|c| c.client_id() == adapter.client_id)
563            {
564                clients.push(adapter.client.as_ref());
565            }
566        }
567
568        clients
569    }
570
571    /// Sets routing for a specific venue to a given client ID.
572    ///
573    /// # Errors
574    ///
575    /// Returns an error if the client ID is not registered.
576    pub fn register_venue_routing(
577        &mut self,
578        client_id: ClientId,
579        venue: Venue,
580    ) -> anyhow::Result<()> {
581        if !self.clients.contains_key(&client_id) {
582            anyhow::bail!("No client registered with ID {client_id}");
583        }
584
585        if let Some(existing_client_id) = self.routing_map.get(&venue)
586            && *existing_client_id != client_id
587        {
588            anyhow::bail!(
589                "Venue {venue} already routed to {existing_client_id}, \
590                 cannot re-route to {client_id}"
591            );
592        }
593
594        self.routing_map.insert(venue, client_id);
595        log::info!("Set client {client_id} routing for {venue}");
596        Ok(())
597    }
598
599    /// Registers the OMS (Order Management System) type for a strategy.
600    ///
601    /// If an OMS type is already registered for this strategy, it will be overridden.
602    pub fn register_oms_type(&mut self, strategy_id: StrategyId, oms_type: OmsType) {
603        self.oms_overrides.insert(strategy_id, oms_type);
604        log::info!("Registered OMS::{oms_type:?} for {strategy_id}");
605    }
606
607    /// Registers external order claims for a strategy.
608    ///
609    /// Venue-sourced external orders, fills, and materialized reconciliation activity for matching
610    /// instruments will be associated with the strategy.
611    ///
612    /// This operation is atomic: either all instruments are registered or none are.
613    ///
614    /// # Errors
615    ///
616    /// Returns an error if any instrument already has a registered claim.
617    pub fn register_external_order_claims(
618        &mut self,
619        strategy_id: StrategyId,
620        instrument_ids: &HashSet<InstrumentId>,
621    ) -> anyhow::Result<()> {
622        // Validate all instruments first
623        for instrument_id in instrument_ids {
624            if let Some(existing) = self.external_order_claims.get(instrument_id) {
625                anyhow::bail!(
626                    "External order claim for {instrument_id} already exists for {existing}"
627                );
628            }
629        }
630
631        // If validation passed, insert all claims
632        for instrument_id in instrument_ids {
633            self.external_order_claims
634                .insert(*instrument_id, strategy_id);
635        }
636
637        if !instrument_ids.is_empty() {
638            log::info!("Registered external order claims for {strategy_id}: {instrument_ids:?}");
639        }
640
641        Ok(())
642    }
643
644    /// Commits external order claims for `strategy_id` without validation.
645    ///
646    /// The caller must have preflighted every instrument against
647    /// [`Self::get_external_order_claim`]: an existing claim is overwritten
648    /// without error. Coordinated live-node callers should use
649    /// `LiveNode::register_external_order_claims`, which preflights both the
650    /// execution engine and the reconciliation manager before committing;
651    /// ordinary callers should use
652    /// [`Self::register_external_order_claims`] instead.
653    pub fn commit_external_order_claims(
654        &mut self,
655        strategy_id: StrategyId,
656        instrument_ids: &HashSet<InstrumentId>,
657    ) {
658        self.external_order_claims.extend(
659            instrument_ids
660                .iter()
661                .map(|instrument_id| (*instrument_id, strategy_id)),
662        );
663
664        if !instrument_ids.is_empty() {
665            log::info!("Registered external order claims for {strategy_id}: {instrument_ids:?}");
666        }
667    }
668
669    /// Deregisters all external order claims owned by `strategy_id`.
670    ///
671    /// Coordinated live-node callers should use
672    /// `LiveNode::deregister_external_order_claims` so the execution engine and
673    /// reconciliation manager remain consistent.
674    pub fn deregister_external_order_claims(&mut self, strategy_id: StrategyId) {
675        self.external_order_claims
676            .retain(|_, owner| *owner != strategy_id);
677    }
678
679    /// # Errors
680    ///
681    /// Returns an error if no client is registered with the given ID.
682    pub fn deregister_client(&mut self, client_id: ClientId) -> anyhow::Result<()> {
683        if self.clients.shift_remove(&client_id).is_some() {
684            if self.default_client_id == Some(client_id) {
685                self.default_client_id = None;
686            }
687
688            // Remove from routing map if present
689            self.routing_map
690                .retain(|_, mapped_id| mapped_id != &client_id);
691            log::info!("Deregistered client {client_id}");
692            Ok(())
693        } else {
694            anyhow::bail!("No client registered with ID {client_id}")
695        }
696    }
697
698    /// Connects all registered execution clients concurrently.
699    ///
700    /// Connection failures are logged but do not prevent the node from running.
701    pub async fn connect(&mut self) {
702        let futures: Vec<_> = self
703            .get_clients_mut()
704            .into_iter()
705            .map(ExecutionClientAdapter::connect)
706            .collect();
707
708        let results = join_all(futures).await;
709
710        for error in results.into_iter().filter_map(Result::err) {
711            log::error!("Failed to connect execution client: {error:#}");
712        }
713    }
714
715    /// Disconnects all registered execution clients concurrently.
716    ///
717    /// # Errors
718    ///
719    /// Returns an error if any client fails to disconnect.
720    pub async fn disconnect(&mut self) -> anyhow::Result<()> {
721        let futures: Vec<_> = self
722            .get_clients_mut()
723            .into_iter()
724            .map(ExecutionClientAdapter::disconnect)
725            .collect();
726
727        let results = join_all(futures).await;
728        let errors: Vec<_> = results.into_iter().filter_map(Result::err).collect();
729
730        if errors.is_empty() {
731            Ok(())
732        } else {
733            let error_msgs: Vec<_> = errors.iter().map(ToString::to_string).collect();
734            anyhow::bail!(
735                "Failed to disconnect execution clients: {}",
736                error_msgs.join("; ")
737            )
738        }
739    }
740
741    /// Sets the `manage_own_order_books` configuration option.
742    pub fn set_manage_own_order_books(&mut self, value: bool) {
743        self.config.manage_own_order_books = value;
744    }
745
746    /// Starts the position snapshot timer if configured.
747    #[expect(
748        clippy::missing_panics_doc,
749        reason = "timer registration is not expected to fail"
750    )]
751    pub fn start_snapshot_timer(&mut self) {
752        if let Some(interval_secs) = self
753            .config
754            .snapshot_positions_interval_secs
755            .filter(|&secs| secs > 0.0)
756            && !self
757                .clock
758                .borrow()
759                .timer_names()
760                .contains(&TIMER_SNAPSHOT_POSITIONS)
761        {
762            let interval_ns = match secs_to_nanos(interval_secs) {
763                Ok(ns) => ns,
764                Err(e) => {
765                    log::error!("Cannot start position snapshots timer: {e}");
766                    return;
767                }
768            };
769            let clock = self.clock.clone();
770            let cache = self.cache.clone();
771            let debug = self.config.debug;
772
773            let callback_fn: Rc<dyn Fn(TimeEvent)> = Rc::new(move |_event| {
774                Self::snapshot_open_positions(&clock, &cache, debug);
775            });
776            let callback = TimeEventCallback::from(callback_fn);
777
778            log::info!("Starting position snapshots timer at {interval_secs} second intervals");
779            self.clock
780                .borrow_mut()
781                .set_timer_ns(
782                    TIMER_SNAPSHOT_POSITIONS,
783                    interval_ns,
784                    None,
785                    None,
786                    Some(callback),
787                    None,
788                    None,
789                )
790                .expect("Failed to set position snapshots timer");
791        }
792    }
793
794    /// Stops the position snapshot timer if running.
795    pub fn stop_snapshot_timer(&mut self) {
796        let timer_registered = self
797            .clock
798            .borrow()
799            .timer_names()
800            .contains(&TIMER_SNAPSHOT_POSITIONS);
801
802        if timer_registered {
803            log::info!("Canceling position snapshots timer");
804            self.clock
805                .borrow_mut()
806                .cancel_timer(TIMER_SNAPSHOT_POSITIONS);
807        }
808    }
809
810    /// Starts the purge timers if configured.
811    pub fn start_purge_timers(&mut self) {
812        if let Some(interval_mins) = self
813            .config
814            .purge_closed_orders_interval_mins
815            .filter(|&m| m > 0)
816            && !self
817                .clock
818                .borrow()
819                .timer_names()
820                .contains(&TIMER_PURGE_CLOSED_ORDERS)
821        {
822            'purge_closed_orders: {
823                let Some(interval_ns) = checked_mins_to_nanos(u64::from(interval_mins)) else {
824                    log::error!(
825                        "Invalid purge_closed_orders_interval_mins {interval_mins}: minutes to nanoseconds conversion overflow"
826                    );
827                    break 'purge_closed_orders;
828                };
829                let buffer_mins = self.config.purge_closed_orders_buffer_mins.unwrap_or(0);
830                let buffer_secs = mins_to_secs(u64::from(buffer_mins));
831                let cache = self.cache.clone();
832                let clock = self.clock.clone();
833
834                let callback_fn: Rc<dyn Fn(TimeEvent)> = Rc::new(move |_event| {
835                    let ts_now = clock.borrow().timestamp_ns();
836                    cache.borrow_mut().purge_closed_orders(ts_now, buffer_secs);
837                });
838                let callback = TimeEventCallback::from(callback_fn);
839
840                log::info!(
841                    "Starting purge closed orders timer at {interval_mins} minute intervals"
842                );
843
844                if let Err(e) = self.clock.borrow_mut().set_timer_ns(
845                    TIMER_PURGE_CLOSED_ORDERS,
846                    interval_ns,
847                    None,
848                    None,
849                    Some(callback),
850                    None,
851                    None,
852                ) {
853                    log::error!("Failed to set {TIMER_PURGE_CLOSED_ORDERS} timer: {e}");
854                }
855            }
856        }
857
858        if let Some(interval_mins) = self
859            .config
860            .purge_closed_positions_interval_mins
861            .filter(|&m| m > 0)
862            && !self
863                .clock
864                .borrow()
865                .timer_names()
866                .contains(&TIMER_PURGE_CLOSED_POSITIONS)
867        {
868            'purge_closed_positions: {
869                let Some(interval_ns) = checked_mins_to_nanos(u64::from(interval_mins)) else {
870                    log::error!(
871                        "Invalid purge_closed_positions_interval_mins {interval_mins}: minutes to nanoseconds conversion overflow"
872                    );
873                    break 'purge_closed_positions;
874                };
875                let buffer_mins = self.config.purge_closed_positions_buffer_mins.unwrap_or(0);
876                let buffer_secs = mins_to_secs(u64::from(buffer_mins));
877                let cache = self.cache.clone();
878                let clock = self.clock.clone();
879
880                let callback_fn: Rc<dyn Fn(TimeEvent)> = Rc::new(move |_event| {
881                    let ts_now = clock.borrow().timestamp_ns();
882                    cache
883                        .borrow_mut()
884                        .purge_closed_positions(ts_now, buffer_secs);
885                });
886                let callback = TimeEventCallback::from(callback_fn);
887
888                log::info!(
889                    "Starting purge closed positions timer at {interval_mins} minute intervals"
890                );
891
892                if let Err(e) = self.clock.borrow_mut().set_timer_ns(
893                    TIMER_PURGE_CLOSED_POSITIONS,
894                    interval_ns,
895                    None,
896                    None,
897                    Some(callback),
898                    None,
899                    None,
900                ) {
901                    log::error!("Failed to set {TIMER_PURGE_CLOSED_POSITIONS} timer: {e}");
902                }
903            }
904        }
905
906        if let Some(interval_mins) = self
907            .config
908            .purge_account_events_interval_mins
909            .filter(|&m| m > 0)
910            && !self
911                .clock
912                .borrow()
913                .timer_names()
914                .contains(&TIMER_PURGE_ACCOUNT_EVENTS)
915        {
916            'purge_account_events: {
917                let Some(interval_ns) = checked_mins_to_nanos(u64::from(interval_mins)) else {
918                    log::error!(
919                        "Invalid purge_account_events_interval_mins {interval_mins}: minutes to nanoseconds conversion overflow"
920                    );
921                    break 'purge_account_events;
922                };
923                let lookback_mins = self.config.purge_account_events_lookback_mins.unwrap_or(0);
924                let lookback_secs = mins_to_secs(u64::from(lookback_mins));
925                let cache = self.cache.clone();
926                let clock = self.clock.clone();
927
928                let callback_fn: Rc<dyn Fn(TimeEvent)> = Rc::new(move |_event| {
929                    let ts_now = clock.borrow().timestamp_ns();
930                    cache
931                        .borrow_mut()
932                        .purge_account_events(ts_now, lookback_secs);
933                });
934                let callback = TimeEventCallback::from(callback_fn);
935
936                log::info!(
937                    "Starting purge account events timer at {interval_mins} minute intervals"
938                );
939
940                if let Err(e) = self.clock.borrow_mut().set_timer_ns(
941                    TIMER_PURGE_ACCOUNT_EVENTS,
942                    interval_ns,
943                    None,
944                    None,
945                    Some(callback),
946                    None,
947                    None,
948                ) {
949                    log::error!("Failed to set {TIMER_PURGE_ACCOUNT_EVENTS} timer: {e}");
950                }
951            }
952        }
953    }
954
955    /// Stops the purge timers if running.
956    pub fn stop_purge_timers(&mut self) {
957        let timer_names: Vec<String> = self
958            .clock
959            .borrow()
960            .timer_names()
961            .into_iter()
962            .map(String::from)
963            .collect();
964
965        if timer_names.iter().any(|n| n == TIMER_PURGE_CLOSED_ORDERS) {
966            log::info!("Canceling purge closed orders timer");
967            self.clock
968                .borrow_mut()
969                .cancel_timer(TIMER_PURGE_CLOSED_ORDERS);
970        }
971
972        if timer_names
973            .iter()
974            .any(|n| n == TIMER_PURGE_CLOSED_POSITIONS)
975        {
976            log::info!("Canceling purge closed positions timer");
977            self.clock
978                .borrow_mut()
979                .cancel_timer(TIMER_PURGE_CLOSED_POSITIONS);
980        }
981
982        if timer_names.iter().any(|n| n == TIMER_PURGE_ACCOUNT_EVENTS) {
983            log::info!("Canceling purge account events timer");
984            self.clock
985                .borrow_mut()
986                .cancel_timer(TIMER_PURGE_ACCOUNT_EVENTS);
987        }
988    }
989
990    /// Creates snapshots of all open positions.
991    pub fn snapshot_open_position_states(&self) {
992        Self::snapshot_open_positions(&self.clock, &self.cache, self.config.debug);
993    }
994
995    fn snapshot_open_positions(
996        clock: &Rc<RefCell<dyn Clock>>,
997        cache: &Rc<RefCell<Cache>>,
998        debug: bool,
999    ) {
1000        let positions: Vec<Position> = cache
1001            .borrow()
1002            .positions_open(None, None, None, None, None)
1003            .into_iter()
1004            .map(|p| p.cloned())
1005            .collect();
1006
1007        for position in positions {
1008            Self::publish_position_state_snapshot(clock, cache, debug, &position, true);
1009        }
1010    }
1011
1012    #[expect(clippy::await_holding_refcell_ref)]
1013    /// Loads persistent state into cache and rebuilds indices.
1014    ///
1015    /// # Errors
1016    ///
1017    /// Returns an error if any cache operation fails.
1018    pub async fn load_cache(&mut self) -> anyhow::Result<()> {
1019        let ts = SystemTime::now(); // dst-ok: init-time log timing, not on DST state path
1020
1021        {
1022            let mut cache = self.cache.borrow_mut();
1023            cache.clear_index();
1024            cache.cache_general()?;
1025        }
1026
1027        self.cache.borrow_mut().cache_all().await?;
1028
1029        // Snapshot before iterating: `get_or_init_own_order_book` re-enters `self.cache.borrow_mut()`.
1030        let own_book_entries: Vec<(InstrumentId, OwnBookOrder)> = {
1031            let mut cache = self.cache.borrow_mut();
1032            cache.build_index();
1033            let _ = cache.check_integrity();
1034
1035            if self.config.manage_own_order_books {
1036                cache
1037                    .orders(None, None, None, None, None)
1038                    .into_iter()
1039                    .filter(|o| !o.is_closed() && should_handle_own_book_order(o))
1040                    .map(|o| (o.instrument_id(), o.to_own_book_order()))
1041                    .collect()
1042            } else {
1043                Vec::new()
1044            }
1045        };
1046
1047        for (instrument_id, own_order) in own_book_entries {
1048            let mut own_book = self.get_or_init_own_order_book(&instrument_id);
1049            own_book.add(own_order);
1050        }
1051
1052        self.set_position_id_counts();
1053
1054        log::info!(
1055            "Loaded cache in {}ms",
1056            SystemTime::now() // dst-ok: init-time log timing, not on DST state path
1057                .duration_since(ts)
1058                .map_err(|e| anyhow::anyhow!("Failed to calculate duration: {e}"))?
1059                .as_millis()
1060        );
1061
1062        Ok(())
1063    }
1064
1065    /// Flushes the database to persist all cached data.
1066    pub fn flush_db(&self) {
1067        self.cache.borrow_mut().flush_db();
1068    }
1069
1070    /// Reconciles an execution report.
1071    pub fn reconcile_execution_report(&mut self, report: &ExecutionReport) {
1072        if !matches!(report, ExecutionReport::MassStatus(_)) {
1073            self.report_count += 1;
1074        }
1075
1076        match report {
1077            ExecutionReport::Order(order_report) => {
1078                self.reconcile_order_status_report(order_report);
1079            }
1080            ExecutionReport::Fill(fill_report) => {
1081                self.reconcile_fill_report(fill_report);
1082            }
1083            ExecutionReport::OrderWithFills(order_report, fills) => {
1084                self.reconcile_order_with_fills(order_report, fills);
1085            }
1086            ExecutionReport::Position(position_report) => {
1087                self.reconcile_position_report(position_report);
1088            }
1089            ExecutionReport::MassStatus(mass_status) => {
1090                self.reconcile_execution_mass_status(mass_status);
1091            }
1092        }
1093    }
1094
1095    /// Reconciles an order status report received at runtime.
1096    ///
1097    /// Handles order status transitions by generating appropriate events when the venue
1098    /// reports a different status than our local state. Supports all order states including
1099    /// fills with inferred fill generation when instruments are available.
1100    ///
1101    /// When the order is not found in cache, creates an external order from the report.
1102    /// This handles exchange-generated orders (liquidation, ADL, settlement) that were
1103    /// not submitted locally.
1104    pub fn reconcile_order_status_report(&mut self, report: &OrderStatusReport) {
1105        msgbus::publish_any(
1106            MessagingSwitchboard::reconciliation_raw_order_status_report_topic(),
1107            report,
1108        );
1109
1110        let cache = self.cache.borrow();
1111
1112        let order = report
1113            .client_order_id
1114            .and_then(|id| cache.order(&id).map(|o| o.clone()))
1115            .or_else(|| {
1116                cache
1117                    .client_order_id(&report.venue_order_id)
1118                    .and_then(|cid| cache.order(cid).map(|o| o.clone()))
1119            });
1120
1121        let instrument = cache.instrument(&report.instrument_id).cloned();
1122
1123        drop(cache);
1124
1125        if let Some(order) = order {
1126            let ts_now = self.clock.borrow().timestamp_ns();
1127            let events =
1128                generate_reconciliation_order_events(&order, report, instrument.as_ref(), ts_now);
1129
1130            for event in &events {
1131                self.handle_event(event);
1132            }
1133        } else {
1134            self.create_external_order(report, instrument.as_ref());
1135        }
1136    }
1137
1138    fn create_external_order(
1139        &mut self,
1140        report: &OrderStatusReport,
1141        instrument: Option<&InstrumentAny>,
1142    ) {
1143        let Some(instrument) = instrument else {
1144            log::warn!(
1145                "Cannot create external order for venue_order_id={}: instrument {} not found",
1146                report.venue_order_id,
1147                report.instrument_id
1148            );
1149            return;
1150        };
1151
1152        let Some(order) = self.materialize_external_order_from_status(report) else {
1153            return;
1154        };
1155
1156        let ts_now = self.clock.borrow().timestamp_ns();
1157        let events = generate_external_order_status_events(
1158            &order,
1159            report,
1160            &report.account_id,
1161            instrument,
1162            ts_now,
1163        );
1164
1165        for event in &events {
1166            self.handle_event(event);
1167        }
1168    }
1169
1170    /// Builds and registers an external order from an [`OrderStatusReport`] without
1171    /// emitting status events. Returns the registered order.
1172    fn materialize_external_order_from_status(
1173        &mut self,
1174        report: &OrderStatusReport,
1175    ) -> Option<OrderAny> {
1176        let strategy_id = self.resolve_external_strategy(&report.instrument_id);
1177        if self.should_filter_unclaimed_external_order(strategy_id) {
1178            self.filtered_unclaimed_external_order_count += 1;
1179
1180            if self.filtered_unclaimed_external_order_count == 1 {
1181                let external_order_id = report
1182                    .client_order_id
1183                    .map_or_else(|| report.venue_order_id.to_string(), |id| id.to_string());
1184                log::info!(
1185                    "Filtering unclaimed external orders; first filtered order {} ({}) for {}",
1186                    external_order_id,
1187                    report.venue_order_id,
1188                    report.instrument_id,
1189                );
1190            } else {
1191                let external_order_id = report
1192                    .client_order_id
1193                    .map_or_else(|| report.venue_order_id.to_string(), |id| id.to_string());
1194                log::debug!(
1195                    "Filtered unclaimed external order {} ({}) for {}",
1196                    external_order_id,
1197                    report.venue_order_id,
1198                    report.instrument_id,
1199                );
1200            }
1201
1202            return None;
1203        }
1204
1205        self.materialize_external_order_from_status_with_strategy(report, strategy_id)
1206    }
1207
1208    fn materialize_external_order_from_status_with_strategy(
1209        &self,
1210        report: &OrderStatusReport,
1211        strategy_id: StrategyId,
1212    ) -> Option<OrderAny> {
1213        let client_order_id = report
1214            .client_order_id
1215            .unwrap_or_else(|| ClientOrderId::from(report.venue_order_id.as_str()));
1216
1217        let trader_id = get_message_bus().borrow().trader_id;
1218        let ts_now = self.clock.borrow().timestamp_ns();
1219
1220        let initialized = match OrderInitialized::new_checked(
1221            trader_id,
1222            strategy_id,
1223            report.instrument_id,
1224            client_order_id,
1225            report.order_side,
1226            report.order_type,
1227            report.quantity,
1228            report.time_in_force,
1229            report.post_only,
1230            report.reduce_only,
1231            false, // quote_quantity
1232            true,  // reconciliation
1233            UUID4::new(),
1234            ts_now,
1235            ts_now,
1236            report.price,
1237            report.activation_price,
1238            report.trigger_price,
1239            report.trigger_type,
1240            report.limit_offset,
1241            report.trailing_offset,
1242            Some(report.trailing_offset_type),
1243            report.expire_time,
1244            report.display_qty,
1245            None, // emulation_trigger
1246            None, // trigger_instrument_id
1247            Some(report.contingency_type),
1248            report.order_list_id,
1249            report.linked_order_ids.clone(),
1250            report.parent_order_id,
1251            None, // exec_algorithm_id
1252            None, // exec_algorithm_params
1253            None, // exec_spawn_id
1254            None, // tags
1255        ) {
1256            Ok(initialized) => initialized,
1257            Err(e) => {
1258                log::error!("Failed to create external order from report: {e}");
1259                return None;
1260            }
1261        };
1262
1263        self.materialize_external_order(
1264            initialized,
1265            client_order_id,
1266            report.venue_order_id,
1267            report.instrument_id,
1268            strategy_id,
1269            ts_now,
1270            Some(report.order_status),
1271            self.source_client_id_for_account(report.account_id, &report.instrument_id),
1272        )
1273    }
1274
1275    /// Builds and registers an external order from a [`FillReport`] when no matching
1276    /// order exists in cache. The order is created with `OrderType::Market` and a
1277    /// quantity equal to the fill's `last_qty`, so the fill consumes the entire
1278    /// order on application.
1279    ///
1280    /// This handles venue-initiated fills (most commonly Hyperliquid liquidations)
1281    /// where the venue does not surface a user-level order on its order channel.
1282    fn materialize_external_order_from_fill(&mut self, report: &FillReport) -> Option<OrderAny> {
1283        let strategy_id = self.resolve_external_strategy(&report.instrument_id);
1284        if self.should_filter_unclaimed_external_order(strategy_id) {
1285            self.filtered_unclaimed_external_order_count += 1;
1286
1287            let external_order_id = report
1288                .client_order_id
1289                .map_or_else(|| report.venue_order_id.to_string(), |id| id.to_string());
1290
1291            if self.filtered_unclaimed_external_order_count == 1 {
1292                log::info!(
1293                    "Filtering unclaimed external orders; first filtered fill {} ({}) for {}",
1294                    external_order_id,
1295                    report.venue_order_id,
1296                    report.instrument_id,
1297                );
1298            } else {
1299                log::debug!(
1300                    "Filtered unclaimed external fill {} ({}) for {}",
1301                    external_order_id,
1302                    report.venue_order_id,
1303                    report.instrument_id,
1304                );
1305            }
1306
1307            return None;
1308        }
1309
1310        let client_order_id = report
1311            .client_order_id
1312            .unwrap_or_else(|| ClientOrderId::from(report.venue_order_id.as_str()));
1313
1314        let trader_id = get_message_bus().borrow().trader_id;
1315        let ts_now = self.clock.borrow().timestamp_ns();
1316
1317        let initialized = OrderInitialized::new(
1318            trader_id,
1319            strategy_id,
1320            report.instrument_id,
1321            client_order_id,
1322            report.order_side,
1323            OrderType::Market,
1324            report.last_qty,
1325            TimeInForce::Ioc,
1326            false, // post_only
1327            true,  // reduce_only: venue-initiated closes always reduce
1328            false, // quote_quantity
1329            true,  // reconciliation
1330            UUID4::new(),
1331            ts_now,
1332            ts_now,
1333            None, // price
1334            None, // activation_price
1335            None, // trigger_price
1336            None, // trigger_type
1337            None, // limit_offset
1338            None, // trailing_offset
1339            Some(TrailingOffsetType::NoTrailingOffset),
1340            None, // expire_time
1341            None, // display_qty
1342            None, // emulation_trigger
1343            None, // trigger_instrument_id
1344            Some(ContingencyType::NoContingency),
1345            None, // order_list_id
1346            None, // linked_order_ids
1347            None, // parent_order_id
1348            None, // exec_algorithm_id
1349            None, // exec_algorithm_params
1350            None, // exec_spawn_id
1351            None, // tags
1352        );
1353
1354        self.materialize_external_order(
1355            initialized,
1356            client_order_id,
1357            report.venue_order_id,
1358            report.instrument_id,
1359            strategy_id,
1360            ts_now,
1361            None,
1362            self.source_client_id_for_account(report.account_id, &report.instrument_id),
1363        )
1364    }
1365
1366    fn resolve_external_strategy(&self, instrument_id: &InstrumentId) -> StrategyId {
1367        self.external_order_claims
1368            .get(instrument_id)
1369            .copied()
1370            .unwrap_or_else(StrategyId::external)
1371    }
1372
1373    fn should_filter_unclaimed_external_order(&self, strategy_id: StrategyId) -> bool {
1374        self.config.filter_unclaimed_external_orders && strategy_id.is_external()
1375    }
1376
1377    /// Adds an external order to the cache and registers it for adapter routing.
1378    /// Returns the registered order on success.
1379    #[allow(
1380        clippy::too_many_arguments,
1381        reason = "external order materialisation threads several ids and a timestamp"
1382    )]
1383    fn materialize_external_order(
1384        &self,
1385        initialized: OrderInitialized,
1386        client_order_id: ClientOrderId,
1387        venue_order_id: VenueOrderId,
1388        instrument_id: InstrumentId,
1389        strategy_id: StrategyId,
1390        ts_now: UnixNanos,
1391        order_status: Option<OrderStatus>,
1392        source_client_id: Option<ClientId>,
1393    ) -> Option<OrderAny> {
1394        let initialized = OrderEventAny::Initialized(initialized);
1395        let order = match OrderAny::from_events(vec![initialized.clone()]) {
1396            Ok(order) => order,
1397            Err(e) => {
1398                log::error!("Failed to create external order from report: {e}");
1399                return None;
1400            }
1401        };
1402
1403        {
1404            let mut cache = self.cache.borrow_mut();
1405            if let Err(e) = cache.add_venue_order_id(&client_order_id, &venue_order_id, false) {
1406                log::warn!("Failed to claim venue order ID for external order: {e}");
1407                return None;
1408            }
1409
1410            if let Err(e) = cache.add_order(order.clone(), None, source_client_id, false) {
1411                log::error!("Failed to add external order to cache: {e}");
1412                return None;
1413            }
1414        }
1415
1416        self.publish_order_event(&initialized);
1417
1418        match order_status {
1419            Some(status) => log::info!(
1420                "Created external order {client_order_id} ({venue_order_id}) for {instrument_id} [{status}]",
1421            ),
1422            None => log::info!(
1423                "Created external order {client_order_id} ({venue_order_id}) for {instrument_id}",
1424            ),
1425        }
1426
1427        self.register_external_order(
1428            client_order_id,
1429            venue_order_id,
1430            instrument_id,
1431            strategy_id,
1432            ts_now,
1433        );
1434
1435        Some(order)
1436    }
1437
1438    /// Resolves the execution client origin for a live-stream report by matching
1439    /// the report account against registered clients. A unique match stamps the
1440    /// materialized order's client origin; no match or an ambiguous match keeps
1441    /// the order origin-free.
1442    fn source_client_id_for_account(
1443        &self,
1444        account_id: AccountId,
1445        instrument_id: &InstrumentId,
1446    ) -> Option<ClientId> {
1447        let mut matches = self
1448            .clients
1449            .values()
1450            .filter(|adapter| {
1451                adapter.account_id == account_id && adapter.handles_order_venue(instrument_id.venue)
1452            })
1453            .map(|adapter| adapter.client_id);
1454
1455        let first = matches.next()?;
1456
1457        matches.next().is_none().then_some(first)
1458    }
1459
1460    /// Reconciles a fill report received at runtime.
1461    ///
1462    /// Finds the associated order, validates the fill, and generates an `OrderFilled` event
1463    /// if the fill is not a duplicate and won't cause an overfill. When the order is not
1464    /// in cache, an external order is bootstrapped from the fill so that venue-initiated
1465    /// closures (e.g. Hyperliquid liquidations) that arrive without a companion order
1466    /// status report still update the local position.
1467    pub fn reconcile_fill_report(&mut self, report: &FillReport) {
1468        msgbus::publish_any(
1469            MessagingSwitchboard::reconciliation_raw_fill_report_topic(),
1470            report,
1471        );
1472
1473        let cache = self.cache.borrow();
1474
1475        let order = report
1476            .client_order_id
1477            .and_then(|id| cache.order(&id).map(|o| o.clone()))
1478            .or_else(|| {
1479                cache
1480                    .client_order_id(&report.venue_order_id)
1481                    .and_then(|cid| cache.order(cid).map(|o| o.clone()))
1482            });
1483
1484        let instrument = cache.instrument(&report.instrument_id).cloned();
1485
1486        drop(cache);
1487
1488        let Some(instrument) = instrument else {
1489            log::debug!(
1490                "Cannot reconcile fill report for venue_order_id={}: instrument {} not found",
1491                report.venue_order_id,
1492                report.instrument_id
1493            );
1494            return;
1495        };
1496
1497        let order = match order {
1498            Some(order) => order,
1499            None => {
1500                let Some(order) = self.materialize_external_order_from_fill(report) else {
1501                    return;
1502                };
1503                let ts_now = self.clock.borrow().timestamp_ns();
1504                let accepted = OrderAccepted::new(
1505                    order.trader_id(),
1506                    order.strategy_id(),
1507                    order.instrument_id(),
1508                    order.client_order_id(),
1509                    report.venue_order_id,
1510                    report.account_id,
1511                    UUID4::new(),
1512                    report.ts_event,
1513                    ts_now,
1514                    true, // reconciliation
1515                );
1516                self.handle_event(&OrderEventAny::Accepted(accepted));
1517                self.cache
1518                    .borrow()
1519                    .order(&order.client_order_id())
1520                    .map(|o| o.clone())
1521                    .unwrap_or(order)
1522            }
1523        };
1524
1525        let ts_now = self.clock.borrow().timestamp_ns();
1526
1527        if let Some(event) = reconcile_fill(
1528            &order,
1529            report,
1530            &instrument,
1531            ts_now,
1532            self.config.allow_overfills,
1533        ) {
1534            self.handle_event(&event);
1535        }
1536    }
1537
1538    /// Reconciles an [`OrderStatusReport`] paired with companion [`FillReport`]s
1539    /// for the same venue event.
1540    ///
1541    /// Real fills supplied by the adapter are applied first so their `trade_id` and
1542    /// `commission` are preserved; any residual quantity not covered by the fills is
1543    /// then synthesised as an inferred fill from the status report's `avg_px`.
1544    /// Adapters use this to emit ADL / liquidation / settlement events without
1545    /// losing real fill metadata.
1546    pub fn reconcile_order_with_fills(&mut self, report: &OrderStatusReport, fills: &[FillReport]) {
1547        msgbus::publish_any(
1548            MessagingSwitchboard::reconciliation_raw_order_status_report_topic(),
1549            report,
1550        );
1551
1552        let fill_report_topic = MessagingSwitchboard::reconciliation_raw_fill_report_topic();
1553        for fill in fills {
1554            msgbus::publish_any(fill_report_topic, fill);
1555        }
1556
1557        let cache = self.cache.borrow();
1558        let order = report
1559            .client_order_id
1560            .and_then(|id| cache.order(&id).map(|o| o.clone()))
1561            .or_else(|| {
1562                cache
1563                    .client_order_id(&report.venue_order_id)
1564                    .and_then(|cid| cache.order(cid).map(|o| o.clone()))
1565            });
1566        let instrument = cache.instrument(&report.instrument_id).cloned();
1567        drop(cache);
1568
1569        let Some(instrument) = instrument else {
1570            log::debug!(
1571                "Cannot reconcile bundled report for venue_order_id={}: instrument {} not found",
1572                report.venue_order_id,
1573                report.instrument_id,
1574            );
1575
1576            if fills.is_empty()
1577                && let Some(order) = order
1578            {
1579                let ts_now = self.clock.borrow().timestamp_ns();
1580                let events =
1581                    generate_reconciliation_order_snapshot_events(&order, report, None, ts_now);
1582
1583                for event in &events {
1584                    self.handle_event(event);
1585                }
1586            }
1587            return;
1588        };
1589
1590        // Bootstrap the external order with only OrderAccepted; defer fill events to
1591        // the per-fill loop so real fill metadata is preserved.
1592        let mut order = match order {
1593            Some(order) => {
1594                let ts_now = self.clock.borrow().timestamp_ns();
1595                let events = generate_reconciliation_order_pre_fill_events(&order, report, ts_now);
1596                for event in &events {
1597                    self.handle_event(event);
1598                }
1599                self.cache
1600                    .borrow()
1601                    .order(&order.client_order_id())
1602                    .map(|o| o.clone())
1603                    .unwrap_or(order)
1604            }
1605            None => {
1606                let Some(order) = self.materialize_external_order_from_status(report) else {
1607                    return;
1608                };
1609                let ts_now = self.clock.borrow().timestamp_ns();
1610                let accepted = OrderAccepted::new(
1611                    order.trader_id(),
1612                    order.strategy_id(),
1613                    order.instrument_id(),
1614                    order.client_order_id(),
1615                    report.venue_order_id,
1616                    report.account_id,
1617                    UUID4::new(),
1618                    report.ts_accepted,
1619                    ts_now,
1620                    true, // reconciliation
1621                );
1622                self.handle_event(&OrderEventAny::Accepted(accepted));
1623                self.cache
1624                    .borrow()
1625                    .order(&order.client_order_id())
1626                    .map(|o| o.clone())
1627                    .unwrap_or(order)
1628            }
1629        };
1630
1631        let client_order_id = order.client_order_id();
1632
1633        for fill in fills {
1634            let ts_now = self.clock.borrow().timestamp_ns();
1635
1636            if let Some(event) = reconcile_fill(
1637                &order,
1638                fill,
1639                &instrument,
1640                ts_now,
1641                self.config.allow_overfills,
1642            ) {
1643                self.handle_event(&event);
1644            }
1645
1646            // Refresh order after fill to keep filled_qty accurate for the next iteration.
1647            if let Some(refreshed) = self
1648                .cache
1649                .borrow()
1650                .order(&client_order_id)
1651                .map(|o| o.clone())
1652            {
1653                order = refreshed;
1654            }
1655        }
1656
1657        let ts_now = self.clock.borrow().timestamp_ns();
1658        let events = generate_reconciliation_order_snapshot_events(
1659            &order,
1660            report,
1661            Some(&instrument),
1662            ts_now,
1663        );
1664
1665        for event in &events {
1666            self.handle_event(event);
1667        }
1668    }
1669
1670    /// Reconciles a position status report received at runtime.
1671    ///
1672    /// Compares the venue-reported position with cached positions and logs any discrepancies.
1673    /// Handles both hedging (with `venue_position_id`) and netting (without) modes.
1674    pub fn reconcile_position_report(&mut self, report: &PositionStatusReport) {
1675        msgbus::publish_any(
1676            MessagingSwitchboard::reconciliation_raw_position_status_report_topic(),
1677            report,
1678        );
1679
1680        let cache = self.cache.borrow();
1681
1682        let size_precision = cache
1683            .instrument(&report.instrument_id)
1684            .map(InstrumentAny::size_precision);
1685
1686        if report.venue_position_id.is_some() {
1687            self.reconcile_position_report_hedging(report, &cache);
1688        } else {
1689            self.reconcile_position_report_netting(report, &cache, size_precision);
1690        }
1691    }
1692
1693    fn reconcile_position_report_hedging(&self, report: &PositionStatusReport, cache: &Cache) {
1694        let venue_position_id = report.venue_position_id.as_ref().unwrap();
1695
1696        log::debug!(
1697            "Reconciling HEDGE position for {}, venue_position_id={}",
1698            report.instrument_id,
1699            venue_position_id
1700        );
1701
1702        let Some(position) = cache.position(venue_position_id) else {
1703            log::error!("Cannot reconcile position: {venue_position_id} not found in cache");
1704            return;
1705        };
1706
1707        let cached_signed_qty = match position.side {
1708            PositionSide::Long => position.quantity.as_decimal(),
1709            PositionSide::Short => -position.quantity.as_decimal(),
1710            _ => Decimal::ZERO,
1711        };
1712        let venue_signed_qty = report.signed_decimal_qty;
1713
1714        if cached_signed_qty != venue_signed_qty {
1715            log::error!(
1716                "Position mismatch for {} {}: cached={}, venue={}",
1717                report.instrument_id,
1718                venue_position_id,
1719                cached_signed_qty,
1720                venue_signed_qty
1721            );
1722        }
1723    }
1724
1725    fn reconcile_position_report_netting(
1726        &self,
1727        report: &PositionStatusReport,
1728        cache: &Cache,
1729        size_precision: Option<u8>,
1730    ) {
1731        log::debug!("Reconciling NET position for {}", report.instrument_id);
1732
1733        let positions_open = Self::netting_positions_open_for_report(cache, report);
1734
1735        let position_refs = positions_open
1736            .iter()
1737            .map(|position| &**position)
1738            .collect::<Vec<_>>();
1739
1740        if let Some(message) =
1741            Self::netting_split_position_ownership_message(report, &position_refs)
1742        {
1743            log::warn!("{message}");
1744        }
1745
1746        // Sum up cached position quantities using domain types to avoid f64 precision loss
1747        let cached_signed_qty: Decimal = positions_open
1748            .iter()
1749            .map(|position| Self::position_signed_decimal_qty(position))
1750            .sum();
1751
1752        log::debug!(
1753            "Position report: venue_signed_qty={}, cached_signed_qty={}",
1754            report.signed_decimal_qty,
1755            cached_signed_qty
1756        );
1757
1758        let _ = check_position_reconciliation(report, cached_signed_qty, size_precision);
1759    }
1760
1761    fn netting_positions_open_for_report<'a>(
1762        cache: &'a Cache,
1763        report: &PositionStatusReport,
1764    ) -> Vec<PositionRef<'a>> {
1765        cache.positions_open(
1766            None,
1767            Some(&report.instrument_id),
1768            None,
1769            Some(&report.account_id),
1770            None,
1771        )
1772    }
1773
1774    fn netting_split_position_ownership_message(
1775        report: &PositionStatusReport,
1776        positions_open: &[&Position],
1777    ) -> Option<String> {
1778        let mut strategy_ids = positions_open
1779            .iter()
1780            .map(|position| position.strategy_id.to_string())
1781            .collect::<Vec<_>>();
1782        strategy_ids.sort();
1783        strategy_ids.dedup();
1784
1785        if strategy_ids.len() <= 1 {
1786            return None;
1787        }
1788
1789        let position_details = Self::position_details(positions_open.iter().copied());
1790
1791        Some(format!(
1792            "NETTING reconciliation found split ownership for account_id={}, instrument_id={}: \
1793             strategies=[{}], positions=[{}]",
1794            report.account_id,
1795            report.instrument_id,
1796            strategy_ids.join(", "),
1797            position_details
1798        ))
1799    }
1800
1801    /// Reconciles an execution mass status report.
1802    ///
1803    /// Processes all order reports, fill reports, and position reports contained
1804    /// in the mass status. Order reports are paired with their companion fills so
1805    /// real trade IDs and commissions are applied before any residual inferred fill.
1806    pub fn reconcile_execution_mass_status(&mut self, mass_status: &ExecutionMassStatus) {
1807        self.report_count += 1;
1808
1809        log::info!(
1810            "Reconciling mass status for client={}, account={}, venue={}",
1811            mass_status.client_id,
1812            mass_status.account_id,
1813            mass_status.venue
1814        );
1815
1816        let order_reports = mass_status.order_reports();
1817        let fill_reports = mass_status.fill_reports();
1818        let mut paired_venue_ids = AHashSet::new();
1819
1820        for order_report in order_reports.values() {
1821            if let Some(fills) = fill_reports.get(&order_report.venue_order_id)
1822                && !fills.is_empty()
1823            {
1824                self.reconcile_order_with_fills(order_report, fills);
1825                paired_venue_ids.insert(order_report.venue_order_id);
1826            } else {
1827                self.reconcile_order_status_report(order_report);
1828            }
1829        }
1830
1831        for fill_reports in fill_reports.values() {
1832            for fill_report in fill_reports {
1833                if paired_venue_ids.contains(&fill_report.venue_order_id) {
1834                    continue;
1835                }
1836
1837                self.reconcile_fill_report(fill_report);
1838            }
1839        }
1840
1841        for position_reports in mass_status.position_reports().values() {
1842            for position_report in position_reports {
1843                self.reconcile_position_report(position_report);
1844            }
1845        }
1846
1847        log::info!(
1848            "Mass status reconciliation complete: {} orders, {} fills, {} positions",
1849            mass_status.order_reports().len(),
1850            mass_status
1851                .fill_reports()
1852                .values()
1853                .map(Vec::len)
1854                .sum::<usize>(),
1855            mass_status
1856                .position_reports()
1857                .values()
1858                .map(Vec::len)
1859                .sum::<usize>()
1860        );
1861    }
1862
1863    /// Executes a trading command by routing it to the appropriate execution client.
1864    pub fn execute(&self, command: TradingCommand) {
1865        self.execute_command(command);
1866    }
1867
1868    /// Processes an order event, updating internal state and routing as needed.
1869    pub fn process(&mut self, event: &OrderEventAny) {
1870        self.handle_event(event);
1871    }
1872
1873    /// Projects a reconciled fill onto its order without applying position or portfolio economics.
1874    pub fn project_reconciliation_fill(&mut self, fill: &OrderFilled) {
1875        self.handle_event_with_position_application(&OrderEventAny::Filled(fill.clone()), false);
1876    }
1877
1878    /// Starts the execution engine and all registered execution clients.
1879    pub fn start(&mut self) {
1880        for client in self.get_clients_mut() {
1881            if let Err(e) = client.start() {
1882                log::error!("{e}");
1883            }
1884        }
1885
1886        self.start_snapshot_timer();
1887        self.start_purge_timers();
1888
1889        log::info!("Started");
1890    }
1891
1892    /// Stops the execution engine and all registered execution clients.
1893    ///
1894    /// Adapters are expected to be idempotent on repeated `stop()` calls
1895    /// (e.g. via an internal `is_stopped` guard); the backtest teardown
1896    /// sequence calls `stop()` more than once per run.
1897    pub fn stop(&mut self) {
1898        for client in self.get_clients_mut() {
1899            if let Err(e) = client.stop() {
1900                log::error!("{e}");
1901            }
1902        }
1903
1904        self.stop_snapshot_timer();
1905        self.stop_purge_timers();
1906
1907        log::info!("Stopped");
1908    }
1909
1910    /// Stops all registered execution clients without stopping the engine itself.
1911    pub fn stop_clients(&mut self) {
1912        for client in self.get_clients_mut() {
1913            if let Err(e) = client.stop() {
1914                log::error!("{e}");
1915            }
1916        }
1917    }
1918
1919    /// Resets the execution engine and all registered execution clients to initial state.
1920    ///
1921    /// Cancels engine-owned timers (snapshot, purge) but leaves timers owned by
1922    /// other components on the shared clock untouched.
1923    pub fn reset(&mut self) {
1924        for client in self.get_clients_mut() {
1925            if let Err(e) = client.reset() {
1926                log::error!("{e}");
1927            }
1928        }
1929
1930        self.cache.borrow_mut().reset();
1931        self.pos_id_generator.reset();
1932
1933        self.stop_snapshot_timer();
1934        self.stop_purge_timers();
1935
1936        self.command_count.set(0);
1937        self.event_count = 0;
1938        self.report_count = 0;
1939        self.filtered_unclaimed_external_order_count = 0;
1940
1941        log::info!("Reset");
1942    }
1943
1944    /// Disposes of the execution engine, releasing resources from all clients and timers.
1945    ///
1946    /// Cancels engine-owned timers (snapshot, purge) but leaves timers owned by
1947    /// other components on the shared clock untouched.
1948    pub fn dispose(&mut self) {
1949        for client in self.get_clients_mut() {
1950            if let Err(e) = client.dispose() {
1951                log::error!("{e}");
1952            }
1953        }
1954
1955        self.stop_snapshot_timer();
1956        self.stop_purge_timers();
1957
1958        log::info!("Disposed");
1959    }
1960
1961    fn execute_command(&self, command: TradingCommand) {
1962        self.command_count.set(self.command_count.get() + 1);
1963
1964        if self.config.debug {
1965            log::debug!("{RECV}{CMD} {command:?}");
1966        }
1967
1968        if let Some(cid) = command.client_id()
1969            && self.external_clients.contains(&cid)
1970        {
1971            let topic = format!("commands.trading.{cid}");
1972            msgbus::publish_any(topic.into(), &command);
1973
1974            if self.config.debug {
1975                log::debug!("Skipping execution command for external client {cid}: {command:?}");
1976            }
1977            return;
1978        }
1979
1980        let client = if let Some(adapter) = self.find_client_for_command(&command) {
1981            adapter.client.as_ref()
1982        } else {
1983            let routing_context = Self::routing_context_for_command(&command);
1984
1985            log::error!(
1986                "No execution client found for command: client_id={:?}, {routing_context}, command={command:?}",
1987                command.client_id(),
1988            );
1989
1990            let reason = OrderDeniedReason::NoExecutionClient {
1991                client_id: command.client_id(),
1992                routing_context,
1993            }
1994            .to_string();
1995
1996            match command {
1997                TradingCommand::SubmitOrder(cmd) => {
1998                    let order = self
1999                        .cache
2000                        .borrow()
2001                        .order(&cmd.client_order_id)
2002                        .map(|o| o.clone());
2003
2004                    if let Some(order) = order {
2005                        self.deny_order(&order, &reason);
2006                    }
2007                }
2008                TradingCommand::SubmitOrderList(cmd) => {
2009                    let orders: Vec<OrderAny> = self
2010                        .cache
2011                        .borrow()
2012                        .orders_for_ids(&cmd.order_list.client_order_ids, &cmd);
2013
2014                    for order in &orders {
2015                        self.deny_order(order, &reason);
2016                    }
2017                }
2018                _ => {}
2019            }
2020
2021            return;
2022        };
2023
2024        match command {
2025            TradingCommand::SubmitOrder(cmd) => self.handle_submit_order(client, cmd),
2026            TradingCommand::SubmitOrderList(cmd) => self.handle_submit_order_list(client, cmd),
2027            TradingCommand::ModifyOrder(cmd) => self.handle_modify_order(client, cmd),
2028            TradingCommand::ModifyOrders(cmd) => self.handle_batch_modify_orders(client, cmd),
2029            TradingCommand::CancelOrder(cmd) => self.handle_cancel_order(client, cmd),
2030            TradingCommand::CancelOrders(cmd) => self.handle_batch_cancel_orders(client, cmd),
2031            TradingCommand::CancelAllOrders(cmd) => self.handle_cancel_all_orders(client, cmd),
2032            TradingCommand::QueryOrder(cmd) => self.handle_query_order(client, cmd),
2033            TradingCommand::QueryAccount(cmd) => self.handle_query_account(client, cmd),
2034        }
2035    }
2036
2037    fn routing_context_for_command(command: &TradingCommand) -> String {
2038        match command {
2039            TradingCommand::SubmitOrder(cmd) => format!("venue={}", cmd.instrument_id.venue),
2040            TradingCommand::SubmitOrderList(cmd) => format!("venue={}", cmd.instrument_id.venue),
2041            TradingCommand::ModifyOrder(cmd) => format!("venue={}", cmd.instrument_id.venue),
2042            TradingCommand::ModifyOrders(cmd) => format!("venue={}", cmd.instrument_id.venue),
2043            TradingCommand::CancelOrder(cmd) => format!("venue={}", cmd.instrument_id.venue),
2044            TradingCommand::CancelOrders(cmd) => format!("venue={}", cmd.instrument_id.venue),
2045            TradingCommand::CancelAllOrders(cmd) => format!("venue={}", cmd.instrument_id.venue),
2046            TradingCommand::QueryOrder(cmd) => format!("venue={}", cmd.instrument_id.venue),
2047            TradingCommand::QueryAccount(cmd) => {
2048                let issuer = cmd.account_id.get_issuer();
2049                format!("account_id={}, issuer={issuer}", cmd.account_id)
2050            }
2051        }
2052    }
2053
2054    fn find_client_for_command(&self, command: &TradingCommand) -> Option<&ExecutionClientAdapter> {
2055        if let Some(client_id) = command.client_id()
2056            && let Some(adapter) = self.clients.get(&client_id)
2057        {
2058            return Some(adapter);
2059        }
2060
2061        if let Some(account_id) = self.account_id_for_command(command) {
2062            let issuer = account_id.get_issuer();
2063            let issuer_client_id = ClientId::from(issuer.as_str());
2064
2065            if let Some(adapter) = self.clients.get(&issuer_client_id) {
2066                return Some(adapter);
2067            }
2068
2069            if let Some(client_id) = self.routing_map.get(&issuer)
2070                && let Some(adapter) = self.clients.get(client_id)
2071            {
2072                return Some(adapter);
2073            }
2074        }
2075
2076        if let Some(instrument_id) = Self::instrument_id_for_command(command)
2077            && let Some(client_id) = self.routing_map.get(&instrument_id.venue)
2078            && let Some(adapter) = self.clients.get(client_id)
2079        {
2080            return Some(adapter);
2081        }
2082
2083        self.default_client_id.and_then(|id| self.clients.get(&id))
2084    }
2085
2086    fn account_id_for_command(&self, command: &TradingCommand) -> Option<AccountId> {
2087        match command {
2088            TradingCommand::QueryAccount(cmd) => Some(cmd.account_id),
2089            TradingCommand::SubmitOrder(cmd) => self
2090                .cache
2091                .borrow()
2092                .order(&cmd.client_order_id)
2093                .and_then(|order| order.account_id()),
2094            TradingCommand::ModifyOrder(cmd) => self
2095                .cache
2096                .borrow()
2097                .order(&cmd.client_order_id)
2098                .and_then(|order| order.account_id()),
2099            TradingCommand::CancelOrder(cmd) => self
2100                .cache
2101                .borrow()
2102                .order(&cmd.client_order_id)
2103                .and_then(|order| order.account_id()),
2104            TradingCommand::SubmitOrderList(_)
2105            | TradingCommand::ModifyOrders(_)
2106            | TradingCommand::CancelOrders(_)
2107            | TradingCommand::CancelAllOrders(_)
2108            | TradingCommand::QueryOrder(_) => None,
2109        }
2110    }
2111
2112    const fn instrument_id_for_command(command: &TradingCommand) -> Option<InstrumentId> {
2113        match command {
2114            TradingCommand::SubmitOrder(cmd) => Some(cmd.instrument_id),
2115            TradingCommand::SubmitOrderList(cmd) => Some(cmd.instrument_id),
2116            TradingCommand::ModifyOrder(cmd) => Some(cmd.instrument_id),
2117            TradingCommand::ModifyOrders(cmd) => Some(cmd.instrument_id),
2118            TradingCommand::CancelOrder(cmd) => Some(cmd.instrument_id),
2119            TradingCommand::CancelOrders(cmd) => Some(cmd.instrument_id),
2120            TradingCommand::CancelAllOrders(cmd) => Some(cmd.instrument_id),
2121            TradingCommand::QueryOrder(cmd) => Some(cmd.instrument_id),
2122            TradingCommand::QueryAccount(_) => None,
2123        }
2124    }
2125
2126    fn handle_submit_order(&self, client: &dyn ExecutionClient, cmd: SubmitOrder) {
2127        let client_order_id = cmd.client_order_id;
2128        let cached_order = { self.cache.borrow().order_owned(&client_order_id) };
2129
2130        let (order, added_to_cache) = match cached_order {
2131            Some(order) => (order, false),
2132            None => {
2133                let Some(order) = self.add_order_from_init(&cmd.order_init, cmd.position_id, &cmd)
2134                else {
2135                    return;
2136                };
2137
2138                (order, true)
2139            }
2140        };
2141
2142        if added_to_cache && self.config.snapshot_orders {
2143            self.create_order_state_snapshot(&order);
2144        }
2145
2146        let order_venue = order.instrument_id().venue;
2147        let client_venue = client.venue();
2148        if !client.handles_order_venue(order_venue) {
2149            let client_id = client.client_id();
2150            let reason = OrderDeniedReason::ClientVenueMismatch {
2151                client_id,
2152                order_venue,
2153                client_venue,
2154            }
2155            .to_string();
2156            self.deny_order(&order, &reason);
2157            return;
2158        }
2159
2160        if let Some(reason) = self.check_position_id_against_oms(
2161            cmd.instrument_id,
2162            cmd.strategy_id,
2163            cmd.position_id,
2164            client,
2165        ) {
2166            self.deny_order(&order, &reason.to_string());
2167            return;
2168        }
2169
2170        let instrument_id = order.instrument_id();
2171
2172        if !added_to_cache && self.config.snapshot_orders {
2173            self.create_order_state_snapshot(&order);
2174        }
2175
2176        {
2177            let cache = self.cache.borrow();
2178            if cache.instrument(&instrument_id).is_none() {
2179                log::error!(
2180                    "Cannot handle submit order: no instrument found for {instrument_id}, {cmd}",
2181                );
2182                return;
2183            }
2184        }
2185
2186        let client_id = client.client_id();
2187        let claim_result = self
2188            .cache
2189            .borrow_mut()
2190            .claim_order_clients(&[(client_order_id, client_id)]);
2191
2192        if let Err(e) = claim_result {
2193            self.deny_order(
2194                &order,
2195                &OrderDeniedReason::ValidationFailed {
2196                    detail: format!(
2197                        "Failed to claim execution client {client_id} for {client_order_id}: {e}"
2198                    ),
2199                }
2200                .to_string(),
2201            );
2202            return;
2203        }
2204
2205        if self.config.manage_own_order_books && should_handle_own_book_order(&order) {
2206            let mut own_book = self.get_or_init_own_order_book(&order.instrument_id());
2207            own_book.add(order.to_own_book_order());
2208        }
2209
2210        log_info!("Submit {order}", color = LogColor::Blue);
2211
2212        if let Err(e) = client.submit_order(cmd) {
2213            self.deny_order(
2214                &order,
2215                &OrderDeniedReason::SubmitFailed {
2216                    detail: e.to_string(),
2217                }
2218                .to_string(),
2219            );
2220        }
2221    }
2222
2223    fn handle_submit_order_list(&self, client: &dyn ExecutionClient, cmd: SubmitOrderList) {
2224        let mut orders = Vec::with_capacity(cmd.order_list.client_order_ids.len());
2225        let mut added_client_order_ids = AHashSet::new();
2226
2227        for client_order_id in &cmd.order_list.client_order_ids {
2228            let cached_order = { self.cache.borrow().order_owned(client_order_id) };
2229
2230            if let Some(order) = cached_order {
2231                orders.push(order);
2232                continue;
2233            }
2234
2235            let Some(order_init) = cmd
2236                .order_inits
2237                .iter()
2238                .find(|init| init.client_order_id == *client_order_id)
2239            else {
2240                log::error!(
2241                    "Cannot handle submit order list: order not found in cache and no initialization event for {client_order_id}, {cmd}"
2242                );
2243                continue;
2244            };
2245
2246            let Some(order) = self.add_order_from_init(order_init, cmd.position_id, &cmd) else {
2247                continue;
2248            };
2249
2250            added_client_order_ids.insert(order.client_order_id());
2251            orders.push(order);
2252        }
2253
2254        if self.config.snapshot_orders {
2255            for order in &orders {
2256                if added_client_order_ids.contains(&order.client_order_id()) {
2257                    self.create_order_state_snapshot(order);
2258                }
2259            }
2260        }
2261
2262        if orders.len() != cmd.order_list.client_order_ids.len() {
2263            let reason = OrderDeniedReason::OrderListIncomplete {
2264                order_list_id: cmd.order_list.id,
2265            }
2266            .to_string();
2267
2268            for order in &orders {
2269                self.deny_order(order, &reason);
2270            }
2271            return;
2272        }
2273
2274        let order_list_venue = cmd.instrument_id.venue;
2275        let client_venue = client.venue();
2276        if !client.handles_order_venue(order_list_venue) {
2277            let client_id = client.client_id();
2278            let reason = OrderDeniedReason::ClientVenueMismatch {
2279                client_id,
2280                order_venue: order_list_venue,
2281                client_venue,
2282            }
2283            .to_string();
2284
2285            for order in &orders {
2286                self.deny_order(order, &reason);
2287            }
2288            return;
2289        }
2290
2291        let is_uniform_instrument = orders
2292            .iter()
2293            .all(|o| o.instrument_id() == cmd.instrument_id);
2294
2295        if let Some(position_id) = cmd.position_id
2296            && !is_uniform_instrument
2297        {
2298            let reason = OrderDeniedReason::InvalidPositionId {
2299                position_id,
2300                detail: "not valid for a mixed-instrument order list; a position belongs to a single instrument"
2301                    .to_string(),
2302            }
2303            .to_string();
2304
2305            for order in &orders {
2306                self.deny_order(order, &reason);
2307            }
2308            return;
2309        }
2310
2311        if let Some(reason) = self.check_position_id_against_oms(
2312            cmd.instrument_id,
2313            cmd.strategy_id,
2314            cmd.position_id,
2315            client,
2316        ) {
2317            let reason = reason.to_string();
2318            for order in &orders {
2319                self.deny_order(order, &reason);
2320            }
2321            return;
2322        }
2323
2324        if self.config.snapshot_orders {
2325            for order in &orders {
2326                if !added_client_order_ids.contains(&order.client_order_id()) {
2327                    self.create_order_state_snapshot(order);
2328                }
2329            }
2330        }
2331
2332        {
2333            let cache = self.cache.borrow();
2334            if cache.instrument(&cmd.instrument_id).is_none() {
2335                log::error!(
2336                    "Cannot handle submit order list: no instrument found for {}, {cmd}",
2337                    cmd.instrument_id,
2338                );
2339                return;
2340            }
2341        }
2342
2343        let client_id = client.client_id();
2344        let claims = orders
2345            .iter()
2346            .map(|order| (order.client_order_id(), client_id))
2347            .collect::<Vec<_>>();
2348        let claim_result = self.cache.borrow_mut().claim_order_clients(&claims);
2349        if let Err(e) = claim_result {
2350            let reason = OrderDeniedReason::ValidationFailed {
2351                detail: format!(
2352                    "Failed to claim execution client {client_id} for order list {}: {e}",
2353                    cmd.order_list.id,
2354                ),
2355            }
2356            .to_string();
2357
2358            for order in &orders {
2359                self.deny_order(order, &reason);
2360            }
2361            return;
2362        }
2363
2364        if self.config.manage_own_order_books {
2365            for order in &orders {
2366                if should_handle_own_book_order(order) {
2367                    let mut own_book = self.get_or_init_own_order_book(&order.instrument_id());
2368                    own_book.add(order.to_own_book_order());
2369                }
2370            }
2371        }
2372
2373        log_info!("Submit {}", cmd.order_list, color = LogColor::Blue);
2374
2375        if let Err(e) = client.submit_order_list(cmd) {
2376            log::error!("Error submitting order list to client: {e}");
2377            let reason = OrderDeniedReason::SubmitFailed {
2378                detail: e.to_string(),
2379            }
2380            .to_string();
2381
2382            for order in &orders {
2383                self.deny_order(order, &reason);
2384            }
2385        }
2386    }
2387
2388    fn add_order_from_init(
2389        &self,
2390        order_init: &OrderInitialized,
2391        position_id: Option<PositionId>,
2392        context: &dyn Display,
2393    ) -> Option<OrderAny> {
2394        let client_order_id = order_init.client_order_id;
2395        let order = match OrderAny::from_events(vec![OrderEventAny::Initialized(
2396            order_init.clone(),
2397        )]) {
2398            Ok(order) => order,
2399            Err(e) => {
2400                log::error!(
2401                    "Cannot reconstruct order from initialization event for {client_order_id}: {e}, {context}"
2402                );
2403                return None;
2404            }
2405        };
2406
2407        if let Err(e) = self
2408            .cache
2409            .borrow_mut()
2410            .add_order(order.clone(), position_id, None, true)
2411        {
2412            log::error!(
2413                "Cannot add reconstructed order to cache for {client_order_id}: {e}, {context}"
2414            );
2415            return None;
2416        }
2417
2418        Some(order)
2419    }
2420
2421    fn handle_modify_order(&self, client: &dyn ExecutionClient, cmd: ModifyOrder) {
2422        let venue_str = cmd
2423            .venue_order_id
2424            .map_or_else(String::new, |venue_order_id| format!(" {venue_order_id}"));
2425
2426        log_info!(
2427            "Modify {}{venue_str}",
2428            cmd.client_order_id,
2429            color = LogColor::Blue
2430        );
2431
2432        if let Err(e) = client.modify_order(cmd) {
2433            log::error!("Error modifying order: {e}");
2434        }
2435    }
2436
2437    fn handle_batch_modify_orders(&self, client: &dyn ExecutionClient, cmd: BatchModifyOrders) {
2438        if let Err(e) = client.batch_modify_orders(cmd) {
2439            log::error!("Error batch modifying orders: {e}");
2440        }
2441    }
2442
2443    fn handle_cancel_order(&self, client: &dyn ExecutionClient, cmd: CancelOrder) {
2444        let venue_str = cmd
2445            .venue_order_id
2446            .map_or_else(String::new, |venue_order_id| format!(" {venue_order_id}"));
2447
2448        log_info!(
2449            "Cancel {}{venue_str}",
2450            cmd.client_order_id,
2451            color = LogColor::Blue
2452        );
2453
2454        if let Err(e) = client.cancel_order(cmd) {
2455            log::error!("Error canceling order: {e}");
2456        }
2457    }
2458
2459    fn handle_cancel_all_orders(&self, client: &dyn ExecutionClient, cmd: CancelAllOrders) {
2460        let side_str = match cmd.order_side {
2461            OrderSide::NoOrderSide => " ".to_string(),
2462            order_side => format!(" {order_side} "),
2463        };
2464
2465        log_info!("Cancel all{side_str}orders", color = LogColor::Blue);
2466
2467        if let Err(e) = client.cancel_all_orders(cmd) {
2468            log::error!("Error canceling all orders: {e}");
2469        }
2470    }
2471
2472    fn handle_batch_cancel_orders(&self, client: &dyn ExecutionClient, cmd: BatchCancelOrders) {
2473        let client_order_ids: Vec<ClientOrderId> = cmd
2474            .cancels
2475            .iter()
2476            .map(|cancel| cancel.client_order_id)
2477            .collect();
2478
2479        log_info!(
2480            "Batch cancel orders {client_order_ids:?}",
2481            color = LogColor::Blue
2482        );
2483
2484        if let Err(e) = client.batch_cancel_orders(cmd) {
2485            log::error!("Error batch canceling orders: {e}");
2486        }
2487    }
2488
2489    fn handle_query_account(&self, client: &dyn ExecutionClient, cmd: QueryAccount) {
2490        log_info!("Query {}", cmd.account_id, color = LogColor::Blue);
2491
2492        if let Err(e) = client.query_account(cmd) {
2493            log::warn!("Error querying account: {e}");
2494        }
2495    }
2496
2497    fn handle_query_order(&self, client: &dyn ExecutionClient, cmd: QueryOrder) {
2498        log_info!("Query {}", cmd.client_order_id, color = LogColor::Blue);
2499
2500        if let Err(e) = client.query_order(cmd) {
2501            log::warn!("Error querying order: {e}");
2502        }
2503    }
2504
2505    fn create_order_state_snapshot(&self, order: &OrderAny) {
2506        if self.config.debug {
2507            log::debug!("Creating order state snapshot for {order}");
2508        }
2509
2510        if self.cache.borrow().has_backing()
2511            && let Err(e) = self.cache.borrow().snapshot_order_state(order)
2512        {
2513            log::warn!("Failed to snapshot order state: {e}");
2514        }
2515    }
2516
2517    fn create_position_state_snapshot(&self, position: &Position, open_only: bool) {
2518        Self::publish_position_state_snapshot(
2519            &self.clock,
2520            &self.cache,
2521            self.config.debug,
2522            position,
2523            open_only,
2524        );
2525    }
2526
2527    fn publish_position_state_snapshot(
2528        clock: &Rc<RefCell<dyn Clock>>,
2529        cache: &Rc<RefCell<Cache>>,
2530        debug: bool,
2531        position: &Position,
2532        open_only: bool,
2533    ) {
2534        if debug {
2535            log::debug!("Creating position state snapshot for {position}");
2536        }
2537
2538        let ts_snapshot = clock.borrow().timestamp_ns();
2539        let unrealized_pnl = cache.borrow().calculate_unrealized_pnl(position);
2540
2541        let snapshot = PositionStateSnapshot {
2542            position: position.clone(),
2543            unrealized_pnl,
2544            ts_snapshot,
2545        };
2546
2547        let topic = switchboard::get_snapshot_position_topic(position.id);
2548        msgbus::publish_any(topic, &snapshot);
2549
2550        let has_backing = cache.borrow().has_backing();
2551        if has_backing
2552            && let Err(e) = cache.borrow_mut().snapshot_position_state(
2553                position,
2554                ts_snapshot,
2555                unrealized_pnl,
2556                Some(open_only),
2557            )
2558        {
2559            log::warn!("Failed to snapshot position state: {e}");
2560        }
2561    }
2562
2563    fn handle_event(&mut self, event: &OrderEventAny) {
2564        self.handle_event_with_position_application(event, true);
2565    }
2566
2567    fn handle_event_with_position_application(
2568        &mut self,
2569        event: &OrderEventAny,
2570        apply_position: bool,
2571    ) {
2572        self.event_count += 1;
2573
2574        if self.config.debug {
2575            log::debug!("{RECV}{EVT} {event:?}");
2576        }
2577
2578        let event_client_order_id = event.client_order_id();
2579        let cache = self.cache.borrow();
2580        let client_order_id = if cache.order_exists(&event_client_order_id) {
2581            event_client_order_id
2582        } else {
2583            let is_leg_fill =
2584                matches!(event, OrderEventAny::Filled(fill) if self.is_leg_fill(fill));
2585            if !is_leg_fill {
2586                log::warn!(
2587                    "Order with {} not found in the cache to apply {}",
2588                    event.client_order_id(),
2589                    event
2590                );
2591            }
2592
2593            // Try to find order by venue order ID if available
2594            let venue_order_id = if let Some(id) = event.venue_order_id() {
2595                id
2596            } else {
2597                log::error!(
2598                    "Cannot apply event to any order: {} not found in the cache with no VenueOrderId",
2599                    event.client_order_id()
2600                );
2601                return;
2602            };
2603
2604            // Look up client order ID from venue order ID
2605            let client_order_id = if let Some(id) = cache.client_order_id(&venue_order_id) {
2606                *id
2607            } else {
2608                if let OrderEventAny::Filled(fill) = event
2609                    && is_leg_fill
2610                {
2611                    log::info!(
2612                        "Processing leg fill without corresponding order: {} for instrument {}",
2613                        fill.client_order_id,
2614                        fill.instrument_id
2615                    );
2616                    drop(cache);
2617                    self.handle_leg_fill_without_order(fill.clone());
2618                    return;
2619                }
2620
2621                log::error!(
2622                    "Cannot apply event to any order: {} and {venue_order_id} not found in the cache",
2623                    event.client_order_id(),
2624                );
2625                return;
2626            };
2627
2628            // Get order using found client order ID
2629            if cache.order_exists(&client_order_id) {
2630                log::info!("Order with {client_order_id} was found in the cache");
2631                client_order_id
2632            } else {
2633                if let OrderEventAny::Filled(fill) = event
2634                    && is_leg_fill
2635                {
2636                    log::info!(
2637                        "Processing leg fill without corresponding order: {} for instrument {}",
2638                        fill.client_order_id,
2639                        fill.instrument_id
2640                    );
2641                    drop(cache);
2642                    self.handle_leg_fill_without_order(fill.clone());
2643                    return;
2644                }
2645
2646                log::error!(
2647                    "Cannot apply event to any order: {client_order_id} and {venue_order_id} not found in cache",
2648                );
2649                return;
2650            }
2651        };
2652        let order_before_fill = if matches!(event, OrderEventAny::Filled(_)) {
2653            cache.order(&client_order_id).map(|o| o.clone())
2654        } else {
2655            None
2656        };
2657
2658        drop(cache);
2659
2660        let event = if event_client_order_id == client_order_id {
2661            event.clone()
2662        } else {
2663            event.clone().with_client_order_id(client_order_id)
2664        };
2665
2666        match &event {
2667            OrderEventAny::Filled(fill) => {
2668                let Some(order_before_fill) = order_before_fill else {
2669                    log::error!(
2670                        "Cannot apply fill: order {} not found in the cache",
2671                        fill.client_order_id()
2672                    );
2673                    return;
2674                };
2675                let configured_oms_type = self.determine_oms_type(fill);
2676                let position_id =
2677                    self.determine_position_id(fill, configured_oms_type, Some(&order_before_fill));
2678                let oms_type = self
2679                    .cache
2680                    .borrow()
2681                    .oms_type(&position_id)
2682                    .unwrap_or(configured_oms_type);
2683
2684                let mut fill = fill.clone();
2685                fill.position_id = Some(position_id);
2686
2687                let validation = if apply_position {
2688                    self.validate_fill_for_order(&order_before_fill, &fill)
2689                } else {
2690                    self.validate_fill_for_order_projection(&order_before_fill, &fill)
2691                };
2692
2693                if validation.is_ok() {
2694                    let event = OrderEventAny::Filled(fill.clone());
2695                    let Some(order) =
2696                        self.update_cached_order(client_order_id, &event, apply_position)
2697                    else {
2698                        return;
2699                    };
2700
2701                    let position_events = if apply_position {
2702                        self.handle_order_fill(&order, fill, oms_type)
2703                    } else {
2704                        Vec::new()
2705                    };
2706                    self.publish_order_event(&event);
2707                    self.publish_position_events(position_events);
2708                }
2709            }
2710            OrderEventAny::FillVoided(voided) => {
2711                let mut voided = voided.clone();
2712                let Some(order_before_void) = self
2713                    .cache
2714                    .borrow()
2715                    .order(&client_order_id)
2716                    .map(|order| order.clone())
2717                else {
2718                    log::error!("Cannot apply fill void: order {client_order_id} not found");
2719                    return;
2720                };
2721                let original_fill = order_before_void
2722                    .events()
2723                    .into_iter()
2724                    .find_map(|candidate| match candidate {
2725                        OrderEventAny::Filled(fill) if fill.trade_id == voided.trade_id => {
2726                            Some(fill.clone())
2727                        }
2728                        _ => None,
2729                    });
2730
2731                if voided.position_id.is_none() {
2732                    voided.position_id = original_fill.as_ref().and_then(|fill| fill.position_id);
2733                }
2734                let event = OrderEventAny::FillVoided(voided.clone());
2735
2736                let mut validated_order = order_before_void.clone();
2737                match validated_order.apply(event.clone()) {
2738                    Ok(()) => {}
2739                    Err(OrderError::DuplicateFillVoid(trade_id)) => {
2740                        log::warn!(
2741                            "Duplicate fill void rejected at order level: trade_id={trade_id}"
2742                        );
2743                        return;
2744                    }
2745                    Err(e) => {
2746                        log::error!("Cannot apply fill void to order: {e}");
2747                        return;
2748                    }
2749                }
2750
2751                let corrected_positions = if apply_position
2752                    && original_fill
2753                        .as_ref()
2754                        .is_some_and(|fill| fill.position_id.is_some())
2755                {
2756                    match self.prepare_order_fill_void_positions(&order_before_void, &voided) {
2757                        Ok(positions) => positions,
2758                        Err(e) => {
2759                            log::error!("Cannot apply fill void to positions: {e}");
2760                            return;
2761                        }
2762                    }
2763                } else {
2764                    Vec::new()
2765                };
2766
2767                let mut position_events = Vec::new();
2768
2769                for CorrectedPosition {
2770                    position,
2771                    corrected_qty,
2772                    absorbed_prior_cycles,
2773                    closed_cycles_pnl,
2774                } in corrected_positions
2775                {
2776                    if let Err(e) = self.cache.borrow_mut().update_position(&position) {
2777                        log::error!("Cannot apply fill void to position {}: {e}", position.id);
2778                        return;
2779                    }
2780
2781                    if absorbed_prior_cycles {
2782                        log::info!(
2783                            "Settling archived NETTING cycles rebuilt by fill void {} for position {}: realized={closed_cycles_pnl:?}",
2784                            voided.trade_id,
2785                            position.id,
2786                        );
2787
2788                        self.cache
2789                            .borrow_mut()
2790                            .settle_position_snapshots(&position, closed_cycles_pnl);
2791                    }
2792
2793                    if self.config.snapshot_positions {
2794                        self.create_position_state_snapshot(&position, false);
2795                    }
2796
2797                    position_events.push(Self::create_fill_void_position_event(
2798                        &position,
2799                        &voided,
2800                        corrected_qty,
2801                    ));
2802                }
2803
2804                if self
2805                    .update_cached_order(client_order_id, &event, true)
2806                    .is_none()
2807                {
2808                    return;
2809                }
2810
2811                if original_fill.is_some() {
2812                    let portfolio_endpoint = MessagingSwitchboard::portfolio_update_order();
2813                    msgbus::send_order_event(portfolio_endpoint, event.clone());
2814                }
2815                self.publish_order_event(&event);
2816                self.publish_position_events(position_events);
2817            }
2818            _ => {
2819                if self
2820                    .update_cached_order(client_order_id, &event, true)
2821                    .is_some()
2822                {
2823                    self.publish_order_event(&event);
2824                }
2825            }
2826        }
2827    }
2828
2829    fn handle_leg_fill_without_order(&mut self, mut fill: OrderFilled) {
2830        let instrument =
2831            if let Some(instrument) = self.cache.borrow().instrument(&fill.instrument_id) {
2832                instrument.clone()
2833            } else {
2834                log::error!(
2835                    "Cannot handle leg fill: no instrument found for {}, {fill}",
2836                    fill.instrument_id,
2837                );
2838                return;
2839            };
2840
2841        if let Err(e) = self.cache.borrow().try_account(&fill.account_id) {
2842            log::error!("Cannot handle leg fill: {e}, {fill}");
2843            return;
2844        }
2845
2846        let oms_type = self.determine_oms_type(&fill);
2847        let position_id = self.determine_leg_fill_position_id(&fill, oms_type);
2848        fill.position_id = Some(position_id);
2849        let duplicate_position_fill = self.position_contains_trade_id(position_id, fill.trade_id);
2850
2851        let event = OrderEventAny::Filled(fill.clone());
2852
2853        if duplicate_position_fill {
2854            log::warn!(
2855                "Duplicate leg fill: {} trade_id={} already applied to position {}, skipping",
2856                fill.client_order_id,
2857                fill.trade_id,
2858                position_id
2859            );
2860            return;
2861        }
2862
2863        let portfolio_endpoint = MessagingSwitchboard::portfolio_update_order();
2864        msgbus::send_order_event(portfolio_endpoint, event.clone());
2865        let position_events = self.handle_position_update(&instrument, fill, oms_type);
2866        self.publish_order_event(&event);
2867        self.publish_position_events(position_events);
2868    }
2869
2870    fn determine_leg_fill_position_id(
2871        &mut self,
2872        fill: &OrderFilled,
2873        oms_type: OmsType,
2874    ) -> PositionId {
2875        let cache = self.cache.borrow();
2876        let cached_position_id = cache.position_id(&fill.client_order_id()).copied();
2877        drop(cache);
2878
2879        if let Some(position_id) = cached_position_id {
2880            if let Some(fill_position_id) = fill.position_id
2881                && fill_position_id != position_id
2882            {
2883                log::warn!(
2884                    "Incorrect position ID assigned to leg fill: \
2885                     cached={position_id}, assigned={fill_position_id}; \
2886                     re-assigning from cache",
2887                );
2888            }
2889
2890            return position_id;
2891        }
2892
2893        match oms_type {
2894            OmsType::Hedging => self
2895                .orderless_hedging_leg_position_id(fill)
2896                .or(fill.position_id)
2897                .unwrap_or_else(|| self.pos_id_generator.generate(fill.strategy_id, false)),
2898            OmsType::Netting => self.determine_netting_position_id(fill),
2899            _ => self.determine_netting_position_id(fill),
2900        }
2901    }
2902
2903    fn orderless_hedging_leg_position_id(&self, fill: &OrderFilled) -> Option<PositionId> {
2904        if !self.is_leg_fill(fill) {
2905            return None;
2906        }
2907
2908        let cache = self.cache.borrow();
2909        if cache.order_exists(&fill.client_order_id()) {
2910            return None;
2911        }
2912
2913        let matching_positions: Vec<PositionId> = cache
2914            .positions_open(
2915                Some(&fill.instrument_id.venue),
2916                Some(&fill.instrument_id),
2917                Some(&fill.strategy_id),
2918                Some(&fill.account_id),
2919                None,
2920            )
2921            .iter()
2922            .filter(|position| position.opening_order_id == fill.client_order_id)
2923            .map(|position| position.id)
2924            .collect();
2925
2926        match matching_positions.as_slice() {
2927            [position_id] => Some(*position_id),
2928            [] => None,
2929            _ => {
2930                log::warn!(
2931                    "Cannot uniquely correlate HEDGING leg fill {} to an orderless position: \
2932                     found {} positions with opening_order_id={}",
2933                    fill.trade_id,
2934                    matching_positions.len(),
2935                    fill.client_order_id,
2936                );
2937                None
2938            }
2939        }
2940    }
2941
2942    fn is_leg_fill(&self, fill: &OrderFilled) -> bool {
2943        if !fill.client_order_id.as_str().contains("-LEG-")
2944            && !fill.venue_order_id.as_str().contains("-LEG-")
2945        {
2946            return false;
2947        }
2948
2949        self.cache
2950            .borrow()
2951            .instrument(&fill.instrument_id)
2952            .is_some_and(|instrument| !instrument.is_spread())
2953    }
2954
2955    fn determine_oms_type(&self, fill: &OrderFilled) -> OmsType {
2956        if let Some(oms_type) = self.oms_overrides.get(&fill.strategy_id)
2957            && *oms_type != OmsType::Unspecified
2958        {
2959            return *oms_type;
2960        }
2961
2962        if let Some(client_id) = self.routing_map.get(&fill.instrument_id.venue)
2963            && let Some(client) = self.clients.get(client_id)
2964        {
2965            return client.oms_type;
2966        }
2967
2968        if let Some(client) = self.default_client_id.and_then(|id| self.clients.get(&id)) {
2969            return client.oms_type;
2970        }
2971
2972        OmsType::Netting // Default fallback
2973    }
2974
2975    fn resolve_oms_type_for_client(
2976        &self,
2977        strategy_id: StrategyId,
2978        client: &dyn ExecutionClient,
2979    ) -> OmsType {
2980        if let Some(oms_type) = self.oms_overrides.get(&strategy_id)
2981            && *oms_type != OmsType::Unspecified
2982        {
2983            return *oms_type;
2984        }
2985
2986        client.oms_type()
2987    }
2988
2989    fn check_position_id_against_oms(
2990        &self,
2991        instrument_id: InstrumentId,
2992        strategy_id: StrategyId,
2993        position_id: Option<PositionId>,
2994        client: &dyn ExecutionClient,
2995    ) -> Option<OrderDeniedReason> {
2996        let position_id = position_id?;
2997
2998        if self.resolve_oms_type_for_client(strategy_id, client) != OmsType::Netting {
2999            return None;
3000        }
3001
3002        let expected = format!("{instrument_id}-{strategy_id}");
3003        if position_id.as_str() == expected {
3004            return None;
3005        }
3006
3007        Some(OrderDeniedReason::InvalidPositionId {
3008            position_id,
3009            detail: format!(
3010                "not valid for NETTING OMS; expected '{expected}' (use HEDGING for custom position IDs)"
3011            ),
3012        })
3013    }
3014
3015    fn determine_position_id(
3016        &mut self,
3017        fill: &OrderFilled,
3018        oms_type: OmsType,
3019        order: Option<&OrderAny>,
3020    ) -> PositionId {
3021        let cache = self.cache.borrow();
3022        let cached_position_id = cache.position_id(&fill.client_order_id()).copied();
3023        drop(cache);
3024
3025        if self.config.debug {
3026            log::debug!(
3027                "Determining position ID for {}, position_id={:?}",
3028                fill.client_order_id(),
3029                cached_position_id,
3030            );
3031        }
3032
3033        if let Some(position_id) = cached_position_id {
3034            if let Some(fill_position_id) = fill.position_id
3035                && fill_position_id != position_id
3036            {
3037                log::warn!(
3038                    "Incorrect position ID assigned to fill: \
3039                     cached={position_id}, assigned={fill_position_id}; \
3040                     re-assigning from cache",
3041                );
3042            }
3043
3044            if self.config.debug {
3045                log::debug!("Assigned {position_id} to {}", fill.client_order_id());
3046            }
3047
3048            return position_id;
3049        }
3050
3051        let position_id = match oms_type {
3052            OmsType::Hedging => self.determine_hedging_position_id(fill, order),
3053            OmsType::Netting => self.determine_netting_position_id(fill),
3054            _ => self.determine_netting_position_id(fill),
3055        };
3056
3057        let order = if let Some(o) = order {
3058            o.clone()
3059        } else {
3060            let cache = self.cache.borrow();
3061            cache.order(&fill.client_order_id()).map_or_else(
3062                || {
3063                    panic!(
3064                        "Order for {} not found to determine position ID",
3065                        fill.client_order_id()
3066                    )
3067                },
3068                |o| o.clone(),
3069            )
3070        };
3071
3072        if order.exec_algorithm_id().is_some()
3073            && let Some(exec_spawn_id) = order.exec_spawn_id()
3074        {
3075            let cache = self.cache.borrow();
3076            let primary = if let Some(p) = cache.order(&exec_spawn_id) {
3077                p.clone()
3078            } else {
3079                log::warn!(
3080                    "Primary exec spawn order {exec_spawn_id} not found, \
3081                     skipping position ID propagation"
3082                );
3083                return position_id;
3084            };
3085            let primary_already_indexed = cache.position_id(&primary.client_order_id()).is_some();
3086            drop(cache);
3087
3088            if primary.position_id().is_none() && !primary_already_indexed {
3089                if let Some(mut primary_mut) = self.cache.borrow_mut().order_mut(&exec_spawn_id) {
3090                    primary_mut.set_position_id(Some(position_id));
3091                }
3092                let _ = self.cache.borrow_mut().add_position_id(
3093                    &position_id,
3094                    &primary.instrument_id().venue,
3095                    &primary.client_order_id(),
3096                    &primary.strategy_id(),
3097                );
3098                log::debug!("Assigned primary order {position_id}");
3099            }
3100        }
3101
3102        position_id
3103    }
3104
3105    fn determine_hedging_position_id(
3106        &mut self,
3107        fill: &OrderFilled,
3108        order: Option<&OrderAny>,
3109    ) -> PositionId {
3110        // Check if position ID already exists
3111        if let Some(position_id) = fill.position_id {
3112            if self.config.debug {
3113                log::debug!("Already had a position ID of: {position_id}");
3114            }
3115            return position_id;
3116        }
3117
3118        let cache = self.cache.borrow();
3119
3120        let cached_order;
3121        let order: &OrderAny = if let Some(order) = order {
3122            order
3123        } else {
3124            cached_order = cache.order(&fill.client_order_id()).unwrap_or_else(|| {
3125                panic!(
3126                    "Order for {} not found to determine position ID",
3127                    fill.client_order_id()
3128                )
3129            });
3130            &cached_order
3131        };
3132
3133        // Check execution spawn orders
3134        if let Some(spawn_id) = order.exec_spawn_id() {
3135            let spawn_orders = cache.orders_for_exec_spawn(&spawn_id);
3136            for spawned_order in spawn_orders {
3137                if let Some(pos_id) = spawned_order.position_id() {
3138                    if self.config.debug {
3139                        log::debug!("Found spawned {} for {}", pos_id, fill.client_order_id());
3140                    }
3141                    return pos_id;
3142                }
3143            }
3144        }
3145
3146        if order.is_reduce_only() {
3147            let mut candidates = cache
3148                .positions_open(
3149                    None,
3150                    Some(&fill.instrument_id),
3151                    Some(&fill.strategy_id),
3152                    Some(&fill.account_id),
3153                    None,
3154                )
3155                .into_iter()
3156                .filter(|position| position.is_opposite_side(fill.order_side));
3157            let candidate = candidates.next();
3158
3159            if let Some(position) = candidate
3160                && candidates.next().is_none()
3161                && order.would_reduce_only(position.side, position.quantity)
3162            {
3163                if self.config.debug {
3164                    log::debug!(
3165                        "Assigned reduce-only fill {} to position {}",
3166                        fill.client_order_id(),
3167                        position.id
3168                    );
3169                }
3170                return position.id;
3171            }
3172        }
3173
3174        // Generate new position ID
3175        let position_id = self.pos_id_generator.generate(fill.strategy_id, false);
3176
3177        if self.config.debug {
3178            log::debug!("Generated {} for {}", position_id, fill.client_order_id());
3179        }
3180        position_id
3181    }
3182
3183    fn determine_netting_position_id(&self, fill: &OrderFilled) -> PositionId {
3184        PositionId::new(format!("{}-{}", fill.instrument_id, fill.strategy_id))
3185    }
3186
3187    fn validate_fill_for_order(&self, order: &OrderAny, fill: &OrderFilled) -> anyhow::Result<()> {
3188        if order.is_duplicate_fill(fill) {
3189            log::warn!(
3190                "Duplicate fill: {} trade_id={} already applied, skipping",
3191                order.client_order_id(),
3192                fill.trade_id
3193            );
3194            anyhow::bail!("Duplicate fill");
3195        }
3196
3197        if let Some(position_id) = fill.position_id
3198            && self.position_contains_trade_id(position_id, fill.trade_id)
3199        {
3200            log::warn!(
3201                "Duplicate fill: {} trade_id={} already applied to position {}, skipping",
3202                order.client_order_id(),
3203                fill.trade_id,
3204                position_id
3205            );
3206            anyhow::bail!("Duplicate position fill");
3207        }
3208
3209        self.check_overfill(order, fill)
3210    }
3211
3212    fn validate_fill_for_order_projection(
3213        &self,
3214        order: &OrderAny,
3215        fill: &OrderFilled,
3216    ) -> anyhow::Result<()> {
3217        if order.is_duplicate_fill(fill) {
3218            anyhow::bail!("Duplicate fill");
3219        }
3220
3221        self.check_overfill(order, fill)
3222    }
3223
3224    fn position_contains_trade_id(&self, position_id: PositionId, trade_id: TradeId) -> bool {
3225        self.cache
3226            .borrow()
3227            .position(&position_id)
3228            .is_some_and(|position| position.trade_ids.contains(&trade_id))
3229    }
3230
3231    fn update_cached_order(
3232        &self,
3233        client_order_id: ClientOrderId,
3234        event: &OrderEventAny,
3235        send_portfolio_update: bool,
3236    ) -> Option<OrderAny> {
3237        let result = { self.cache.borrow_mut().update_order(event) };
3238
3239        let order = match result {
3240            Ok(order) => order,
3241            Err(e) => {
3242                if matches!(
3243                    e.downcast_ref::<OrderError>(),
3244                    Some(OrderError::InvalidStateTransition)
3245                ) {
3246                    // A non-fill event that fails to apply to an already-closed order is an
3247                    // expected venue race (e.g. a place reject then a stream cancel for the same
3248                    // order), not an anomaly. A dropped fill stays at warn even on a closed order,
3249                    // since it represents real, possibly lost, execution.
3250                    let already_closed = self
3251                        .cache
3252                        .borrow()
3253                        .order(&client_order_id)
3254                        .is_some_and(|o| o.is_closed());
3255
3256                    if already_closed && !matches!(event, OrderEventAny::Filled(_)) {
3257                        log::debug!("InvalidStateTrigger: {e}, did not apply {event}");
3258                    } else {
3259                        log::warn!("InvalidStateTrigger: {e}, did not apply {event}");
3260                    }
3261                    return None;
3262                }
3263
3264                if let Some(OrderError::DuplicateFill(trade_id)) = e.downcast_ref::<OrderError>() {
3265                    log::warn!(
3266                        "Duplicate fill rejected at order level: trade_id={trade_id}, did not apply {event}"
3267                    );
3268                    return None;
3269                }
3270
3271                if let Some(OrderError::DuplicateFillVoid(trade_id)) =
3272                    e.downcast_ref::<OrderError>()
3273                {
3274                    log::warn!(
3275                        "Duplicate fill void rejected at order level: trade_id={trade_id}, did not apply {event}"
3276                    );
3277                    return None;
3278                }
3279
3280                log::error!("Error applying event: {e}, did not apply {event}");
3281
3282                if matches!(
3283                    event,
3284                    OrderEventAny::Denied(_)
3285                        | OrderEventAny::Rejected(_)
3286                        | OrderEventAny::Canceled(_)
3287                        | OrderEventAny::Expired(_)
3288                ) {
3289                    log::warn!(
3290                        "Terminal event {event} failed to apply to {client_order_id}, forcing cleanup from own book"
3291                    );
3292                    self.cache
3293                        .borrow_mut()
3294                        .force_remove_from_own_order_book(&client_order_id);
3295                } else {
3296                    let order = self
3297                        .cache
3298                        .borrow()
3299                        .order(&client_order_id)
3300                        .map(|o| o.clone());
3301
3302                    if let Some(order) = order {
3303                        let should_update_own_book = {
3304                            let cache = self.cache.borrow();
3305                            let own_book = cache.own_order_book(&order.instrument_id());
3306                            (own_book.is_some() && order.is_closed())
3307                                || should_handle_own_book_order(&order)
3308                        };
3309
3310                        if should_update_own_book {
3311                            self.cache.borrow_mut().update_own_order_book(&order);
3312                        }
3313                    }
3314                }
3315                return None;
3316            }
3317        };
3318
3319        if self.config.manage_own_order_books && should_handle_own_book_order(&order) {
3320            let needs_own_book = {
3321                self.cache
3322                    .borrow()
3323                    .own_order_book(&order.instrument_id())
3324                    .is_none()
3325            };
3326
3327            if needs_own_book {
3328                self.cache.borrow_mut().update_own_order_book(&order);
3329            }
3330        }
3331
3332        if self.config.debug {
3333            log::debug!("{SEND}{EVT} {event}");
3334        }
3335
3336        if self.config.snapshot_orders {
3337            self.create_order_state_snapshot(&order);
3338        }
3339
3340        if send_portfolio_update {
3341            self.send_order_update_to_portfolio(event);
3342        }
3343
3344        Some(order)
3345    }
3346
3347    fn send_order_update_to_portfolio(&self, event: &OrderEventAny) {
3348        let is_wallet = event.account_id().is_some_and(|account_id| {
3349            self.cache
3350                .borrow()
3351                .account(&account_id)
3352                .is_some_and(|account| account.account_type() == AccountType::Wallet)
3353        });
3354        let send_to_portfolio = match event {
3355            OrderEventAny::Filled(fill) => self
3356                .cache
3357                .borrow()
3358                .account(&fill.account_id)
3359                .is_none_or(|account| !account.is_margin_account()),
3360            OrderEventAny::Accepted(_)
3361            | OrderEventAny::Canceled(_)
3362            | OrderEventAny::Expired(_)
3363            | OrderEventAny::Rejected(_)
3364            | OrderEventAny::Updated(_) => true,
3365            OrderEventAny::Submitted(_)
3366            | OrderEventAny::Triggered(_)
3367            | OrderEventAny::PendingUpdate(_)
3368            | OrderEventAny::PendingCancel(_)
3369            | OrderEventAny::ModifyRejected(_)
3370            | OrderEventAny::CancelRejected(_)
3371            | OrderEventAny::FillVoided(_) => is_wallet,
3372            _ => false,
3373        };
3374
3375        if send_to_portfolio {
3376            let portfolio_endpoint = MessagingSwitchboard::portfolio_update_order();
3377            msgbus::send_order_event(portfolio_endpoint, event.clone());
3378        }
3379    }
3380
3381    fn publish_order_event(&self, event: &OrderEventAny) {
3382        let topic = switchboard::get_event_order_topic(event.strategy_id());
3383        msgbus::publish_order_event(topic, event);
3384
3385        let topic = match event {
3386            OrderEventAny::Submitted(_) => {
3387                switchboard::get_order_submitted_topic(event.instrument_id())
3388            }
3389            OrderEventAny::Rejected(_) => {
3390                switchboard::get_order_rejected_topic(event.instrument_id())
3391            }
3392            OrderEventAny::PendingUpdate(_) => {
3393                switchboard::get_order_pending_update_topic(event.instrument_id())
3394            }
3395            OrderEventAny::PendingCancel(_) => {
3396                switchboard::get_order_pending_cancel_topic(event.instrument_id())
3397            }
3398            OrderEventAny::ModifyRejected(_) => {
3399                switchboard::get_order_modify_rejected_topic(event.instrument_id())
3400            }
3401            OrderEventAny::CancelRejected(_) => {
3402                switchboard::get_order_cancel_rejected_topic(event.instrument_id())
3403            }
3404            OrderEventAny::Canceled(_) => {
3405                switchboard::get_order_canceled_topic(event.instrument_id())
3406            }
3407            // Keep Filled out of this generic fanout: handle_order_fill publishes the instrument
3408            // topic, while leg fills stay on the strategy topic.
3409            _ => return,
3410        };
3411
3412        msgbus::publish_order_event(topic, event);
3413    }
3414
3415    fn publish_position_events(&self, events: Vec<PositionEvent>) {
3416        for event in events {
3417            let strategy_id = match &event {
3418                PositionEvent::PositionOpened(event) => event.strategy_id,
3419                PositionEvent::PositionChanged(event) => event.strategy_id,
3420                PositionEvent::PositionClosed(event) => event.strategy_id,
3421                PositionEvent::PositionAdjusted(event) => event.strategy_id,
3422            };
3423            let topic = switchboard::get_event_position_topic(strategy_id);
3424            msgbus::publish_position_event(topic, &event);
3425        }
3426    }
3427
3428    fn check_overfill(&self, order: &OrderAny, fill: &OrderFilled) -> anyhow::Result<()> {
3429        let potential_overfill = order.calculate_overfill(fill.last_qty);
3430
3431        if potential_overfill.is_positive() {
3432            if self.config.allow_overfills {
3433                log::warn!(
3434                    "Order overfill detected: {} potential_overfill={}, current_filled={}, last_qty={}, quantity={}",
3435                    order.client_order_id(),
3436                    potential_overfill,
3437                    order.filled_qty(),
3438                    fill.last_qty,
3439                    order.quantity()
3440                );
3441            } else {
3442                let msg = format!(
3443                    "Order overfill rejected: {} potential_overfill={}, current_filled={}, last_qty={}, quantity={}. \
3444                Set `allow_overfills=true` in ExecutionEngineConfig to allow overfills.",
3445                    order.client_order_id(),
3446                    potential_overfill,
3447                    order.filled_qty(),
3448                    fill.last_qty,
3449                    order.quantity()
3450                );
3451                anyhow::bail!("{msg}");
3452            }
3453        }
3454
3455        Ok(())
3456    }
3457
3458    fn handle_order_fill(
3459        &mut self,
3460        order: &OrderAny,
3461        fill: OrderFilled,
3462        oms_type: OmsType,
3463    ) -> Vec<PositionEvent> {
3464        let instrument =
3465            if let Some(instrument) = self.cache.borrow().instrument(&fill.instrument_id) {
3466                instrument.clone()
3467            } else {
3468                log::error!(
3469                    "Cannot handle order fill: no instrument found for {}, {fill}",
3470                    fill.instrument_id,
3471                );
3472                return Vec::new();
3473            };
3474
3475        let is_margin_account = {
3476            let cache = self.cache.borrow();
3477            let account = match cache.try_account(&fill.account_id) {
3478                Ok(account) => account,
3479                Err(e) => {
3480                    log::error!("Cannot handle order fill: {e}, {fill}");
3481                    return Vec::new();
3482                }
3483            };
3484
3485            account.is_margin_account()
3486        };
3487
3488        // Skip portfolio position updates for combo fills (spread instruments)
3489        // Combo fills are only used for order management, not portfolio updates
3490        if !instrument.is_spread() && is_margin_account {
3491            let portfolio_endpoint = MessagingSwitchboard::portfolio_update_order();
3492            msgbus::send_order_event(portfolio_endpoint, OrderEventAny::Filled(fill.clone()));
3493        }
3494
3495        let (position, position_events) = if instrument.is_spread() {
3496            (None, Vec::new())
3497        } else {
3498            let position_events = self.handle_position_update(&instrument, fill.clone(), oms_type);
3499            let position_id = fill.position_id.unwrap();
3500            (
3501                self.cache.borrow().position_owned(&position_id),
3502                position_events,
3503            )
3504        };
3505
3506        // Handle contingent orders for both spread and non-spread instruments
3507        // For spread instruments, contingent orders work without position linkage
3508        if matches!(order.contingency_type(), Some(ContingencyType::Oto)) {
3509            // For non-spread instruments, link to position if available
3510            if !instrument.is_spread()
3511                && let Some(ref pos) = position
3512                && pos.is_open()
3513            {
3514                let position_id = pos.id;
3515
3516                for client_order_id in order.linked_order_ids().unwrap_or_default() {
3517                    // Take a scoped write borrow on the contingent's cell. The borrow drops at
3518                    // the end of `and_then` so the subsequent `add_position_id` on the cache is
3519                    // free to take `&mut Cache`.
3520                    let link = self.cache.borrow_mut().order_mut(client_order_id).and_then(
3521                        |mut contingent_order| {
3522                            if contingent_order.position_id().is_none() {
3523                                contingent_order.set_position_id(Some(position_id));
3524                                Some((
3525                                    contingent_order.instrument_id().venue,
3526                                    contingent_order.client_order_id(),
3527                                    contingent_order.strategy_id(),
3528                                ))
3529                            } else {
3530                                None
3531                            }
3532                        },
3533                    );
3534
3535                    if let Some((venue, contingent_id, strategy_id)) = link
3536                        && let Err(e) = self.cache.borrow_mut().add_position_id(
3537                            &position_id,
3538                            &venue,
3539                            &contingent_id,
3540                            &strategy_id,
3541                        )
3542                    {
3543                        log::error!("Failed to add position ID: {e}");
3544                    }
3545                }
3546            }
3547            // For spread instruments, contingent orders can still be triggered
3548            // but without position linkage (since no position is created for spreads)
3549        }
3550
3551        let topic = switchboard::get_order_filled_topic(fill.instrument_id);
3552        let event = OrderEventAny::Filled(fill);
3553        msgbus::publish_order_event(topic, &event);
3554
3555        position_events
3556    }
3557
3558    fn prepare_order_fill_void_positions(
3559        &self,
3560        order: &OrderAny,
3561        event: &OrderFillVoided,
3562    ) -> anyhow::Result<Vec<CorrectedPosition>> {
3563        let source_event_id = order
3564            .events()
3565            .into_iter()
3566            .find_map(|order_event| match order_event {
3567                OrderEventAny::Filled(fill) if fill.trade_id == event.trade_id => {
3568                    Some(fill.event_id)
3569                }
3570                _ => None,
3571            })
3572            .ok_or_else(|| anyhow::anyhow!("fill {} is not in order history", event.trade_id))?;
3573
3574        let positions: Vec<Position> = {
3575            let cache = self.cache.borrow();
3576            cache
3577                .positions(
3578                    None,
3579                    Some(&event.instrument_id),
3580                    Some(&event.strategy_id),
3581                    Some(&event.account_id),
3582                    None,
3583                )
3584                .into_iter()
3585                .map(|position| position.cloned())
3586                .collect()
3587        };
3588        let mut fragments = Vec::new();
3589
3590        for position in &positions {
3591            for replay_event in &position.replay_events {
3592                let PositionReplayEvent::Filled(fill) = replay_event else {
3593                    continue;
3594                };
3595
3596                if fill.client_order_id != event.client_order_id || fill.trade_id != event.trade_id
3597                {
3598                    continue;
3599                }
3600                let split_rank = if fill.event_id == source_event_id {
3601                    0
3602                } else if fill.causation_id == Some(source_event_id) {
3603                    1
3604                } else {
3605                    continue;
3606                };
3607                fragments.push((position.id, split_rank, fill.last_qty, fill.commission));
3608            }
3609        }
3610        anyhow::ensure!(
3611            !fragments.is_empty(),
3612            "no position fragments found for fill {}",
3613            event.trade_id
3614        );
3615        fragments.sort_by_key(|(_, split_rank, _, _)| *split_rank);
3616
3617        let mut allocations = IndexMap::<PositionId, (Quantity, Option<Money>)>::new();
3618        let mut remaining_qty = event.voided_qty;
3619        for (position_id, _, quantity, _) in fragments.iter().rev() {
3620            if remaining_qty.is_zero() {
3621                break;
3622            }
3623            let removed = remaining_qty.min(*quantity);
3624            allocations
3625                .entry(*position_id)
3626                .and_modify(|allocation| allocation.0 = allocation.0 + removed)
3627                .or_insert((removed, None));
3628            remaining_qty = remaining_qty - removed;
3629        }
3630        anyhow::ensure!(
3631            remaining_qty.is_zero(),
3632            "position fragments do not cover voided quantity for fill {}",
3633            event.trade_id
3634        );
3635
3636        if let Some(mut remaining_commission) = event.commission_voided {
3637            for (position_id, _, _, commission) in fragments.iter().rev() {
3638                if remaining_commission.is_zero() {
3639                    break;
3640                }
3641                let Some(commission) = commission else {
3642                    continue;
3643                };
3644                anyhow::ensure!(
3645                    commission.currency == remaining_commission.currency,
3646                    "position commission currency differs for fill {}",
3647                    event.trade_id
3648                );
3649                let removed_raw = remaining_commission.raw.abs().min(commission.raw.abs());
3650                let removed = Money::from_raw(
3651                    removed_raw * remaining_commission.raw.signum(),
3652                    remaining_commission.currency,
3653                );
3654                allocations
3655                    .entry(*position_id)
3656                    .and_modify(|allocation| {
3657                        allocation.1 = Some(
3658                            allocation
3659                                .1
3660                                .map_or(removed, |commission| commission + removed),
3661                        );
3662                    })
3663                    .or_insert((Quantity::zero(event.voided_qty.precision), Some(removed)));
3664                remaining_commission = remaining_commission - removed;
3665            }
3666            anyhow::ensure!(
3667                remaining_commission.is_zero(),
3668                "position fragments do not cover voided commission for fill {}",
3669                event.trade_id
3670            );
3671        }
3672
3673        let mut corrected_positions = Vec::new();
3674
3675        for (position_id, (voided_qty, commission_voided)) in allocations {
3676            if voided_qty.is_zero() {
3677                anyhow::bail!(
3678                    "commission-only position correction requires authoritative reconciliation for fill {}",
3679                    event.trade_id
3680                );
3681            }
3682            let mut position = self
3683                .cache
3684                .borrow()
3685                .position_owned(&position_id)
3686                .ok_or_else(|| anyhow::anyhow!("position {position_id} is not cached"))?;
3687            let previous = position
3688                .fill_voids
3689                .iter()
3690                .rev()
3691                .find(|record| {
3692                    record.event.client_order_id == event.client_order_id
3693                        && record.event.trade_id == event.trade_id
3694                })
3695                .map(|record| (record.voided_qty, record.commission_voided));
3696            if previous == Some((voided_qty, commission_voided)) {
3697                continue;
3698            }
3699            let corrected_qty = previous.map_or(voided_qty, |(prior_qty, _)| {
3700                voided_qty.saturating_sub(prior_qty)
3701            });
3702
3703            // `events` holds the fills since the position was last flat, because `apply_fill`
3704            // clears it when reopening from flat. A NETTING flip splits one fill across the
3705            // closing and reopening cycles under the same trade, so compare quantities rather
3706            // than presence: the correction reaches an earlier cycle once it exceeds what the
3707            // current cycle originally held. Earlier corrections have already shrunk the
3708            // fragments in `events` while `voided_qty` stays cumulative, so add back what this
3709            // position already voided. Read this before `apply_fill_void`, whose rebuild
3710            // re-derives `events` and can move that boundary.
3711            let previously_voided = previous
3712                .map_or(Quantity::zero(position.size_precision), |(prior_qty, _)| {
3713                    prior_qty
3714                });
3715            let current_cycle_qty = position
3716                .events
3717                .iter()
3718                .filter(|fill| {
3719                    fill.client_order_id == event.client_order_id && fill.trade_id == event.trade_id
3720                })
3721                .fold(previously_voided, |total, fill| total + fill.last_qty);
3722            let absorbed_prior_cycles = voided_qty > current_cycle_qty;
3723            let closed_cycles_pnl =
3724                position.apply_fill_void(event.clone(), voided_qty, commission_voided)?;
3725            corrected_positions.push(CorrectedPosition {
3726                position,
3727                corrected_qty,
3728                absorbed_prior_cycles,
3729                closed_cycles_pnl,
3730            });
3731        }
3732        Ok(corrected_positions)
3733    }
3734
3735    fn create_fill_void_position_event(
3736        position: &Position,
3737        fill_voided: &OrderFillVoided,
3738        corrected_qty: Quantity,
3739    ) -> PositionEvent {
3740        let event_id = UUID4::new();
3741        let ts_init = fill_voided.ts_init;
3742
3743        if position.is_closed() {
3744            PositionEvent::PositionClosed(PositionClosed {
3745                trader_id: position.trader_id,
3746                strategy_id: position.strategy_id,
3747                instrument_id: position.instrument_id,
3748                position_id: position.id,
3749                account_id: position.account_id,
3750                opening_order_id: position.opening_order_id,
3751                closing_order_id: position.closing_order_id,
3752                entry: position.entry,
3753                side: position.side,
3754                signed_qty: position.signed_qty,
3755                quantity: position.quantity,
3756                peak_quantity: position.peak_qty,
3757                last_qty: corrected_qty,
3758                last_px: fill_voided.last_px,
3759                currency: position.quote_currency,
3760                avg_px_open: position.avg_px_open,
3761                avg_px_close: position.avg_px_close,
3762                realized_return: position.realized_return,
3763                realized_pnl: position.realized_pnl,
3764                unrealized_pnl: Money::zero(position.quote_currency),
3765                duration: position.duration_ns,
3766                event_id,
3767                ts_opened: position.ts_opened,
3768                ts_closed: position.ts_closed,
3769                ts_event: fill_voided.ts_event,
3770                ts_init,
3771            })
3772        } else {
3773            PositionEvent::PositionChanged(PositionChanged {
3774                trader_id: position.trader_id,
3775                strategy_id: position.strategy_id,
3776                instrument_id: position.instrument_id,
3777                position_id: position.id,
3778                account_id: position.account_id,
3779                opening_order_id: position.opening_order_id,
3780                entry: position.entry,
3781                side: position.side,
3782                signed_qty: position.signed_qty,
3783                quantity: position.quantity,
3784                peak_quantity: position.peak_qty,
3785                last_qty: corrected_qty,
3786                last_px: fill_voided.last_px,
3787                currency: position.quote_currency,
3788                avg_px_open: position.avg_px_open,
3789                avg_px_close: position.avg_px_close,
3790                realized_return: position.realized_return,
3791                realized_pnl: position.realized_pnl,
3792                unrealized_pnl: Money::zero(position.quote_currency),
3793                event_id,
3794                ts_opened: position.ts_opened,
3795                ts_event: fill_voided.ts_event,
3796                ts_init,
3797            })
3798        }
3799    }
3800
3801    /// Handle position creation or update for a fill.
3802    ///
3803    /// This function mirrors the Python `_handle_position_update` method.
3804    fn handle_position_update(
3805        &mut self,
3806        instrument: &InstrumentAny,
3807        fill: OrderFilled,
3808        oms_type: OmsType,
3809    ) -> Vec<PositionEvent> {
3810        let position_id = if let Some(position_id) = fill.position_id {
3811            position_id
3812        } else {
3813            log::error!("Cannot handle position update: no position ID found for fill {fill}");
3814            return Vec::new();
3815        };
3816
3817        let position_opt = self.cache.borrow().position_owned(&position_id);
3818
3819        match position_opt {
3820            None => {
3821                if self.reject_reduce_only_position_open(&fill, oms_type) {
3822                    return Vec::new();
3823                }
3824
3825                self.open_position(instrument, None, fill, oms_type)
3826                    .unwrap_or_default()
3827            }
3828            Some(pos) if pos.is_closed() => {
3829                if self.reject_reduce_only_position_open(&fill, oms_type) {
3830                    return Vec::new();
3831                }
3832
3833                self.open_position(instrument, Some(&pos), fill, oms_type)
3834                    .unwrap_or_default()
3835            }
3836            Some(mut pos) => {
3837                if self.will_flip_position(&pos, &fill) {
3838                    self.flip_position(instrument, &mut pos, &fill, oms_type)
3839                } else {
3840                    self.update_position(&mut pos, &fill).into_iter().collect()
3841                }
3842            }
3843        }
3844    }
3845
3846    fn reject_reduce_only_position_open(&self, fill: &OrderFilled, oms_type: OmsType) -> bool {
3847        let cache = self.cache.borrow();
3848        let Some(order) = cache.order_owned(&fill.client_order_id) else {
3849            return false;
3850        };
3851
3852        if !order.is_reduce_only() {
3853            return false;
3854        }
3855
3856        let positions_open = cache.positions_open(
3857            None,
3858            Some(&fill.instrument_id),
3859            None,
3860            Some(&fill.account_id),
3861            None,
3862        );
3863        let position_id = fill
3864            .position_id
3865            .map_or_else(|| "None".to_string(), |position_id| position_id.to_string());
3866        let matching_position_details = Self::position_details(
3867            positions_open
3868                .iter()
3869                .filter(|position| position.is_opposite_side(fill.order_side))
3870                .map(|position| &**position),
3871        );
3872        let open_position_details =
3873            Self::position_details(positions_open.iter().map(|position| &**position));
3874
3875        log::error!(
3876            "Cannot open {oms_type} position {position_id} from reduce-only fill {} for {}; \
3877             matching_reduce_positions=[{}], open_positions=[{}]",
3878            fill.trade_id,
3879            fill.instrument_id,
3880            matching_position_details,
3881            open_position_details
3882        );
3883
3884        true
3885    }
3886
3887    #[allow(
3888        clippy::needless_pass_by_value,
3889        reason = "takes the opening fill by value to seed the new position"
3890    )]
3891    fn open_position(
3892        &self,
3893        instrument: &InstrumentAny,
3894        position: Option<&Position>,
3895        fill: OrderFilled,
3896        oms_type: OmsType,
3897    ) -> anyhow::Result<Vec<PositionEvent>> {
3898        if let Some(position) = position {
3899            if Self::is_duplicate_closed_fill(position, &fill) {
3900                log::warn!(
3901                    "Ignoring duplicate fill {} for closed position {}; no position reopened (side={:?}, qty={}, px={})",
3902                    fill.trade_id,
3903                    position.id,
3904                    fill.order_side,
3905                    fill.last_qty,
3906                    fill.last_px
3907                );
3908                return Ok(Vec::new());
3909            }
3910            self.reopen_position(position, oms_type)?;
3911        }
3912
3913        // The prior-position clone exists only to carry replay state across the reopen
3914        let prior_position = if self.config.carry_replay_events_on_reopen {
3915            position.cloned().or_else(|| {
3916                fill.position_id
3917                    .and_then(|position_id| self.cache.borrow().position_owned(&position_id))
3918            })
3919        } else {
3920            None
3921        };
3922        let mut position = Position::new(instrument, fill.clone());
3923        if let Some(prior) = prior_position
3924            && prior.id == position.id
3925        {
3926            let current_replay = position.replay_events.clone();
3927            position.replay_events = prior.replay_events;
3928            position.replay_events.extend(current_replay);
3929            position.fill_voids = prior.fill_voids;
3930        }
3931        let is_orderless_leg = self.is_leg_fill(&fill)
3932            && !self.cache.borrow().order_exists(&position.opening_order_id);
3933        if is_orderless_leg {
3934            self.cache
3935                .borrow_mut()
3936                .add_position_without_order(&position, oms_type)?;
3937        } else {
3938            self.cache.borrow_mut().add_position(&position, oms_type)?;
3939        }
3940
3941        if self.config.snapshot_positions {
3942            self.create_position_state_snapshot(&position, true);
3943        }
3944
3945        let ts_init = self.clock.borrow().timestamp_ns();
3946        let event = PositionOpened::create(&position, &fill, UUID4::new(), ts_init);
3947
3948        Ok(vec![PositionEvent::PositionOpened(event)])
3949    }
3950
3951    fn is_duplicate_closed_fill(position: &Position, fill: &OrderFilled) -> bool {
3952        position.replay_events.iter().any(|event| {
3953            matches!(
3954                event,
3955                PositionReplayEvent::Filled(replayed) if replayed.trade_id == fill.trade_id
3956            )
3957        })
3958    }
3959
3960    fn reopen_position(&self, position: &Position, oms_type: OmsType) -> anyhow::Result<()> {
3961        if oms_type == OmsType::Netting {
3962            if position.is_open() {
3963                anyhow::bail!(
3964                    "Cannot reopen position {} (oms_type=NETTING): reopening is only valid for closed positions in NETTING mode",
3965                    position.id
3966                );
3967            }
3968            // Snapshot closed position if reopening (NETTING mode)
3969            self.snapshot_position(position)?;
3970        } else {
3971            // HEDGING mode
3972            log::warn!(
3973                "Received fill for closed position {} in HEDGING mode; creating new position and ignoring previous state",
3974                position.id
3975            );
3976        }
3977        Ok(())
3978    }
3979
3980    /// Archives the closed `position` and anchors the frame when an anchorer is installed.
3981    ///
3982    /// An installed anchorer needs the encoded frame, so this takes the eager path. Without one
3983    /// the cache defers the encode unless a backing database has to persist the frame.
3984    fn snapshot_position(&self, position: &Position) -> anyhow::Result<()> {
3985        let mut cache = self.cache.borrow_mut();
3986
3987        let Some(anchorer) = &self.snapshot_anchorer else {
3988            return cache.snapshot_position(position);
3989        };
3990
3991        let snapshot_ref = cache.snapshot_position_encoded(position)?;
3992        drop(cache);
3993
3994        if let Err(e) = anchorer(snapshot_ref) {
3995            log::warn!("Failed to record cache snapshot anchor: {e}");
3996        }
3997
3998        Ok(())
3999    }
4000
4001    fn update_position(
4002        &self,
4003        position: &mut Position,
4004        fill: &OrderFilled,
4005    ) -> Option<PositionEvent> {
4006        // Apply the fill to the position
4007        position.apply(fill);
4008
4009        // Check if position is closed after applying the fill
4010        let is_closed = position.is_closed();
4011
4012        // Update position in cache - this should handle the closed state tracking
4013        if let Err(e) = self.cache.borrow_mut().update_position(position) {
4014            log::error!("Failed to update position: {e:?}");
4015            return None;
4016        }
4017
4018        // Verify cache state after update
4019        let cache = self.cache.borrow();
4020
4021        drop(cache);
4022
4023        // Create position state snapshot if enabled
4024        if self.config.snapshot_positions {
4025            self.create_position_state_snapshot(position, false);
4026        }
4027
4028        let ts_init = self.clock.borrow().timestamp_ns();
4029
4030        if is_closed {
4031            let event = PositionClosed::create(position, fill, UUID4::new(), ts_init);
4032            Some(PositionEvent::PositionClosed(event))
4033        } else {
4034            let event = PositionChanged::create(position, fill, UUID4::new(), ts_init);
4035            Some(PositionEvent::PositionChanged(event))
4036        }
4037    }
4038
4039    fn will_flip_position(&self, position: &Position, fill: &OrderFilled) -> bool {
4040        position.is_opposite_side(fill.order_side) && (fill.last_qty.raw > position.quantity.raw)
4041    }
4042
4043    fn position_signed_decimal_qty(position: &Position) -> Decimal {
4044        match position.side {
4045            PositionSide::Long => position.quantity.as_decimal(),
4046            PositionSide::Short => -position.quantity.as_decimal(),
4047            _ => Decimal::ZERO,
4048        }
4049    }
4050
4051    fn position_details<'a>(positions: impl IntoIterator<Item = &'a Position>) -> String {
4052        positions
4053            .into_iter()
4054            .map(|position| {
4055                format!(
4056                    "{} strategy_id={} signed_qty={}",
4057                    position.id,
4058                    position.strategy_id,
4059                    Self::position_signed_decimal_qty(position)
4060                )
4061            })
4062            .collect::<Vec<_>>()
4063            .join(", ")
4064    }
4065
4066    fn flip_position(
4067        &mut self,
4068        instrument: &InstrumentAny,
4069        position: &mut Position,
4070        fill: &OrderFilled,
4071        oms_type: OmsType,
4072    ) -> Vec<PositionEvent> {
4073        let mut position_events = Vec::new();
4074
4075        if fill.commission.is_none() {
4076            log::warn!(
4077                "Commission is not available for position flip, splitting with no commission"
4078            );
4079        }
4080
4081        let position_id_flip = if oms_type == OmsType::Hedging
4082            && let Some(position_id) = fill.position_id
4083            && position_id.is_virtual()
4084        {
4085            // Generate new position ID for flipped virtual position (Hedging OMS only)
4086            Some(self.pos_id_generator.generate(fill.strategy_id, true))
4087        } else {
4088            // Default: use the same position ID as the fill (Python behavior)
4089            fill.position_id
4090        };
4091
4092        let (fill_split1, fill_split2) = fill
4093            .split_for_position_flip(position.quantity, position_id_flip, UUID4::new())
4094            .expect("Invalid position flip split");
4095
4096        if let Some(position_event) = self.update_position(position, &fill_split1) {
4097            position_events.push(position_event);
4098        }
4099
4100        // Snapshot closed position before reusing ID (NETTING mode)
4101        if oms_type == OmsType::Netting
4102            && let Err(e) = self.snapshot_position(position)
4103        {
4104            log::warn!("Failed to snapshot position during flip: {e:?}");
4105        }
4106
4107        if oms_type == OmsType::Hedging
4108            && let Some(position_id) = fill.position_id
4109            && position_id.is_virtual()
4110        {
4111            log::warn!("Closing position {fill_split1:?}");
4112            log::warn!("Flipping position {fill_split2:?}");
4113        }
4114
4115        // Open flipped position
4116        match self.open_position(instrument, None, fill_split2, oms_type) {
4117            Ok(opened_events) => position_events.extend(opened_events),
4118            Err(e) => log::error!("Failed to open flipped position: {e:?}"),
4119        }
4120
4121        position_events
4122    }
4123
4124    /// Sets the internal position ID generator counts based on existing cached positions.
4125    pub fn set_position_id_counts(&mut self) {
4126        let cache = self.cache.borrow();
4127        let positions = cache.positions(None, None, None, None, None);
4128
4129        // Count positions per instrument_id using a HashMap
4130        let mut counts: HashMap<StrategyId, usize> = HashMap::new();
4131
4132        for position in positions {
4133            *counts.entry(position.strategy_id).or_insert(0) += 1;
4134        }
4135
4136        self.pos_id_generator.reset();
4137
4138        for (strategy_id, count) in counts {
4139            self.pos_id_generator.set_count(count, strategy_id);
4140            log::info!("Set PositionId count for {strategy_id} to {count}");
4141        }
4142    }
4143
4144    fn deny_order(&self, order: &OrderAny, reason: &str) {
4145        let denied = OrderDenied::new(
4146            order.trader_id(),
4147            order.strategy_id(),
4148            order.instrument_id(),
4149            order.client_order_id(),
4150            reason.into(),
4151            UUID4::new(),
4152            self.clock.borrow().timestamp_ns(),
4153            self.clock.borrow().timestamp_ns(),
4154        );
4155
4156        let event = OrderEventAny::Denied(denied);
4157        let order = match self.cache.borrow_mut().update_order(&event) {
4158            Ok(order) => order,
4159            Err(e) => {
4160                log::error!("Failed to apply denied event to order: {e}");
4161                return;
4162            }
4163        };
4164
4165        let topic = switchboard::get_event_order_topic(order.strategy_id());
4166        msgbus::publish_order_event(topic, &event);
4167
4168        if self.config.snapshot_orders {
4169            self.create_order_state_snapshot(&order);
4170        }
4171    }
4172
4173    fn get_or_init_own_order_book(&self, instrument_id: &InstrumentId) -> RefMut<'_, OwnOrderBook> {
4174        let mut cache = self.cache.borrow_mut();
4175        if cache.own_order_book_mut(instrument_id).is_none() {
4176            let own_book = OwnOrderBook::new(*instrument_id);
4177            cache.add_own_order_book(own_book).unwrap();
4178        }
4179
4180        RefMut::map(cache, |c| c.own_order_book_mut(instrument_id).unwrap())
4181    }
4182}
4183
4184#[cfg(test)]
4185mod tests {
4186    use nautilus_common::clock::TestClock;
4187    use nautilus_model::{
4188        enums::{LiquiditySide, OrderSide, OrderType, PositionSideSpecified},
4189        events::order::spec::OrderFilledSpec,
4190        identifiers::{AccountId, ClientOrderId, TradeId, VenueOrderId},
4191        instruments::{InstrumentAny, stubs::audusd_sim},
4192        orders::builder::OrderTestBuilder,
4193        types::Price,
4194    };
4195    use rstest::*;
4196
4197    use super::*;
4198
4199    #[rstest]
4200    fn netting_positions_open_for_report_scopes_positions_by_account() {
4201        let instrument = InstrumentAny::CurrencyPair(audusd_sim());
4202        let account1_id = AccountId::from("SIM-001");
4203        let account2_id = AccountId::from("SIM-002");
4204        let position1 = position_for_account(
4205            &instrument,
4206            account1_id,
4207            StrategyId::from("S-001"),
4208            PositionId::from("P-ACC-1"),
4209            OrderSide::Buy,
4210            Quantity::from(1_000),
4211        );
4212        let position2 = position_for_account(
4213            &instrument,
4214            account2_id,
4215            StrategyId::from("S-002"),
4216            PositionId::from("P-ACC-2"),
4217            OrderSide::Buy,
4218            Quantity::from(2_000),
4219        );
4220        let mut cache = Cache::default();
4221        cache.add_position(&position1, OmsType::Netting).unwrap();
4222        cache.add_position(&position2, OmsType::Netting).unwrap();
4223
4224        let report = PositionStatusReport::new(
4225            account1_id,
4226            instrument.id(),
4227            PositionSideSpecified::Long,
4228            Quantity::from(1_000),
4229            UnixNanos::from(1_000_000),
4230            UnixNanos::from(1_000_000),
4231            None,
4232            None,
4233            None,
4234        );
4235
4236        let positions_open = ExecutionEngine::netting_positions_open_for_report(&cache, &report);
4237        let signed_qty: Decimal = positions_open
4238            .iter()
4239            .map(|position| ExecutionEngine::position_signed_decimal_qty(position))
4240            .sum();
4241
4242        assert_eq!(positions_open.len(), 1);
4243        assert_eq!(positions_open[0].id, position1.id);
4244        assert_eq!(signed_qty, Decimal::from(1_000));
4245    }
4246
4247    #[rstest]
4248    fn netting_split_position_ownership_message_reports_only_split_ownership() {
4249        let instrument = InstrumentAny::CurrencyPair(audusd_sim());
4250        let account_id = AccountId::from("SIM-001");
4251        let external_position = position_for_account(
4252            &instrument,
4253            account_id,
4254            StrategyId::from("EXTERNAL"),
4255            PositionId::from("P-EXTERNAL"),
4256            OrderSide::Buy,
4257            Quantity::from(1_000),
4258        );
4259        let strategy_position = position_for_account(
4260            &instrument,
4261            account_id,
4262            StrategyId::from("S-001"),
4263            PositionId::from("P-STRATEGY"),
4264            OrderSide::Buy,
4265            Quantity::from(500),
4266        );
4267        let same_strategy_position = position_for_account(
4268            &instrument,
4269            account_id,
4270            StrategyId::from("EXTERNAL"),
4271            PositionId::from("P-EXTERNAL-2"),
4272            OrderSide::Buy,
4273            Quantity::from(250),
4274        );
4275        let report = PositionStatusReport::new(
4276            account_id,
4277            instrument.id(),
4278            PositionSideSpecified::Long,
4279            Quantity::from(1_500),
4280            UnixNanos::from(1_000_000),
4281            UnixNanos::from(1_000_000),
4282            None,
4283            None,
4284            None,
4285        );
4286
4287        let message = ExecutionEngine::netting_split_position_ownership_message(
4288            &report,
4289            &[&external_position, &strategy_position],
4290        )
4291        .expect("split ownership should produce a warning message");
4292
4293        assert!(message.contains("account_id=SIM-001"));
4294        assert!(message.contains(&format!("instrument_id={}", instrument.id())));
4295        assert!(message.contains("EXTERNAL"));
4296        assert!(message.contains("S-001"));
4297        assert!(message.contains("P-EXTERNAL"));
4298        assert!(message.contains("P-STRATEGY"));
4299        assert!(message.contains("signed_qty=1000"));
4300        assert!(message.contains("signed_qty=500"));
4301        assert!(
4302            ExecutionEngine::netting_split_position_ownership_message(
4303                &report,
4304                &[&external_position, &same_strategy_position],
4305            )
4306            .is_none()
4307        );
4308    }
4309
4310    #[rstest]
4311    fn materialize_external_order_rejects_venue_id_owned_by_another_order() {
4312        let cache = Rc::new(RefCell::new(Cache::default()));
4313        let venue_order_id = VenueOrderId::from("V-SHARED");
4314        let owner_id = ClientOrderId::from("O-OWNER");
4315        cache
4316            .borrow_mut()
4317            .add_venue_order_id(&owner_id, &venue_order_id, false)
4318            .unwrap();
4319        let engine = ExecutionEngine::new(
4320            Rc::new(RefCell::new(TestClock::new())),
4321            Rc::clone(&cache),
4322            None,
4323        );
4324        let instrument = InstrumentAny::CurrencyPair(audusd_sim());
4325        let claimant_id = ClientOrderId::from("O-CLAIMANT");
4326        let order = OrderTestBuilder::new(OrderType::Limit)
4327            .instrument_id(instrument.id())
4328            .client_order_id(claimant_id)
4329            .side(OrderSide::Buy)
4330            .quantity(Quantity::from(100_000))
4331            .price(Price::from("1.00000"))
4332            .build();
4333        let OrderEventAny::Initialized(initialized) = order.last_event().clone() else {
4334            panic!("Expected initialized order");
4335        };
4336
4337        let result = engine.materialize_external_order(
4338            initialized,
4339            claimant_id,
4340            venue_order_id,
4341            instrument.id(),
4342            order.strategy_id(),
4343            UnixNanos::default(),
4344            None,
4345            None,
4346        );
4347
4348        assert!(result.is_none());
4349        assert!(!cache.borrow().order_exists(&claimant_id));
4350        assert_eq!(
4351            cache.borrow().client_order_id(&venue_order_id),
4352            Some(&owner_id)
4353        );
4354        assert_eq!(cache.borrow().venue_order_id(&claimant_id), None);
4355    }
4356
4357    fn position_for_account(
4358        instrument: &InstrumentAny,
4359        account_id: AccountId,
4360        strategy_id: StrategyId,
4361        position_id: PositionId,
4362        order_side: OrderSide,
4363        quantity: Quantity,
4364    ) -> Position {
4365        let client_order_id = ClientOrderId::from(format!("O-{position_id}"));
4366        let fill = OrderFilledSpec::builder()
4367            .strategy_id(strategy_id)
4368            .instrument_id(instrument.id())
4369            .client_order_id(client_order_id)
4370            .venue_order_id(VenueOrderId::from(format!("V-{position_id}")))
4371            .account_id(account_id)
4372            .trade_id(TradeId::new(format!("T-{position_id}")))
4373            .order_side(order_side)
4374            .last_qty(quantity)
4375            .last_px(Price::from("1.0"))
4376            .currency(instrument.quote_currency())
4377            .liquidity_side(LiquiditySide::Maker)
4378            .position_id(position_id)
4379            .commission(Money::from("2 USD"))
4380            .build();
4381
4382        Position::new(instrument, fill)
4383    }
4384}