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