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