Skip to main content

nautilus_execution/engine/
mod.rs

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