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