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