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