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