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