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 UUID4, UnixNanos, WeakCell,
66 datetime::{mins_to_nanos, mins_to_secs, secs_to_nanos},
67};
68use nautilus_model::{
69 accounts::Account,
70 enums::{
71 ContingencyType, OmsType, OrderSide, OrderStatus, OrderType, PositionSide, TimeInForce,
72 TrailingOffsetType,
73 },
74 events::{
75 OrderAccepted, OrderDenied, OrderDeniedReason, OrderEvent, OrderEventAny, OrderFillVoided,
76 OrderFilled, OrderInitialized, PositionChanged, PositionClosed, PositionEvent,
77 PositionOpened,
78 },
79 identifiers::{
80 AccountId, ClientId, ClientOrderId, InstrumentId, PositionId, StrategyId, TradeId, Venue,
81 VenueOrderId,
82 },
83 instruments::{Instrument, InstrumentAny},
84 orderbook::own::{OwnBookOrder, OwnOrderBook, should_handle_own_book_order},
85 orders::{Order, OrderAny, OrderError},
86 position::{Position, PositionReplayEvent},
87 reports::{ExecutionMassStatus, FillReport, OrderStatusReport, PositionStatusReport},
88 types::{Money, Quantity},
89};
90use position::CorrectedPosition;
91pub use position::{PositionStateSnapshot, SnapshotAnchorer};
92use rust_decimal::Decimal;
93
94use crate::{
95 client::ExecutionClientAdapter,
96 reconciliation::{
97 check_position_reconciliation, generate_external_order_status_events,
98 generate_reconciliation_order_events, generate_reconciliation_order_pre_fill_events,
99 generate_reconciliation_order_snapshot_events, reconcile_fill_report as reconcile_fill,
100 },
101};
102
103const TIMER_SNAPSHOT_POSITIONS: &str = "ExecEngine_SNAPSHOT_POSITIONS";
104const TIMER_PURGE_CLOSED_ORDERS: &str = "ExecEngine_PURGE_CLOSED_ORDERS";
105const TIMER_PURGE_CLOSED_POSITIONS: &str = "ExecEngine_PURGE_CLOSED_POSITIONS";
106const TIMER_PURGE_ACCOUNT_EVENTS: &str = "ExecEngine_PURGE_ACCOUNT_EVENTS";
107
108pub struct ExecutionEngine {
115 clock: Rc<RefCell<dyn Clock>>,
116 cache: Rc<RefCell<Cache>>,
117 clients: IndexMap<ClientId, ExecutionClientAdapter>,
118 default_client_id: Option<ClientId>,
119 routing_map: HashMap<Venue, ClientId>,
120 oms_overrides: HashMap<StrategyId, OmsType>,
121 external_order_claims: HashMap<InstrumentId, StrategyId>,
122 external_clients: HashSet<ClientId>,
123 pos_id_generator: PositionIdGenerator,
124 config: ExecutionEngineConfig,
125 command_count: Cell<u64>,
126 event_count: u64,
127 report_count: u64,
128 filtered_unclaimed_external_order_count: u64,
129 snapshot_anchorer: Option<SnapshotAnchorer>,
130}
131
132impl Debug for ExecutionEngine {
133 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134 f.debug_struct(stringify!(ExecutionEngine))
135 .field("client_count", &self.clients.len())
136 .finish()
137 }
138}
139
140impl ExecutionEngine {
141 pub fn new(
143 clock: Rc<RefCell<dyn Clock>>,
144 cache: Rc<RefCell<Cache>>,
145 config: Option<ExecutionEngineConfig>,
146 ) -> Self {
147 let trader_id = get_message_bus().borrow().trader_id;
148 Self {
149 clock: clock.clone(),
150 cache,
151 clients: IndexMap::new(),
152 default_client_id: None,
153 routing_map: HashMap::new(),
154 oms_overrides: HashMap::new(),
155 external_order_claims: HashMap::new(),
156 external_clients: config
157 .as_ref()
158 .and_then(|c| c.external_clients.clone())
159 .unwrap_or_default()
160 .into_iter()
161 .collect(),
162 pos_id_generator: PositionIdGenerator::new(trader_id, clock),
163 config: config.unwrap_or_default(),
164 command_count: Cell::new(0),
165 event_count: 0,
166 report_count: 0,
167 filtered_unclaimed_external_order_count: 0,
168 snapshot_anchorer: None,
169 }
170 }
171
172 pub fn register_msgbus_handlers(engine: &Rc<RefCell<Self>>) {
174 let weak = WeakCell::from(Rc::downgrade(engine));
175
176 let weak1 = weak.clone();
177 msgbus::register_trading_command_endpoint(
178 MessagingSwitchboard::exec_engine_execute(),
179 TypedIntoHandler::from(move |cmd: TradingCommand| {
180 if let Some(rc) = weak1.upgrade() {
181 rc.borrow().execute(cmd);
182 }
183 }),
184 );
185
186 msgbus::register_trading_command_endpoint(
189 MessagingSwitchboard::exec_engine_queue_execute(),
190 TypedIntoHandler::from(move |cmd: TradingCommand| {
191 let endpoint = MessagingSwitchboard::exec_engine_execute();
192 if trading_cmd_is_dispatching() {
193 capture_trading_cmd(TradingCommandMessage::new(endpoint, cmd));
194 } else if let Some(sender) = try_get_trading_cmd_sender() {
195 sender.execute(TradingCommandMessage::new(endpoint, cmd));
196 } else {
197 msgbus::send_trading_command(endpoint, cmd);
198 }
199 }),
200 );
201
202 let weak2 = weak.clone();
203 msgbus::register_order_event_endpoint(
204 MessagingSwitchboard::exec_engine_process(),
205 TypedIntoHandler::from(move |event: OrderEventAny| {
206 if let Some(rc) = weak2.upgrade() {
207 rc.borrow_mut().process(&event);
208 }
209 }),
210 );
211
212 let weak3 = weak;
213 msgbus::register_execution_report_endpoint(
214 MessagingSwitchboard::exec_engine_reconcile_execution_report(),
215 TypedIntoHandler::from(move |report: ExecutionReport| {
216 if let Some(rc) = weak3.upgrade() {
217 rc.borrow_mut().reconcile_execution_report(&report);
218 }
219 }),
220 );
221 }
222
223 #[must_use]
225 pub fn command_count(&self) -> u64 {
226 self.command_count.get()
227 }
228
229 #[must_use]
231 pub const fn event_count(&self) -> u64 {
232 self.event_count
233 }
234
235 #[must_use]
237 pub const fn report_count(&self) -> u64 {
238 self.report_count
239 }
240
241 #[must_use]
243 pub const fn filtered_unclaimed_external_order_count(&self) -> u64 {
244 self.filtered_unclaimed_external_order_count
245 }
246
247 pub fn subscribe_venue_instruments(engine: &Rc<RefCell<Self>>, venue: Venue) {
252 let weak = WeakCell::from(Rc::downgrade(engine));
253 let pattern = switchboard::get_instruments_pattern(venue);
254
255 let handler = TypedHandler::from(move |instrument: &InstrumentAny| {
256 if let Some(rc) = weak.upgrade() {
257 let venue = instrument.id().venue;
258 let client_id = rc.borrow().routing_map.get(&venue).copied();
259 if let Some(client_id) = client_id {
260 let mut engine = rc.borrow_mut();
261 if let Some(adapter) = engine.get_client_adapter_mut(&client_id) {
262 adapter.on_instrument(instrument.clone());
263 }
264 }
265 }
266 });
267
268 msgbus::subscribe_instruments(pattern, handler, None);
269 log::info!("Subscribed to instrument updates for venue {venue}");
270 }
271
272 #[must_use]
273 pub fn position_id_count(&self, strategy_id: StrategyId) -> usize {
275 self.pos_id_generator.count(strategy_id)
276 }
277
278 #[must_use]
279 pub fn cache(&self) -> &Rc<RefCell<Cache>> {
281 &self.cache
282 }
283
284 #[must_use]
285 pub const fn config(&self) -> &ExecutionEngineConfig {
287 &self.config
288 }
289
290 pub fn set_snapshot_anchorer(&mut self, anchorer: Option<SnapshotAnchorer>) {
295 self.snapshot_anchorer = anchorer;
296 }
297
298 #[must_use]
299 pub fn check_integrity(&self) -> bool {
301 self.cache.borrow_mut().check_integrity()
302 }
303
304 #[must_use]
305 pub fn check_connected(&self) -> bool {
307 self.clients.values().all(|c| c.is_connected())
308 }
309
310 #[must_use]
311 pub fn check_disconnected(&self) -> bool {
313 self.clients.values().all(|c| !c.is_connected())
314 }
315
316 #[must_use]
318 pub fn client_connection_status(&self) -> Vec<(ClientId, bool)> {
319 self.clients
320 .values()
321 .map(|c| (c.client_id(), c.is_connected()))
322 .collect()
323 }
324
325 #[must_use]
326 pub fn check_residuals(&self) -> bool {
328 self.cache.borrow().check_residuals()
329 }
330
331 #[must_use]
332 pub fn get_external_order_claims_instruments(&self) -> HashSet<InstrumentId> {
334 self.external_order_claims.keys().copied().collect()
335 }
336
337 #[must_use]
338 pub fn get_external_client_ids(&self) -> HashSet<ClientId> {
340 self.external_clients.clone()
341 }
342
343 #[must_use]
344 pub fn get_external_order_claim(&self, instrument_id: &InstrumentId) -> Option<StrategyId> {
346 self.external_order_claims.get(instrument_id).copied()
347 }
348
349 pub fn register_client(&mut self, client: Box<dyn ExecutionClient>) -> anyhow::Result<()> {
355 let client_id = client.client_id();
356 let venue = client.venue();
357
358 if self.clients.contains_key(&client_id) {
359 anyhow::bail!("Client already registered with ID {client_id}");
360 }
361
362 let adapter = ExecutionClientAdapter::new(client);
363
364 if let Some(existing_client_id) = self.routing_map.get(&venue) {
365 anyhow::bail!(
366 "Venue {venue} already routed to {existing_client_id}, \
367 cannot register {client_id} for the same venue"
368 );
369 }
370
371 self.routing_map.insert(venue, client_id);
372 log::debug!("Registered client {client_id}");
373 self.clients.insert(client_id, adapter);
374 Ok(())
375 }
376
377 pub fn register_default_client(&mut self, client: Box<dyn ExecutionClient>) {
379 let client_id = client.client_id();
380 let adapter = ExecutionClientAdapter::new(client);
381
382 self.clients.insert(client_id, adapter);
383 self.default_client_id = Some(client_id);
384 log::debug!("Registered default client {client_id}");
385 }
386
387 pub fn set_default_client(&mut self, client_id: ClientId) -> anyhow::Result<()> {
394 if self.default_client_id.is_some() {
395 anyhow::bail!("default client already registered");
396 }
397
398 if !self.clients.contains_key(&client_id) {
399 anyhow::bail!("No client registered with ID {client_id}");
400 }
401 self.default_client_id = Some(client_id);
402 log::debug!("Set client {client_id} as default");
403 Ok(())
404 }
405
406 #[must_use]
407 pub fn get_client(&self, client_id: &ClientId) -> Option<&dyn ExecutionClient> {
409 self.clients.get(client_id).map(|a| a.client.as_ref())
410 }
411
412 #[must_use]
413 pub fn get_client_adapter_mut(
415 &mut self,
416 client_id: &ClientId,
417 ) -> Option<&mut ExecutionClientAdapter> {
418 self.clients.get_mut(client_id)
419 }
420
421 pub async fn generate_mass_status(
427 &mut self,
428 client_id: &ClientId,
429 lookback_mins: Option<u64>,
430 ) -> anyhow::Result<Option<ExecutionMassStatus>> {
431 if let Some(client) = self.get_client_adapter_mut(client_id) {
432 client.generate_mass_status(lookback_mins).await
433 } else {
434 anyhow::bail!("Client {client_id} not found")
435 }
436 }
437
438 pub fn register_external_order(
443 &self,
444 client_order_id: ClientOrderId,
445 venue_order_id: VenueOrderId,
446 instrument_id: InstrumentId,
447 strategy_id: StrategyId,
448 ts_init: UnixNanos,
449 ) {
450 let venue = instrument_id.venue;
451 let client_id = self
452 .routing_map
453 .get(&venue)
454 .copied()
455 .or(self.default_client_id);
456
457 if let Some(client_id) = client_id
458 && let Some(client) = self.clients.get(&client_id)
459 {
460 client.register_external_order(
461 client_order_id,
462 venue_order_id,
463 instrument_id,
464 strategy_id,
465 ts_init,
466 );
467 }
468 }
469
470 #[must_use]
471 pub fn client_ids(&self) -> Vec<ClientId> {
473 self.clients.keys().copied().collect()
474 }
475
476 #[must_use]
477 pub fn get_clients_mut(&mut self) -> Vec<&mut ExecutionClientAdapter> {
479 self.clients.values_mut().collect()
480 }
481
482 #[must_use]
484 pub fn get_all_clients(&self) -> Vec<&dyn ExecutionClient> {
485 self.clients.values().map(|a| a.client.as_ref()).collect()
486 }
487
488 #[must_use]
489 pub fn get_clients_for_orders(&self, orders: &[OrderAny]) -> Vec<&dyn ExecutionClient> {
494 let mut client_ids: IndexSet<ClientId> = IndexSet::new();
495 let mut venues: IndexSet<Venue> = IndexSet::new();
496
497 for order in orders {
499 venues.insert(order.instrument_id().venue);
500 if let Some(client_id) = self.cache.borrow().client_id(&order.client_order_id()) {
501 client_ids.insert(*client_id);
502 }
503 }
504
505 let mut clients: Vec<&dyn ExecutionClient> = Vec::new();
506
507 for client_id in &client_ids {
509 if let Some(adapter) = self.clients.get(client_id)
510 && !clients.iter().any(|c| c.client_id() == adapter.client_id)
511 {
512 clients.push(adapter.client.as_ref());
513 }
514 }
515
516 for venue in &venues {
518 let resolved_id = self
519 .routing_map
520 .get(venue)
521 .copied()
522 .or(self.default_client_id);
523
524 if let Some(adapter) = resolved_id.and_then(|id| self.clients.get(&id))
525 && !clients.iter().any(|c| c.client_id() == adapter.client_id)
526 {
527 clients.push(adapter.client.as_ref());
528 }
529 }
530
531 clients
532 }
533
534 pub fn register_venue_routing(
540 &mut self,
541 client_id: ClientId,
542 venue: Venue,
543 ) -> anyhow::Result<()> {
544 if !self.clients.contains_key(&client_id) {
545 anyhow::bail!("No client registered with ID {client_id}");
546 }
547
548 if let Some(existing_client_id) = self.routing_map.get(&venue)
549 && *existing_client_id != client_id
550 {
551 anyhow::bail!(
552 "Venue {venue} already routed to {existing_client_id}, \
553 cannot re-route to {client_id}"
554 );
555 }
556
557 self.routing_map.insert(venue, client_id);
558 log::info!("Set client {client_id} routing for {venue}");
559 Ok(())
560 }
561
562 pub fn register_oms_type(&mut self, strategy_id: StrategyId, oms_type: OmsType) {
566 self.oms_overrides.insert(strategy_id, oms_type);
567 log::info!("Registered OMS::{oms_type:?} for {strategy_id}");
568 }
569
570 pub fn register_external_order_claims(
581 &mut self,
582 strategy_id: StrategyId,
583 instrument_ids: &HashSet<InstrumentId>,
584 ) -> anyhow::Result<()> {
585 for instrument_id in instrument_ids {
587 if let Some(existing) = self.external_order_claims.get(instrument_id) {
588 anyhow::bail!(
589 "External order claim for {instrument_id} already exists for {existing}"
590 );
591 }
592 }
593
594 for instrument_id in instrument_ids {
596 self.external_order_claims
597 .insert(*instrument_id, strategy_id);
598 }
599
600 if !instrument_ids.is_empty() {
601 log::info!("Registered external order claims for {strategy_id}: {instrument_ids:?}");
602 }
603
604 Ok(())
605 }
606
607 pub fn deregister_client(&mut self, client_id: ClientId) -> anyhow::Result<()> {
611 if self.clients.shift_remove(&client_id).is_some() {
612 if self.default_client_id == Some(client_id) {
613 self.default_client_id = None;
614 }
615
616 self.routing_map
618 .retain(|_, mapped_id| mapped_id != &client_id);
619 log::info!("Deregistered client {client_id}");
620 Ok(())
621 } else {
622 anyhow::bail!("No client registered with ID {client_id}")
623 }
624 }
625
626 pub async fn connect(&mut self) {
630 let futures: Vec<_> = self
631 .get_clients_mut()
632 .into_iter()
633 .map(ExecutionClientAdapter::connect)
634 .collect();
635
636 let results = join_all(futures).await;
637
638 for error in results.into_iter().filter_map(Result::err) {
639 log::error!("Failed to connect execution client: {error:#}");
640 }
641 }
642
643 pub async fn disconnect(&mut self) -> anyhow::Result<()> {
649 let futures: Vec<_> = self
650 .get_clients_mut()
651 .into_iter()
652 .map(ExecutionClientAdapter::disconnect)
653 .collect();
654
655 let results = join_all(futures).await;
656 let errors: Vec<_> = results.into_iter().filter_map(Result::err).collect();
657
658 if errors.is_empty() {
659 Ok(())
660 } else {
661 let error_msgs: Vec<_> = errors.iter().map(ToString::to_string).collect();
662 anyhow::bail!(
663 "Failed to disconnect execution clients: {}",
664 error_msgs.join("; ")
665 )
666 }
667 }
668
669 pub fn set_manage_own_order_books(&mut self, value: bool) {
671 self.config.manage_own_order_books = value;
672 }
673
674 #[expect(
676 clippy::missing_panics_doc,
677 reason = "timer registration is not expected to fail"
678 )]
679 pub fn start_snapshot_timer(&mut self) {
680 if let Some(interval_secs) = self
681 .config
682 .snapshot_positions_interval_secs
683 .filter(|&secs| secs > 0.0)
684 && !self
685 .clock
686 .borrow()
687 .timer_names()
688 .contains(&TIMER_SNAPSHOT_POSITIONS)
689 {
690 let interval_ns = match secs_to_nanos(interval_secs) {
691 Ok(ns) => ns,
692 Err(e) => {
693 log::error!("Cannot start position snapshots timer: {e}");
694 return;
695 }
696 };
697 let clock = self.clock.clone();
698 let cache = self.cache.clone();
699 let debug = self.config.debug;
700
701 let callback_fn: Rc<dyn Fn(TimeEvent)> = Rc::new(move |_event| {
702 Self::snapshot_open_positions(&clock, &cache, debug);
703 });
704 let callback = TimeEventCallback::from(callback_fn);
705
706 log::info!("Starting position snapshots timer at {interval_secs} second intervals");
707 self.clock
708 .borrow_mut()
709 .set_timer_ns(
710 TIMER_SNAPSHOT_POSITIONS,
711 interval_ns,
712 None,
713 None,
714 Some(callback),
715 None,
716 None,
717 )
718 .expect("Failed to set position snapshots timer");
719 }
720 }
721
722 pub fn stop_snapshot_timer(&mut self) {
724 let timer_registered = self
725 .clock
726 .borrow()
727 .timer_names()
728 .contains(&TIMER_SNAPSHOT_POSITIONS);
729
730 if timer_registered {
731 log::info!("Canceling position snapshots timer");
732 self.clock
733 .borrow_mut()
734 .cancel_timer(TIMER_SNAPSHOT_POSITIONS);
735 }
736 }
737
738 #[expect(
740 clippy::missing_panics_doc,
741 reason = "timer registration is not expected to fail"
742 )]
743 pub fn start_purge_timers(&mut self) {
744 if let Some(interval_mins) = self
745 .config
746 .purge_closed_orders_interval_mins
747 .filter(|&m| m > 0)
748 && !self
749 .clock
750 .borrow()
751 .timer_names()
752 .contains(&TIMER_PURGE_CLOSED_ORDERS)
753 {
754 let interval_ns = mins_to_nanos(u64::from(interval_mins));
755 let buffer_mins = self.config.purge_closed_orders_buffer_mins.unwrap_or(0);
756 let buffer_secs = mins_to_secs(u64::from(buffer_mins));
757 let cache = self.cache.clone();
758 let clock = self.clock.clone();
759
760 let callback_fn: Rc<dyn Fn(TimeEvent)> = Rc::new(move |_event| {
761 let ts_now = clock.borrow().timestamp_ns();
762 cache.borrow_mut().purge_closed_orders(ts_now, buffer_secs);
763 });
764 let callback = TimeEventCallback::from(callback_fn);
765
766 log::info!("Starting purge closed orders timer at {interval_mins} minute intervals");
767 self.clock
768 .borrow_mut()
769 .set_timer_ns(
770 TIMER_PURGE_CLOSED_ORDERS,
771 interval_ns,
772 None,
773 None,
774 Some(callback),
775 None,
776 None,
777 )
778 .expect("Failed to set purge closed orders timer");
779 }
780
781 if let Some(interval_mins) = self
782 .config
783 .purge_closed_positions_interval_mins
784 .filter(|&m| m > 0)
785 && !self
786 .clock
787 .borrow()
788 .timer_names()
789 .contains(&TIMER_PURGE_CLOSED_POSITIONS)
790 {
791 let interval_ns = mins_to_nanos(u64::from(interval_mins));
792 let buffer_mins = self.config.purge_closed_positions_buffer_mins.unwrap_or(0);
793 let buffer_secs = mins_to_secs(u64::from(buffer_mins));
794 let cache = self.cache.clone();
795 let clock = self.clock.clone();
796
797 let callback_fn: Rc<dyn Fn(TimeEvent)> = Rc::new(move |_event| {
798 let ts_now = clock.borrow().timestamp_ns();
799 cache
800 .borrow_mut()
801 .purge_closed_positions(ts_now, buffer_secs);
802 });
803 let callback = TimeEventCallback::from(callback_fn);
804
805 log::info!("Starting purge closed positions timer at {interval_mins} minute intervals");
806 self.clock
807 .borrow_mut()
808 .set_timer_ns(
809 TIMER_PURGE_CLOSED_POSITIONS,
810 interval_ns,
811 None,
812 None,
813 Some(callback),
814 None,
815 None,
816 )
817 .expect("Failed to set purge closed positions timer");
818 }
819
820 if let Some(interval_mins) = self
821 .config
822 .purge_account_events_interval_mins
823 .filter(|&m| m > 0)
824 && !self
825 .clock
826 .borrow()
827 .timer_names()
828 .contains(&TIMER_PURGE_ACCOUNT_EVENTS)
829 {
830 let interval_ns = mins_to_nanos(u64::from(interval_mins));
831 let lookback_mins = self.config.purge_account_events_lookback_mins.unwrap_or(0);
832 let lookback_secs = mins_to_secs(u64::from(lookback_mins));
833 let cache = self.cache.clone();
834 let clock = self.clock.clone();
835
836 let callback_fn: Rc<dyn Fn(TimeEvent)> = Rc::new(move |_event| {
837 let ts_now = clock.borrow().timestamp_ns();
838 cache
839 .borrow_mut()
840 .purge_account_events(ts_now, lookback_secs);
841 });
842 let callback = TimeEventCallback::from(callback_fn);
843
844 log::info!("Starting purge account events timer at {interval_mins} minute intervals");
845 self.clock
846 .borrow_mut()
847 .set_timer_ns(
848 TIMER_PURGE_ACCOUNT_EVENTS,
849 interval_ns,
850 None,
851 None,
852 Some(callback),
853 None,
854 None,
855 )
856 .expect("Failed to set purge account events timer");
857 }
858 }
859
860 pub fn stop_purge_timers(&mut self) {
862 let timer_names: Vec<String> = self
863 .clock
864 .borrow()
865 .timer_names()
866 .into_iter()
867 .map(String::from)
868 .collect();
869
870 if timer_names.iter().any(|n| n == TIMER_PURGE_CLOSED_ORDERS) {
871 log::info!("Canceling purge closed orders timer");
872 self.clock
873 .borrow_mut()
874 .cancel_timer(TIMER_PURGE_CLOSED_ORDERS);
875 }
876
877 if timer_names
878 .iter()
879 .any(|n| n == TIMER_PURGE_CLOSED_POSITIONS)
880 {
881 log::info!("Canceling purge closed positions timer");
882 self.clock
883 .borrow_mut()
884 .cancel_timer(TIMER_PURGE_CLOSED_POSITIONS);
885 }
886
887 if timer_names.iter().any(|n| n == TIMER_PURGE_ACCOUNT_EVENTS) {
888 log::info!("Canceling purge account events timer");
889 self.clock
890 .borrow_mut()
891 .cancel_timer(TIMER_PURGE_ACCOUNT_EVENTS);
892 }
893 }
894
895 pub fn snapshot_open_position_states(&self) {
897 Self::snapshot_open_positions(&self.clock, &self.cache, self.config.debug);
898 }
899
900 fn snapshot_open_positions(
901 clock: &Rc<RefCell<dyn Clock>>,
902 cache: &Rc<RefCell<Cache>>,
903 debug: bool,
904 ) {
905 let positions: Vec<Position> = cache
906 .borrow()
907 .positions_open(None, None, None, None, None)
908 .into_iter()
909 .map(|p| p.cloned())
910 .collect();
911
912 for position in positions {
913 Self::publish_position_state_snapshot(clock, cache, debug, &position, true);
914 }
915 }
916
917 #[expect(clippy::await_holding_refcell_ref)]
918 pub async fn load_cache(&mut self) -> anyhow::Result<()> {
924 let ts = SystemTime::now(); {
927 let mut cache = self.cache.borrow_mut();
928 cache.clear_index();
929 cache.cache_general()?;
930 }
931
932 self.cache.borrow_mut().cache_all().await?;
933
934 let own_book_entries: Vec<(InstrumentId, OwnBookOrder)> = {
936 let mut cache = self.cache.borrow_mut();
937 cache.build_index();
938 let _ = cache.check_integrity();
939
940 if self.config.manage_own_order_books {
941 cache
942 .orders(None, None, None, None, None)
943 .into_iter()
944 .filter(|o| !o.is_closed() && should_handle_own_book_order(o))
945 .map(|o| (o.instrument_id(), o.to_own_book_order()))
946 .collect()
947 } else {
948 Vec::new()
949 }
950 };
951
952 for (instrument_id, own_order) in own_book_entries {
953 let mut own_book = self.get_or_init_own_order_book(&instrument_id);
954 own_book.add(own_order);
955 }
956
957 self.set_position_id_counts();
958
959 log::info!(
960 "Loaded cache in {}ms",
961 SystemTime::now() .duration_since(ts)
963 .map_err(|e| anyhow::anyhow!("Failed to calculate duration: {e}"))?
964 .as_millis()
965 );
966
967 Ok(())
968 }
969
970 pub fn flush_db(&self) {
972 self.cache.borrow_mut().flush_db();
973 }
974
975 pub fn reconcile_execution_report(&mut self, report: &ExecutionReport) {
977 if !matches!(report, ExecutionReport::MassStatus(_)) {
978 self.report_count += 1;
979 }
980
981 match report {
982 ExecutionReport::Order(order_report) => {
983 self.reconcile_order_status_report(order_report);
984 }
985 ExecutionReport::Fill(fill_report) => {
986 self.reconcile_fill_report(fill_report);
987 }
988 ExecutionReport::OrderWithFills(order_report, fills) => {
989 self.reconcile_order_with_fills(order_report, fills);
990 }
991 ExecutionReport::Position(position_report) => {
992 self.reconcile_position_report(position_report);
993 }
994 ExecutionReport::MassStatus(mass_status) => {
995 self.reconcile_execution_mass_status(mass_status);
996 }
997 }
998 }
999
1000 pub fn reconcile_order_status_report(&mut self, report: &OrderStatusReport) {
1010 msgbus::publish_any(
1011 MessagingSwitchboard::reconciliation_raw_order_status_report_topic(),
1012 report,
1013 );
1014
1015 let cache = self.cache.borrow();
1016
1017 let order = report
1018 .client_order_id
1019 .and_then(|id| cache.order(&id).map(|o| o.clone()))
1020 .or_else(|| {
1021 cache
1022 .client_order_id(&report.venue_order_id)
1023 .and_then(|cid| cache.order(cid).map(|o| o.clone()))
1024 });
1025
1026 let instrument = cache.instrument(&report.instrument_id).cloned();
1027
1028 drop(cache);
1029
1030 if let Some(order) = order {
1031 let ts_now = self.clock.borrow().timestamp_ns();
1032 let events =
1033 generate_reconciliation_order_events(&order, report, instrument.as_ref(), ts_now);
1034
1035 for event in &events {
1036 self.handle_event(event);
1037 }
1038 } else {
1039 self.create_external_order(report, instrument.as_ref());
1040 }
1041 }
1042
1043 fn create_external_order(
1044 &mut self,
1045 report: &OrderStatusReport,
1046 instrument: Option<&InstrumentAny>,
1047 ) {
1048 let Some(instrument) = instrument else {
1049 log::warn!(
1050 "Cannot create external order for venue_order_id={}: instrument {} not found",
1051 report.venue_order_id,
1052 report.instrument_id
1053 );
1054 return;
1055 };
1056
1057 let Some(order) = self.materialize_external_order_from_status(report) else {
1058 return;
1059 };
1060
1061 let ts_now = self.clock.borrow().timestamp_ns();
1062 let events = generate_external_order_status_events(
1063 &order,
1064 report,
1065 &report.account_id,
1066 instrument,
1067 ts_now,
1068 );
1069
1070 for event in &events {
1071 self.handle_event(event);
1072 }
1073 }
1074
1075 fn materialize_external_order_from_status(
1078 &mut self,
1079 report: &OrderStatusReport,
1080 ) -> Option<OrderAny> {
1081 let strategy_id = self.resolve_external_strategy(&report.instrument_id);
1082 if self.should_filter_unclaimed_external_order(strategy_id) {
1083 self.filtered_unclaimed_external_order_count += 1;
1084
1085 if self.filtered_unclaimed_external_order_count == 1 {
1086 let external_order_id = report
1087 .client_order_id
1088 .map_or_else(|| report.venue_order_id.to_string(), |id| id.to_string());
1089 log::info!(
1090 "Filtering unclaimed external orders; first filtered order {} ({}) for {}",
1091 external_order_id,
1092 report.venue_order_id,
1093 report.instrument_id,
1094 );
1095 } else {
1096 let external_order_id = report
1097 .client_order_id
1098 .map_or_else(|| report.venue_order_id.to_string(), |id| id.to_string());
1099 log::debug!(
1100 "Filtered unclaimed external order {} ({}) for {}",
1101 external_order_id,
1102 report.venue_order_id,
1103 report.instrument_id,
1104 );
1105 }
1106
1107 return None;
1108 }
1109
1110 self.materialize_external_order_from_status_with_strategy(report, strategy_id)
1111 }
1112
1113 fn materialize_external_order_from_status_with_strategy(
1114 &self,
1115 report: &OrderStatusReport,
1116 strategy_id: StrategyId,
1117 ) -> Option<OrderAny> {
1118 let client_order_id = report
1119 .client_order_id
1120 .unwrap_or_else(|| ClientOrderId::from(report.venue_order_id.as_str()));
1121
1122 let trader_id = get_message_bus().borrow().trader_id;
1123 let ts_now = self.clock.borrow().timestamp_ns();
1124
1125 let initialized = match OrderInitialized::new_checked(
1126 trader_id,
1127 strategy_id,
1128 report.instrument_id,
1129 client_order_id,
1130 report.order_side,
1131 report.order_type,
1132 report.quantity,
1133 report.time_in_force,
1134 report.post_only,
1135 report.reduce_only,
1136 false, true, UUID4::new(),
1139 ts_now,
1140 ts_now,
1141 report.price,
1142 report.activation_price,
1143 report.trigger_price,
1144 report.trigger_type,
1145 report.limit_offset,
1146 report.trailing_offset,
1147 Some(report.trailing_offset_type),
1148 report.expire_time,
1149 report.display_qty,
1150 None, None, Some(report.contingency_type),
1153 report.order_list_id,
1154 report.linked_order_ids.clone(),
1155 report.parent_order_id,
1156 None, None, None, None, ) {
1161 Ok(initialized) => initialized,
1162 Err(e) => {
1163 log::error!("Failed to create external order from report: {e}");
1164 return None;
1165 }
1166 };
1167
1168 self.materialize_external_order(
1169 initialized,
1170 client_order_id,
1171 report.venue_order_id,
1172 report.instrument_id,
1173 strategy_id,
1174 ts_now,
1175 Some(report.order_status),
1176 )
1177 }
1178
1179 fn materialize_external_order_from_fill(&mut self, report: &FillReport) -> Option<OrderAny> {
1187 let strategy_id = self.resolve_external_strategy(&report.instrument_id);
1188 if self.should_filter_unclaimed_external_order(strategy_id) {
1189 self.filtered_unclaimed_external_order_count += 1;
1190
1191 let external_order_id = report
1192 .client_order_id
1193 .map_or_else(|| report.venue_order_id.to_string(), |id| id.to_string());
1194
1195 if self.filtered_unclaimed_external_order_count == 1 {
1196 log::info!(
1197 "Filtering unclaimed external orders; first filtered fill {} ({}) for {}",
1198 external_order_id,
1199 report.venue_order_id,
1200 report.instrument_id,
1201 );
1202 } else {
1203 log::debug!(
1204 "Filtered unclaimed external fill {} ({}) for {}",
1205 external_order_id,
1206 report.venue_order_id,
1207 report.instrument_id,
1208 );
1209 }
1210
1211 return None;
1212 }
1213
1214 let client_order_id = report
1215 .client_order_id
1216 .unwrap_or_else(|| ClientOrderId::from(report.venue_order_id.as_str()));
1217
1218 let trader_id = get_message_bus().borrow().trader_id;
1219 let ts_now = self.clock.borrow().timestamp_ns();
1220
1221 let initialized = OrderInitialized::new(
1222 trader_id,
1223 strategy_id,
1224 report.instrument_id,
1225 client_order_id,
1226 report.order_side,
1227 OrderType::Market,
1228 report.last_qty,
1229 TimeInForce::Ioc,
1230 false, true, false, true, UUID4::new(),
1235 ts_now,
1236 ts_now,
1237 None, None, None, None, None, None, Some(TrailingOffsetType::NoTrailingOffset),
1244 None, None, None, None, Some(ContingencyType::NoContingency),
1249 None, None, None, None, None, None, None, );
1257
1258 self.materialize_external_order(
1259 initialized,
1260 client_order_id,
1261 report.venue_order_id,
1262 report.instrument_id,
1263 strategy_id,
1264 ts_now,
1265 None,
1266 )
1267 }
1268
1269 fn resolve_external_strategy(&self, instrument_id: &InstrumentId) -> StrategyId {
1270 self.external_order_claims
1271 .get(instrument_id)
1272 .copied()
1273 .unwrap_or_else(StrategyId::external)
1274 }
1275
1276 fn should_filter_unclaimed_external_order(&self, strategy_id: StrategyId) -> bool {
1277 self.config.filter_unclaimed_external_orders && strategy_id.is_external()
1278 }
1279
1280 #[allow(
1283 clippy::too_many_arguments,
1284 reason = "external order materialisation threads several ids and a timestamp"
1285 )]
1286 fn materialize_external_order(
1287 &self,
1288 initialized: OrderInitialized,
1289 client_order_id: ClientOrderId,
1290 venue_order_id: VenueOrderId,
1291 instrument_id: InstrumentId,
1292 strategy_id: StrategyId,
1293 ts_now: UnixNanos,
1294 order_status: Option<OrderStatus>,
1295 ) -> Option<OrderAny> {
1296 let initialized = OrderEventAny::Initialized(initialized);
1297 let order = match OrderAny::from_events(vec![initialized.clone()]) {
1298 Ok(order) => order,
1299 Err(e) => {
1300 log::error!("Failed to create external order from report: {e}");
1301 return None;
1302 }
1303 };
1304
1305 {
1306 let mut cache = self.cache.borrow_mut();
1307 if let Err(e) = cache.add_venue_order_id(&client_order_id, &venue_order_id, false) {
1308 log::warn!("Failed to claim venue order ID for external order: {e}");
1309 return None;
1310 }
1311
1312 if let Err(e) = cache.add_order(order.clone(), None, None, false) {
1313 log::error!("Failed to add external order to cache: {e}");
1314 return None;
1315 }
1316 }
1317
1318 self.publish_order_event(&initialized);
1319
1320 match order_status {
1321 Some(status) => log::info!(
1322 "Created external order {client_order_id} ({venue_order_id}) for {instrument_id} [{status}]",
1323 ),
1324 None => log::info!(
1325 "Created external order {client_order_id} ({venue_order_id}) for {instrument_id}",
1326 ),
1327 }
1328
1329 self.register_external_order(
1330 client_order_id,
1331 venue_order_id,
1332 instrument_id,
1333 strategy_id,
1334 ts_now,
1335 );
1336
1337 Some(order)
1338 }
1339
1340 pub fn reconcile_fill_report(&mut self, report: &FillReport) {
1348 msgbus::publish_any(
1349 MessagingSwitchboard::reconciliation_raw_fill_report_topic(),
1350 report,
1351 );
1352
1353 let cache = self.cache.borrow();
1354
1355 let order = report
1356 .client_order_id
1357 .and_then(|id| cache.order(&id).map(|o| o.clone()))
1358 .or_else(|| {
1359 cache
1360 .client_order_id(&report.venue_order_id)
1361 .and_then(|cid| cache.order(cid).map(|o| o.clone()))
1362 });
1363
1364 let instrument = cache.instrument(&report.instrument_id).cloned();
1365
1366 drop(cache);
1367
1368 let Some(instrument) = instrument else {
1369 log::debug!(
1370 "Cannot reconcile fill report for venue_order_id={}: instrument {} not found",
1371 report.venue_order_id,
1372 report.instrument_id
1373 );
1374 return;
1375 };
1376
1377 let order = match order {
1378 Some(order) => order,
1379 None => {
1380 let Some(order) = self.materialize_external_order_from_fill(report) else {
1381 return;
1382 };
1383 let ts_now = self.clock.borrow().timestamp_ns();
1384 let accepted = OrderAccepted::new(
1385 order.trader_id(),
1386 order.strategy_id(),
1387 order.instrument_id(),
1388 order.client_order_id(),
1389 report.venue_order_id,
1390 report.account_id,
1391 UUID4::new(),
1392 report.ts_event,
1393 ts_now,
1394 true, );
1396 self.handle_event(&OrderEventAny::Accepted(accepted));
1397 self.cache
1398 .borrow()
1399 .order(&order.client_order_id())
1400 .map(|o| o.clone())
1401 .unwrap_or(order)
1402 }
1403 };
1404
1405 let ts_now = self.clock.borrow().timestamp_ns();
1406
1407 if let Some(event) = reconcile_fill(
1408 &order,
1409 report,
1410 &instrument,
1411 ts_now,
1412 self.config.allow_overfills,
1413 ) {
1414 self.handle_event(&event);
1415 }
1416 }
1417
1418 pub fn reconcile_order_with_fills(&mut self, report: &OrderStatusReport, fills: &[FillReport]) {
1427 msgbus::publish_any(
1428 MessagingSwitchboard::reconciliation_raw_order_status_report_topic(),
1429 report,
1430 );
1431
1432 let fill_report_topic = MessagingSwitchboard::reconciliation_raw_fill_report_topic();
1433 for fill in fills {
1434 msgbus::publish_any(fill_report_topic, fill);
1435 }
1436
1437 let cache = self.cache.borrow();
1438 let order = report
1439 .client_order_id
1440 .and_then(|id| cache.order(&id).map(|o| o.clone()))
1441 .or_else(|| {
1442 cache
1443 .client_order_id(&report.venue_order_id)
1444 .and_then(|cid| cache.order(cid).map(|o| o.clone()))
1445 });
1446 let instrument = cache.instrument(&report.instrument_id).cloned();
1447 drop(cache);
1448
1449 let Some(instrument) = instrument else {
1450 log::debug!(
1451 "Cannot reconcile bundled report for venue_order_id={}: instrument {} not found",
1452 report.venue_order_id,
1453 report.instrument_id,
1454 );
1455
1456 if fills.is_empty()
1457 && let Some(order) = order
1458 {
1459 let ts_now = self.clock.borrow().timestamp_ns();
1460 let events =
1461 generate_reconciliation_order_snapshot_events(&order, report, None, ts_now);
1462
1463 for event in &events {
1464 self.handle_event(event);
1465 }
1466 }
1467 return;
1468 };
1469
1470 let mut order = match order {
1473 Some(order) => {
1474 let ts_now = self.clock.borrow().timestamp_ns();
1475 let events = generate_reconciliation_order_pre_fill_events(&order, report, ts_now);
1476 for event in &events {
1477 self.handle_event(event);
1478 }
1479 self.cache
1480 .borrow()
1481 .order(&order.client_order_id())
1482 .map(|o| o.clone())
1483 .unwrap_or(order)
1484 }
1485 None => {
1486 let Some(order) = self.materialize_external_order_from_status(report) else {
1487 return;
1488 };
1489 let ts_now = self.clock.borrow().timestamp_ns();
1490 let accepted = OrderAccepted::new(
1491 order.trader_id(),
1492 order.strategy_id(),
1493 order.instrument_id(),
1494 order.client_order_id(),
1495 report.venue_order_id,
1496 report.account_id,
1497 UUID4::new(),
1498 report.ts_accepted,
1499 ts_now,
1500 true, );
1502 self.handle_event(&OrderEventAny::Accepted(accepted));
1503 self.cache
1504 .borrow()
1505 .order(&order.client_order_id())
1506 .map(|o| o.clone())
1507 .unwrap_or(order)
1508 }
1509 };
1510
1511 let client_order_id = order.client_order_id();
1512
1513 for fill in fills {
1514 let ts_now = self.clock.borrow().timestamp_ns();
1515
1516 if let Some(event) = reconcile_fill(
1517 &order,
1518 fill,
1519 &instrument,
1520 ts_now,
1521 self.config.allow_overfills,
1522 ) {
1523 self.handle_event(&event);
1524 }
1525
1526 if let Some(refreshed) = self
1528 .cache
1529 .borrow()
1530 .order(&client_order_id)
1531 .map(|o| o.clone())
1532 {
1533 order = refreshed;
1534 }
1535 }
1536
1537 let ts_now = self.clock.borrow().timestamp_ns();
1538 let events = generate_reconciliation_order_snapshot_events(
1539 &order,
1540 report,
1541 Some(&instrument),
1542 ts_now,
1543 );
1544
1545 for event in &events {
1546 self.handle_event(event);
1547 }
1548 }
1549
1550 pub fn reconcile_position_report(&mut self, report: &PositionStatusReport) {
1555 msgbus::publish_any(
1556 MessagingSwitchboard::reconciliation_raw_position_status_report_topic(),
1557 report,
1558 );
1559
1560 let cache = self.cache.borrow();
1561
1562 let size_precision = cache
1563 .instrument(&report.instrument_id)
1564 .map(InstrumentAny::size_precision);
1565
1566 if report.venue_position_id.is_some() {
1567 self.reconcile_position_report_hedging(report, &cache);
1568 } else {
1569 self.reconcile_position_report_netting(report, &cache, size_precision);
1570 }
1571 }
1572
1573 fn reconcile_position_report_hedging(&self, report: &PositionStatusReport, cache: &Cache) {
1574 let venue_position_id = report.venue_position_id.as_ref().unwrap();
1575
1576 log::debug!(
1577 "Reconciling HEDGE position for {}, venue_position_id={}",
1578 report.instrument_id,
1579 venue_position_id
1580 );
1581
1582 let Some(position) = cache.position(venue_position_id) else {
1583 log::error!("Cannot reconcile position: {venue_position_id} not found in cache");
1584 return;
1585 };
1586
1587 let cached_signed_qty = match position.side {
1588 PositionSide::Long => position.quantity.as_decimal(),
1589 PositionSide::Short => -position.quantity.as_decimal(),
1590 _ => Decimal::ZERO,
1591 };
1592 let venue_signed_qty = report.signed_decimal_qty;
1593
1594 if cached_signed_qty != venue_signed_qty {
1595 log::error!(
1596 "Position mismatch for {} {}: cached={}, venue={}",
1597 report.instrument_id,
1598 venue_position_id,
1599 cached_signed_qty,
1600 venue_signed_qty
1601 );
1602 }
1603 }
1604
1605 fn reconcile_position_report_netting(
1606 &self,
1607 report: &PositionStatusReport,
1608 cache: &Cache,
1609 size_precision: Option<u8>,
1610 ) {
1611 log::debug!("Reconciling NET position for {}", report.instrument_id);
1612
1613 let positions_open = Self::netting_positions_open_for_report(cache, report);
1614
1615 let position_refs = positions_open
1616 .iter()
1617 .map(|position| &**position)
1618 .collect::<Vec<_>>();
1619
1620 if let Some(message) =
1621 Self::netting_split_position_ownership_message(report, &position_refs)
1622 {
1623 log::warn!("{message}");
1624 }
1625
1626 let cached_signed_qty: Decimal = positions_open
1628 .iter()
1629 .map(|position| Self::position_signed_decimal_qty(position))
1630 .sum();
1631
1632 log::debug!(
1633 "Position report: venue_signed_qty={}, cached_signed_qty={}",
1634 report.signed_decimal_qty,
1635 cached_signed_qty
1636 );
1637
1638 let _ = check_position_reconciliation(report, cached_signed_qty, size_precision);
1639 }
1640
1641 fn netting_positions_open_for_report<'a>(
1642 cache: &'a Cache,
1643 report: &PositionStatusReport,
1644 ) -> Vec<PositionRef<'a>> {
1645 cache.positions_open(
1646 None,
1647 Some(&report.instrument_id),
1648 None,
1649 Some(&report.account_id),
1650 None,
1651 )
1652 }
1653
1654 fn netting_split_position_ownership_message(
1655 report: &PositionStatusReport,
1656 positions_open: &[&Position],
1657 ) -> Option<String> {
1658 let mut strategy_ids = positions_open
1659 .iter()
1660 .map(|position| position.strategy_id.to_string())
1661 .collect::<Vec<_>>();
1662 strategy_ids.sort();
1663 strategy_ids.dedup();
1664
1665 if strategy_ids.len() <= 1 {
1666 return None;
1667 }
1668
1669 let position_details = Self::position_details(positions_open.iter().copied());
1670
1671 Some(format!(
1672 "NETTING reconciliation found split ownership for account_id={}, instrument_id={}: \
1673 strategies=[{}], positions=[{}]",
1674 report.account_id,
1675 report.instrument_id,
1676 strategy_ids.join(", "),
1677 position_details
1678 ))
1679 }
1680
1681 pub fn reconcile_execution_mass_status(&mut self, mass_status: &ExecutionMassStatus) {
1687 self.report_count += 1;
1688
1689 log::info!(
1690 "Reconciling mass status for client={}, account={}, venue={}",
1691 mass_status.client_id,
1692 mass_status.account_id,
1693 mass_status.venue
1694 );
1695
1696 let order_reports = mass_status.order_reports();
1697 let fill_reports = mass_status.fill_reports();
1698 let mut paired_venue_ids = AHashSet::new();
1699
1700 for order_report in order_reports.values() {
1701 if let Some(fills) = fill_reports.get(&order_report.venue_order_id)
1702 && !fills.is_empty()
1703 {
1704 self.reconcile_order_with_fills(order_report, fills);
1705 paired_venue_ids.insert(order_report.venue_order_id);
1706 } else {
1707 self.reconcile_order_status_report(order_report);
1708 }
1709 }
1710
1711 for fill_reports in fill_reports.values() {
1712 for fill_report in fill_reports {
1713 if paired_venue_ids.contains(&fill_report.venue_order_id) {
1714 continue;
1715 }
1716
1717 self.reconcile_fill_report(fill_report);
1718 }
1719 }
1720
1721 for position_reports in mass_status.position_reports().values() {
1722 for position_report in position_reports {
1723 self.reconcile_position_report(position_report);
1724 }
1725 }
1726
1727 log::info!(
1728 "Mass status reconciliation complete: {} orders, {} fills, {} positions",
1729 mass_status.order_reports().len(),
1730 mass_status
1731 .fill_reports()
1732 .values()
1733 .map(Vec::len)
1734 .sum::<usize>(),
1735 mass_status
1736 .position_reports()
1737 .values()
1738 .map(Vec::len)
1739 .sum::<usize>()
1740 );
1741 }
1742
1743 pub fn execute(&self, command: TradingCommand) {
1745 self.execute_command(command);
1746 }
1747
1748 pub fn process(&mut self, event: &OrderEventAny) {
1750 self.handle_event(event);
1751 }
1752
1753 pub fn project_reconciliation_fill(&mut self, fill: &OrderFilled) {
1755 self.handle_event_with_position_application(&OrderEventAny::Filled(fill.clone()), false);
1756 }
1757
1758 pub fn start(&mut self) {
1760 for client in self.get_clients_mut() {
1761 if let Err(e) = client.start() {
1762 log::error!("{e}");
1763 }
1764 }
1765
1766 self.start_snapshot_timer();
1767 self.start_purge_timers();
1768
1769 log::info!("Started");
1770 }
1771
1772 pub fn stop(&mut self) {
1778 for client in self.get_clients_mut() {
1779 if let Err(e) = client.stop() {
1780 log::error!("{e}");
1781 }
1782 }
1783
1784 self.stop_snapshot_timer();
1785 self.stop_purge_timers();
1786
1787 log::info!("Stopped");
1788 }
1789
1790 pub fn stop_clients(&mut self) {
1792 for client in self.get_clients_mut() {
1793 if let Err(e) = client.stop() {
1794 log::error!("{e}");
1795 }
1796 }
1797 }
1798
1799 pub fn reset(&mut self) {
1804 for client in self.get_clients_mut() {
1805 if let Err(e) = client.reset() {
1806 log::error!("{e}");
1807 }
1808 }
1809
1810 self.cache.borrow_mut().reset();
1811 self.pos_id_generator.reset();
1812
1813 self.stop_snapshot_timer();
1814 self.stop_purge_timers();
1815
1816 self.command_count.set(0);
1817 self.event_count = 0;
1818 self.report_count = 0;
1819 self.filtered_unclaimed_external_order_count = 0;
1820
1821 log::info!("Reset");
1822 }
1823
1824 pub fn dispose(&mut self) {
1829 for client in self.get_clients_mut() {
1830 if let Err(e) = client.dispose() {
1831 log::error!("{e}");
1832 }
1833 }
1834
1835 self.stop_snapshot_timer();
1836 self.stop_purge_timers();
1837
1838 log::info!("Disposed");
1839 }
1840
1841 fn execute_command(&self, command: TradingCommand) {
1842 self.command_count.set(self.command_count.get() + 1);
1843
1844 if self.config.debug {
1845 log::debug!("{RECV}{CMD} {command:?}");
1846 }
1847
1848 if let Some(cid) = command.client_id()
1849 && self.external_clients.contains(&cid)
1850 {
1851 let topic = format!("commands.trading.{cid}");
1852 msgbus::publish_any(topic.into(), &command);
1853
1854 if self.config.debug {
1855 log::debug!("Skipping execution command for external client {cid}: {command:?}");
1856 }
1857 return;
1858 }
1859
1860 let client = if let Some(adapter) = self.find_client_for_command(&command) {
1861 adapter.client.as_ref()
1862 } else {
1863 let routing_context = Self::routing_context_for_command(&command);
1864
1865 log::error!(
1866 "No execution client found for command: client_id={:?}, {routing_context}, command={command:?}",
1867 command.client_id(),
1868 );
1869
1870 let reason = OrderDeniedReason::NoExecutionClient {
1871 client_id: command.client_id(),
1872 routing_context,
1873 }
1874 .to_string();
1875
1876 match command {
1877 TradingCommand::SubmitOrder(cmd) => {
1878 let order = self
1879 .cache
1880 .borrow()
1881 .order(&cmd.client_order_id)
1882 .map(|o| o.clone());
1883
1884 if let Some(order) = order {
1885 self.deny_order(&order, &reason);
1886 }
1887 }
1888 TradingCommand::SubmitOrderList(cmd) => {
1889 let orders: Vec<OrderAny> = self
1890 .cache
1891 .borrow()
1892 .orders_for_ids(&cmd.order_list.client_order_ids, &cmd);
1893
1894 for order in &orders {
1895 self.deny_order(order, &reason);
1896 }
1897 }
1898 _ => {}
1899 }
1900
1901 return;
1902 };
1903
1904 match command {
1905 TradingCommand::SubmitOrder(cmd) => self.handle_submit_order(client, cmd),
1906 TradingCommand::SubmitOrderList(cmd) => self.handle_submit_order_list(client, cmd),
1907 TradingCommand::ModifyOrder(cmd) => self.handle_modify_order(client, cmd),
1908 TradingCommand::ModifyOrders(cmd) => self.handle_batch_modify_orders(client, cmd),
1909 TradingCommand::CancelOrder(cmd) => self.handle_cancel_order(client, cmd),
1910 TradingCommand::CancelOrders(cmd) => self.handle_batch_cancel_orders(client, cmd),
1911 TradingCommand::CancelAllOrders(cmd) => self.handle_cancel_all_orders(client, cmd),
1912 TradingCommand::QueryOrder(cmd) => self.handle_query_order(client, cmd),
1913 TradingCommand::QueryAccount(cmd) => self.handle_query_account(client, cmd),
1914 }
1915 }
1916
1917 fn routing_context_for_command(command: &TradingCommand) -> String {
1918 match command {
1919 TradingCommand::SubmitOrder(cmd) => format!("venue={}", cmd.instrument_id.venue),
1920 TradingCommand::SubmitOrderList(cmd) => format!("venue={}", cmd.instrument_id.venue),
1921 TradingCommand::ModifyOrder(cmd) => format!("venue={}", cmd.instrument_id.venue),
1922 TradingCommand::ModifyOrders(cmd) => format!("venue={}", cmd.instrument_id.venue),
1923 TradingCommand::CancelOrder(cmd) => format!("venue={}", cmd.instrument_id.venue),
1924 TradingCommand::CancelOrders(cmd) => format!("venue={}", cmd.instrument_id.venue),
1925 TradingCommand::CancelAllOrders(cmd) => format!("venue={}", cmd.instrument_id.venue),
1926 TradingCommand::QueryOrder(cmd) => format!("venue={}", cmd.instrument_id.venue),
1927 TradingCommand::QueryAccount(cmd) => {
1928 let issuer = cmd.account_id.get_issuer();
1929 format!("account_id={}, issuer={issuer}", cmd.account_id)
1930 }
1931 }
1932 }
1933
1934 fn find_client_for_command(&self, command: &TradingCommand) -> Option<&ExecutionClientAdapter> {
1935 if let Some(client_id) = command.client_id()
1936 && let Some(adapter) = self.clients.get(&client_id)
1937 {
1938 return Some(adapter);
1939 }
1940
1941 if let Some(account_id) = self.account_id_for_command(command) {
1942 let issuer = account_id.get_issuer();
1943 let issuer_client_id = ClientId::from(issuer.as_str());
1944
1945 if let Some(adapter) = self.clients.get(&issuer_client_id) {
1946 return Some(adapter);
1947 }
1948
1949 if let Some(client_id) = self.routing_map.get(&issuer)
1950 && let Some(adapter) = self.clients.get(client_id)
1951 {
1952 return Some(adapter);
1953 }
1954 }
1955
1956 if let Some(instrument_id) = Self::instrument_id_for_command(command)
1957 && let Some(client_id) = self.routing_map.get(&instrument_id.venue)
1958 && let Some(adapter) = self.clients.get(client_id)
1959 {
1960 return Some(adapter);
1961 }
1962
1963 self.default_client_id.and_then(|id| self.clients.get(&id))
1964 }
1965
1966 fn account_id_for_command(&self, command: &TradingCommand) -> Option<AccountId> {
1967 match command {
1968 TradingCommand::QueryAccount(cmd) => Some(cmd.account_id),
1969 TradingCommand::SubmitOrder(cmd) => self
1970 .cache
1971 .borrow()
1972 .order(&cmd.client_order_id)
1973 .and_then(|order| order.account_id()),
1974 TradingCommand::ModifyOrder(cmd) => self
1975 .cache
1976 .borrow()
1977 .order(&cmd.client_order_id)
1978 .and_then(|order| order.account_id()),
1979 TradingCommand::CancelOrder(cmd) => self
1980 .cache
1981 .borrow()
1982 .order(&cmd.client_order_id)
1983 .and_then(|order| order.account_id()),
1984 TradingCommand::SubmitOrderList(_)
1985 | TradingCommand::ModifyOrders(_)
1986 | TradingCommand::CancelOrders(_)
1987 | TradingCommand::CancelAllOrders(_)
1988 | TradingCommand::QueryOrder(_) => None,
1989 }
1990 }
1991
1992 const fn instrument_id_for_command(command: &TradingCommand) -> Option<InstrumentId> {
1993 match command {
1994 TradingCommand::SubmitOrder(cmd) => Some(cmd.instrument_id),
1995 TradingCommand::SubmitOrderList(cmd) => Some(cmd.instrument_id),
1996 TradingCommand::ModifyOrder(cmd) => Some(cmd.instrument_id),
1997 TradingCommand::ModifyOrders(cmd) => Some(cmd.instrument_id),
1998 TradingCommand::CancelOrder(cmd) => Some(cmd.instrument_id),
1999 TradingCommand::CancelOrders(cmd) => Some(cmd.instrument_id),
2000 TradingCommand::CancelAllOrders(cmd) => Some(cmd.instrument_id),
2001 TradingCommand::QueryOrder(cmd) => Some(cmd.instrument_id),
2002 TradingCommand::QueryAccount(_) => None,
2003 }
2004 }
2005
2006 fn handle_submit_order(&self, client: &dyn ExecutionClient, cmd: SubmitOrder) {
2007 let client_order_id = cmd.client_order_id;
2008 let cached_order = { self.cache.borrow().order_owned(&client_order_id) };
2009
2010 let (order, added_to_cache) = match cached_order {
2011 Some(order) => (order, false),
2012 None => {
2013 let Some(order) =
2014 self.add_order_from_init(&cmd.order_init, cmd.position_id, cmd.client_id, &cmd)
2015 else {
2016 return;
2017 };
2018
2019 (order, true)
2020 }
2021 };
2022
2023 if added_to_cache && self.config.snapshot_orders {
2024 self.create_order_state_snapshot(&order);
2025 }
2026
2027 let order_venue = order.instrument_id().venue;
2028 let client_venue = client.venue();
2029 if !client.handles_order_venue(order_venue) {
2030 let client_id = client.client_id();
2031 let reason = OrderDeniedReason::ClientVenueMismatch {
2032 client_id,
2033 order_venue,
2034 client_venue,
2035 }
2036 .to_string();
2037 self.deny_order(&order, &reason);
2038 return;
2039 }
2040
2041 if let Some(reason) = self.check_position_id_against_oms(
2042 cmd.instrument_id,
2043 cmd.strategy_id,
2044 cmd.position_id,
2045 client,
2046 ) {
2047 self.deny_order(&order, &reason.to_string());
2048 return;
2049 }
2050
2051 let instrument_id = order.instrument_id();
2052
2053 if !added_to_cache && self.config.snapshot_orders {
2054 self.create_order_state_snapshot(&order);
2055 }
2056
2057 {
2058 let cache = self.cache.borrow();
2059 if cache.instrument(&instrument_id).is_none() {
2060 log::error!(
2061 "Cannot handle submit order: no instrument found for {instrument_id}, {cmd}",
2062 );
2063 return;
2064 }
2065 }
2066
2067 if self.config.manage_own_order_books && should_handle_own_book_order(&order) {
2068 let mut own_book = self.get_or_init_own_order_book(&order.instrument_id());
2069 own_book.add(order.to_own_book_order());
2070 }
2071
2072 log_info!("Submit {order}", color = LogColor::Blue);
2073
2074 if let Err(e) = client.submit_order(cmd) {
2075 self.deny_order(
2076 &order,
2077 &OrderDeniedReason::SubmitFailed {
2078 detail: e.to_string(),
2079 }
2080 .to_string(),
2081 );
2082 }
2083 }
2084
2085 fn handle_submit_order_list(&self, client: &dyn ExecutionClient, cmd: SubmitOrderList) {
2086 let mut orders = Vec::with_capacity(cmd.order_list.client_order_ids.len());
2087 let mut added_client_order_ids = AHashSet::new();
2088
2089 for client_order_id in &cmd.order_list.client_order_ids {
2090 let cached_order = { self.cache.borrow().order_owned(client_order_id) };
2091
2092 if let Some(order) = cached_order {
2093 orders.push(order);
2094 continue;
2095 }
2096
2097 let Some(order_init) = cmd
2098 .order_inits
2099 .iter()
2100 .find(|init| init.client_order_id == *client_order_id)
2101 else {
2102 log::error!(
2103 "Cannot handle submit order list: order not found in cache and no initialization event for {client_order_id}, {cmd}"
2104 );
2105 continue;
2106 };
2107
2108 let Some(order) =
2109 self.add_order_from_init(order_init, cmd.position_id, cmd.client_id, &cmd)
2110 else {
2111 continue;
2112 };
2113
2114 added_client_order_ids.insert(order.client_order_id());
2115 orders.push(order);
2116 }
2117
2118 if self.config.snapshot_orders {
2119 for order in &orders {
2120 if added_client_order_ids.contains(&order.client_order_id()) {
2121 self.create_order_state_snapshot(order);
2122 }
2123 }
2124 }
2125
2126 if orders.len() != cmd.order_list.client_order_ids.len() {
2127 let reason = OrderDeniedReason::OrderListIncomplete {
2128 order_list_id: cmd.order_list.id,
2129 }
2130 .to_string();
2131
2132 for order in &orders {
2133 self.deny_order(order, &reason);
2134 }
2135 return;
2136 }
2137
2138 let order_list_venue = cmd.instrument_id.venue;
2139 let client_venue = client.venue();
2140 if !client.handles_order_venue(order_list_venue) {
2141 let client_id = client.client_id();
2142 let reason = OrderDeniedReason::ClientVenueMismatch {
2143 client_id,
2144 order_venue: order_list_venue,
2145 client_venue,
2146 }
2147 .to_string();
2148
2149 for order in &orders {
2150 self.deny_order(order, &reason);
2151 }
2152 return;
2153 }
2154
2155 let is_uniform_instrument = orders
2156 .iter()
2157 .all(|o| o.instrument_id() == cmd.instrument_id);
2158
2159 if let Some(position_id) = cmd.position_id
2160 && !is_uniform_instrument
2161 {
2162 let reason = OrderDeniedReason::InvalidPositionId {
2163 position_id,
2164 detail: "not valid for a mixed-instrument order list; a position belongs to a single instrument"
2165 .to_string(),
2166 }
2167 .to_string();
2168
2169 for order in &orders {
2170 self.deny_order(order, &reason);
2171 }
2172 return;
2173 }
2174
2175 if let Some(reason) = self.check_position_id_against_oms(
2176 cmd.instrument_id,
2177 cmd.strategy_id,
2178 cmd.position_id,
2179 client,
2180 ) {
2181 let reason = reason.to_string();
2182 for order in &orders {
2183 self.deny_order(order, &reason);
2184 }
2185 return;
2186 }
2187
2188 if self.config.snapshot_orders {
2189 for order in &orders {
2190 if !added_client_order_ids.contains(&order.client_order_id()) {
2191 self.create_order_state_snapshot(order);
2192 }
2193 }
2194 }
2195
2196 {
2197 let cache = self.cache.borrow();
2198 if cache.instrument(&cmd.instrument_id).is_none() {
2199 log::error!(
2200 "Cannot handle submit order list: no instrument found for {}, {cmd}",
2201 cmd.instrument_id,
2202 );
2203 return;
2204 }
2205 }
2206
2207 if self.config.manage_own_order_books {
2208 for order in &orders {
2209 if should_handle_own_book_order(order) {
2210 let mut own_book = self.get_or_init_own_order_book(&order.instrument_id());
2211 own_book.add(order.to_own_book_order());
2212 }
2213 }
2214 }
2215
2216 log_info!("Submit {}", cmd.order_list, color = LogColor::Blue);
2217
2218 if let Err(e) = client.submit_order_list(cmd) {
2219 log::error!("Error submitting order list to client: {e}");
2220 let reason = OrderDeniedReason::SubmitFailed {
2221 detail: e.to_string(),
2222 }
2223 .to_string();
2224
2225 for order in &orders {
2226 self.deny_order(order, &reason);
2227 }
2228 }
2229 }
2230
2231 fn add_order_from_init(
2232 &self,
2233 order_init: &OrderInitialized,
2234 position_id: Option<PositionId>,
2235 client_id: Option<ClientId>,
2236 context: &dyn Display,
2237 ) -> Option<OrderAny> {
2238 let client_order_id = order_init.client_order_id;
2239 let order = match OrderAny::from_events(vec![OrderEventAny::Initialized(
2240 order_init.clone(),
2241 )]) {
2242 Ok(order) => order,
2243 Err(e) => {
2244 log::error!(
2245 "Cannot reconstruct order from initialization event for {client_order_id}: {e}, {context}"
2246 );
2247 return None;
2248 }
2249 };
2250
2251 if let Err(e) =
2252 self.cache
2253 .borrow_mut()
2254 .add_order(order.clone(), position_id, client_id, true)
2255 {
2256 log::error!(
2257 "Cannot add reconstructed order to cache for {client_order_id}: {e}, {context}"
2258 );
2259 return None;
2260 }
2261
2262 Some(order)
2263 }
2264
2265 fn handle_modify_order(&self, client: &dyn ExecutionClient, cmd: ModifyOrder) {
2266 let venue_str = cmd
2267 .venue_order_id
2268 .map_or_else(String::new, |venue_order_id| format!(" {venue_order_id}"));
2269
2270 log_info!(
2271 "Modify {}{venue_str}",
2272 cmd.client_order_id,
2273 color = LogColor::Blue
2274 );
2275
2276 if let Err(e) = client.modify_order(cmd) {
2277 log::error!("Error modifying order: {e}");
2278 }
2279 }
2280
2281 fn handle_batch_modify_orders(&self, client: &dyn ExecutionClient, cmd: BatchModifyOrders) {
2282 if let Err(e) = client.batch_modify_orders(cmd) {
2283 log::error!("Error batch modifying orders: {e}");
2284 }
2285 }
2286
2287 fn handle_cancel_order(&self, client: &dyn ExecutionClient, cmd: CancelOrder) {
2288 let venue_str = cmd
2289 .venue_order_id
2290 .map_or_else(String::new, |venue_order_id| format!(" {venue_order_id}"));
2291
2292 log_info!(
2293 "Cancel {}{venue_str}",
2294 cmd.client_order_id,
2295 color = LogColor::Blue
2296 );
2297
2298 if let Err(e) = client.cancel_order(cmd) {
2299 log::error!("Error canceling order: {e}");
2300 }
2301 }
2302
2303 fn handle_cancel_all_orders(&self, client: &dyn ExecutionClient, cmd: CancelAllOrders) {
2304 let side_str = match cmd.order_side {
2305 OrderSide::NoOrderSide => " ".to_string(),
2306 order_side => format!(" {order_side} "),
2307 };
2308
2309 log_info!("Cancel all{side_str}orders", color = LogColor::Blue);
2310
2311 if let Err(e) = client.cancel_all_orders(cmd) {
2312 log::error!("Error canceling all orders: {e}");
2313 }
2314 }
2315
2316 fn handle_batch_cancel_orders(&self, client: &dyn ExecutionClient, cmd: BatchCancelOrders) {
2317 let client_order_ids: Vec<ClientOrderId> = cmd
2318 .cancels
2319 .iter()
2320 .map(|cancel| cancel.client_order_id)
2321 .collect();
2322
2323 log_info!(
2324 "Batch cancel orders {client_order_ids:?}",
2325 color = LogColor::Blue
2326 );
2327
2328 if let Err(e) = client.batch_cancel_orders(cmd) {
2329 log::error!("Error batch canceling orders: {e}");
2330 }
2331 }
2332
2333 fn handle_query_account(&self, client: &dyn ExecutionClient, cmd: QueryAccount) {
2334 log_info!("Query {}", cmd.account_id, color = LogColor::Blue);
2335
2336 if let Err(e) = client.query_account(cmd) {
2337 log::warn!("Error querying account: {e}");
2338 }
2339 }
2340
2341 fn handle_query_order(&self, client: &dyn ExecutionClient, cmd: QueryOrder) {
2342 log_info!("Query {}", cmd.client_order_id, color = LogColor::Blue);
2343
2344 if let Err(e) = client.query_order(cmd) {
2345 log::warn!("Error querying order: {e}");
2346 }
2347 }
2348
2349 fn create_order_state_snapshot(&self, order: &OrderAny) {
2350 if self.config.debug {
2351 log::debug!("Creating order state snapshot for {order}");
2352 }
2353
2354 if self.cache.borrow().has_backing()
2355 && let Err(e) = self.cache.borrow().snapshot_order_state(order)
2356 {
2357 log::warn!("Failed to snapshot order state: {e}");
2358 }
2359 }
2360
2361 fn create_position_state_snapshot(&self, position: &Position, open_only: bool) {
2362 Self::publish_position_state_snapshot(
2363 &self.clock,
2364 &self.cache,
2365 self.config.debug,
2366 position,
2367 open_only,
2368 );
2369 }
2370
2371 fn publish_position_state_snapshot(
2372 clock: &Rc<RefCell<dyn Clock>>,
2373 cache: &Rc<RefCell<Cache>>,
2374 debug: bool,
2375 position: &Position,
2376 open_only: bool,
2377 ) {
2378 if debug {
2379 log::debug!("Creating position state snapshot for {position}");
2380 }
2381
2382 let ts_snapshot = clock.borrow().timestamp_ns();
2383 let unrealized_pnl = cache.borrow().calculate_unrealized_pnl(position);
2384
2385 let snapshot = PositionStateSnapshot {
2386 position: position.clone(),
2387 unrealized_pnl,
2388 ts_snapshot,
2389 };
2390
2391 let topic = switchboard::get_snapshot_position_topic(position.id);
2392 msgbus::publish_any(topic, &snapshot);
2393
2394 let has_backing = cache.borrow().has_backing();
2395 if has_backing
2396 && let Err(e) = cache.borrow_mut().snapshot_position_state(
2397 position,
2398 ts_snapshot,
2399 unrealized_pnl,
2400 Some(open_only),
2401 )
2402 {
2403 log::warn!("Failed to snapshot position state: {e}");
2404 }
2405 }
2406
2407 fn handle_event(&mut self, event: &OrderEventAny) {
2408 self.handle_event_with_position_application(event, true);
2409 }
2410
2411 fn handle_event_with_position_application(
2412 &mut self,
2413 event: &OrderEventAny,
2414 apply_position: bool,
2415 ) {
2416 self.event_count += 1;
2417
2418 if self.config.debug {
2419 log::debug!("{RECV}{EVT} {event:?}");
2420 }
2421
2422 let event_client_order_id = event.client_order_id();
2423 let cache = self.cache.borrow();
2424 let client_order_id = if cache.order_exists(&event_client_order_id) {
2425 event_client_order_id
2426 } else {
2427 let is_leg_fill =
2428 matches!(event, OrderEventAny::Filled(fill) if self.is_leg_fill(fill));
2429 if !is_leg_fill {
2430 log::warn!(
2431 "Order with {} not found in the cache to apply {}",
2432 event.client_order_id(),
2433 event
2434 );
2435 }
2436
2437 let venue_order_id = if let Some(id) = event.venue_order_id() {
2439 id
2440 } else {
2441 log::error!(
2442 "Cannot apply event to any order: {} not found in the cache with no VenueOrderId",
2443 event.client_order_id()
2444 );
2445 return;
2446 };
2447
2448 let client_order_id = if let Some(id) = cache.client_order_id(&venue_order_id) {
2450 *id
2451 } else {
2452 if let OrderEventAny::Filled(fill) = event
2453 && is_leg_fill
2454 {
2455 log::info!(
2456 "Processing leg fill without corresponding order: {} for instrument {}",
2457 fill.client_order_id,
2458 fill.instrument_id
2459 );
2460 drop(cache);
2461 self.handle_leg_fill_without_order(fill.clone());
2462 return;
2463 }
2464
2465 log::error!(
2466 "Cannot apply event to any order: {} and {venue_order_id} not found in the cache",
2467 event.client_order_id(),
2468 );
2469 return;
2470 };
2471
2472 if cache.order_exists(&client_order_id) {
2474 log::info!("Order with {client_order_id} was found in the cache");
2475 client_order_id
2476 } else {
2477 if let OrderEventAny::Filled(fill) = event
2478 && is_leg_fill
2479 {
2480 log::info!(
2481 "Processing leg fill without corresponding order: {} for instrument {}",
2482 fill.client_order_id,
2483 fill.instrument_id
2484 );
2485 drop(cache);
2486 self.handle_leg_fill_without_order(fill.clone());
2487 return;
2488 }
2489
2490 log::error!(
2491 "Cannot apply event to any order: {client_order_id} and {venue_order_id} not found in cache",
2492 );
2493 return;
2494 }
2495 };
2496 let order_before_fill = if matches!(event, OrderEventAny::Filled(_)) {
2497 cache.order(&client_order_id).map(|o| o.clone())
2498 } else {
2499 None
2500 };
2501
2502 drop(cache);
2503
2504 let event = if event_client_order_id == client_order_id {
2505 event.clone()
2506 } else {
2507 event.clone().with_client_order_id(client_order_id)
2508 };
2509
2510 match &event {
2511 OrderEventAny::Filled(fill) => {
2512 let Some(order_before_fill) = order_before_fill else {
2513 log::error!(
2514 "Cannot apply fill: order {} not found in the cache",
2515 fill.client_order_id()
2516 );
2517 return;
2518 };
2519 let configured_oms_type = self.determine_oms_type(fill);
2520 let position_id =
2521 self.determine_position_id(fill, configured_oms_type, Some(&order_before_fill));
2522 let oms_type = self
2523 .cache
2524 .borrow()
2525 .oms_type(&position_id)
2526 .unwrap_or(configured_oms_type);
2527
2528 let mut fill = fill.clone();
2529 fill.position_id = Some(position_id);
2530
2531 let validation = if apply_position {
2532 self.validate_fill_for_order(&order_before_fill, &fill)
2533 } else {
2534 self.validate_fill_for_order_projection(&order_before_fill, &fill)
2535 };
2536
2537 if validation.is_ok() {
2538 let event = OrderEventAny::Filled(fill.clone());
2539 let Some(order) =
2540 self.update_cached_order(client_order_id, &event, apply_position)
2541 else {
2542 return;
2543 };
2544
2545 let position_events = if apply_position {
2546 self.handle_order_fill(&order, fill, oms_type)
2547 } else {
2548 Vec::new()
2549 };
2550 self.publish_order_event(&event);
2551 self.publish_position_events(position_events);
2552 }
2553 }
2554 OrderEventAny::FillVoided(voided) => {
2555 let mut voided = voided.clone();
2556 let Some(order_before_void) = self
2557 .cache
2558 .borrow()
2559 .order(&client_order_id)
2560 .map(|order| order.clone())
2561 else {
2562 log::error!("Cannot apply fill void: order {client_order_id} not found");
2563 return;
2564 };
2565 let original_fill = order_before_void
2566 .events()
2567 .into_iter()
2568 .find_map(|candidate| match candidate {
2569 OrderEventAny::Filled(fill) if fill.trade_id == voided.trade_id => {
2570 Some(fill.clone())
2571 }
2572 _ => None,
2573 });
2574
2575 if voided.position_id.is_none() {
2576 voided.position_id = original_fill.as_ref().and_then(|fill| fill.position_id);
2577 }
2578 let event = OrderEventAny::FillVoided(voided.clone());
2579
2580 let mut validated_order = order_before_void.clone();
2581 match validated_order.apply(event.clone()) {
2582 Ok(()) => {}
2583 Err(OrderError::DuplicateFillVoid(trade_id)) => {
2584 log::warn!(
2585 "Duplicate fill void rejected at order level: trade_id={trade_id}"
2586 );
2587 return;
2588 }
2589 Err(e) => {
2590 log::error!("Cannot apply fill void to order: {e}");
2591 return;
2592 }
2593 }
2594
2595 let corrected_positions = if apply_position
2596 && original_fill
2597 .as_ref()
2598 .is_some_and(|fill| fill.position_id.is_some())
2599 {
2600 match self.prepare_order_fill_void_positions(&order_before_void, &voided) {
2601 Ok(positions) => positions,
2602 Err(e) => {
2603 log::error!("Cannot apply fill void to positions: {e}");
2604 return;
2605 }
2606 }
2607 } else {
2608 Vec::new()
2609 };
2610
2611 let mut position_events = Vec::new();
2612
2613 for CorrectedPosition {
2614 position,
2615 corrected_qty,
2616 absorbed_prior_cycles,
2617 closed_cycles_pnl,
2618 } in corrected_positions
2619 {
2620 if let Err(e) = self.cache.borrow_mut().update_position(&position) {
2621 log::error!("Cannot apply fill void to position {}: {e}", position.id);
2622 return;
2623 }
2624
2625 if absorbed_prior_cycles {
2626 log::info!(
2627 "Settling archived NETTING cycles rebuilt by fill void {} for position {}: realized={closed_cycles_pnl:?}",
2628 voided.trade_id,
2629 position.id,
2630 );
2631
2632 self.cache
2633 .borrow_mut()
2634 .settle_position_snapshots(&position, closed_cycles_pnl);
2635 }
2636
2637 if self.config.snapshot_positions {
2638 self.create_position_state_snapshot(&position, false);
2639 }
2640
2641 position_events.push(Self::create_fill_void_position_event(
2642 &position,
2643 &voided,
2644 corrected_qty,
2645 ));
2646 }
2647
2648 if self
2649 .update_cached_order(client_order_id, &event, true)
2650 .is_none()
2651 {
2652 return;
2653 }
2654
2655 if original_fill.is_some() {
2656 let portfolio_endpoint = MessagingSwitchboard::portfolio_update_order();
2657 msgbus::send_order_event(portfolio_endpoint, event.clone());
2658 }
2659 self.publish_order_event(&event);
2660 self.publish_position_events(position_events);
2661 }
2662 _ => {
2663 if self
2664 .update_cached_order(client_order_id, &event, true)
2665 .is_some()
2666 {
2667 self.publish_order_event(&event);
2668 }
2669 }
2670 }
2671 }
2672
2673 fn handle_leg_fill_without_order(&mut self, mut fill: OrderFilled) {
2674 let instrument =
2675 if let Some(instrument) = self.cache.borrow().instrument(&fill.instrument_id) {
2676 instrument.clone()
2677 } else {
2678 log::error!(
2679 "Cannot handle leg fill: no instrument found for {}, {fill}",
2680 fill.instrument_id,
2681 );
2682 return;
2683 };
2684
2685 if let Err(e) = self.cache.borrow().try_account(&fill.account_id) {
2686 log::error!("Cannot handle leg fill: {e}, {fill}");
2687 return;
2688 }
2689
2690 let oms_type = self.determine_oms_type(&fill);
2691 let position_id = self.determine_leg_fill_position_id(&fill, oms_type);
2692 fill.position_id = Some(position_id);
2693 let duplicate_position_fill = self.position_contains_trade_id(position_id, fill.trade_id);
2694
2695 let event = OrderEventAny::Filled(fill.clone());
2696
2697 if duplicate_position_fill {
2698 log::warn!(
2699 "Duplicate leg fill: {} trade_id={} already applied to position {}, skipping",
2700 fill.client_order_id,
2701 fill.trade_id,
2702 position_id
2703 );
2704 return;
2705 }
2706
2707 let portfolio_endpoint = MessagingSwitchboard::portfolio_update_order();
2708 msgbus::send_order_event(portfolio_endpoint, event.clone());
2709 let position_events = self.handle_position_update(&instrument, fill, oms_type);
2710 self.publish_order_event(&event);
2711 self.publish_position_events(position_events);
2712 }
2713
2714 fn determine_leg_fill_position_id(
2715 &mut self,
2716 fill: &OrderFilled,
2717 oms_type: OmsType,
2718 ) -> PositionId {
2719 let cache = self.cache.borrow();
2720 let cached_position_id = cache.position_id(&fill.client_order_id()).copied();
2721 drop(cache);
2722
2723 if let Some(position_id) = cached_position_id {
2724 if let Some(fill_position_id) = fill.position_id
2725 && fill_position_id != position_id
2726 {
2727 log::warn!(
2728 "Incorrect position ID assigned to leg fill: \
2729 cached={position_id}, assigned={fill_position_id}; \
2730 re-assigning from cache",
2731 );
2732 }
2733
2734 return position_id;
2735 }
2736
2737 match oms_type {
2738 OmsType::Hedging => fill
2739 .position_id
2740 .unwrap_or_else(|| self.pos_id_generator.generate(fill.strategy_id, false)),
2741 OmsType::Netting => self.determine_netting_position_id(fill),
2742 _ => self.determine_netting_position_id(fill),
2743 }
2744 }
2745
2746 fn is_leg_fill(&self, fill: &OrderFilled) -> bool {
2747 if !fill.client_order_id.as_str().contains("-LEG-")
2748 && !fill.venue_order_id.as_str().contains("-LEG-")
2749 {
2750 return false;
2751 }
2752
2753 self.cache
2754 .borrow()
2755 .instrument(&fill.instrument_id)
2756 .is_some_and(|instrument| !instrument.is_spread())
2757 }
2758
2759 fn determine_oms_type(&self, fill: &OrderFilled) -> OmsType {
2760 if let Some(oms_type) = self.oms_overrides.get(&fill.strategy_id)
2761 && *oms_type != OmsType::Unspecified
2762 {
2763 return *oms_type;
2764 }
2765
2766 if let Some(client_id) = self.routing_map.get(&fill.instrument_id.venue)
2767 && let Some(client) = self.clients.get(client_id)
2768 {
2769 return client.oms_type;
2770 }
2771
2772 if let Some(client) = self.default_client_id.and_then(|id| self.clients.get(&id)) {
2773 return client.oms_type;
2774 }
2775
2776 OmsType::Netting }
2778
2779 fn resolve_oms_type_for_client(
2780 &self,
2781 strategy_id: StrategyId,
2782 client: &dyn ExecutionClient,
2783 ) -> OmsType {
2784 if let Some(oms_type) = self.oms_overrides.get(&strategy_id)
2785 && *oms_type != OmsType::Unspecified
2786 {
2787 return *oms_type;
2788 }
2789
2790 client.oms_type()
2791 }
2792
2793 fn check_position_id_against_oms(
2794 &self,
2795 instrument_id: InstrumentId,
2796 strategy_id: StrategyId,
2797 position_id: Option<PositionId>,
2798 client: &dyn ExecutionClient,
2799 ) -> Option<OrderDeniedReason> {
2800 let position_id = position_id?;
2801
2802 if self.resolve_oms_type_for_client(strategy_id, client) != OmsType::Netting {
2803 return None;
2804 }
2805
2806 let expected = format!("{instrument_id}-{strategy_id}");
2807 if position_id.as_str() == expected {
2808 return None;
2809 }
2810
2811 Some(OrderDeniedReason::InvalidPositionId {
2812 position_id,
2813 detail: format!(
2814 "not valid for NETTING OMS; expected '{expected}' (use HEDGING for custom position IDs)"
2815 ),
2816 })
2817 }
2818
2819 fn determine_position_id(
2820 &mut self,
2821 fill: &OrderFilled,
2822 oms_type: OmsType,
2823 order: Option<&OrderAny>,
2824 ) -> PositionId {
2825 let cache = self.cache.borrow();
2826 let cached_position_id = cache.position_id(&fill.client_order_id()).copied();
2827 drop(cache);
2828
2829 if self.config.debug {
2830 log::debug!(
2831 "Determining position ID for {}, position_id={:?}",
2832 fill.client_order_id(),
2833 cached_position_id,
2834 );
2835 }
2836
2837 if let Some(position_id) = cached_position_id {
2838 if let Some(fill_position_id) = fill.position_id
2839 && fill_position_id != position_id
2840 {
2841 log::warn!(
2842 "Incorrect position ID assigned to fill: \
2843 cached={position_id}, assigned={fill_position_id}; \
2844 re-assigning from cache",
2845 );
2846 }
2847
2848 if self.config.debug {
2849 log::debug!("Assigned {position_id} to {}", fill.client_order_id());
2850 }
2851
2852 return position_id;
2853 }
2854
2855 let position_id = match oms_type {
2856 OmsType::Hedging => self.determine_hedging_position_id(fill, order),
2857 OmsType::Netting => self.determine_netting_position_id(fill),
2858 _ => self.determine_netting_position_id(fill),
2859 };
2860
2861 let order = if let Some(o) = order {
2862 o.clone()
2863 } else {
2864 let cache = self.cache.borrow();
2865 cache.order(&fill.client_order_id()).map_or_else(
2866 || {
2867 panic!(
2868 "Order for {} not found to determine position ID",
2869 fill.client_order_id()
2870 )
2871 },
2872 |o| o.clone(),
2873 )
2874 };
2875
2876 if order.exec_algorithm_id().is_some()
2877 && let Some(exec_spawn_id) = order.exec_spawn_id()
2878 {
2879 let cache = self.cache.borrow();
2880 let primary = if let Some(p) = cache.order(&exec_spawn_id) {
2881 p.clone()
2882 } else {
2883 log::warn!(
2884 "Primary exec spawn order {exec_spawn_id} not found, \
2885 skipping position ID propagation"
2886 );
2887 return position_id;
2888 };
2889 let primary_already_indexed = cache.position_id(&primary.client_order_id()).is_some();
2890 drop(cache);
2891
2892 if primary.position_id().is_none() && !primary_already_indexed {
2893 if let Some(mut primary_mut) = self.cache.borrow_mut().order_mut(&exec_spawn_id) {
2894 primary_mut.set_position_id(Some(position_id));
2895 }
2896 let _ = self.cache.borrow_mut().add_position_id(
2897 &position_id,
2898 &primary.instrument_id().venue,
2899 &primary.client_order_id(),
2900 &primary.strategy_id(),
2901 );
2902 log::debug!("Assigned primary order {position_id}");
2903 }
2904 }
2905
2906 position_id
2907 }
2908
2909 fn determine_hedging_position_id(
2910 &mut self,
2911 fill: &OrderFilled,
2912 order: Option<&OrderAny>,
2913 ) -> PositionId {
2914 if let Some(position_id) = fill.position_id {
2916 if self.config.debug {
2917 log::debug!("Already had a position ID of: {position_id}");
2918 }
2919 return position_id;
2920 }
2921
2922 let cache = self.cache.borrow();
2923
2924 let cached_order;
2925 let order: &OrderAny = if let Some(order) = order {
2926 order
2927 } else {
2928 cached_order = cache.order(&fill.client_order_id()).unwrap_or_else(|| {
2929 panic!(
2930 "Order for {} not found to determine position ID",
2931 fill.client_order_id()
2932 )
2933 });
2934 &cached_order
2935 };
2936
2937 if let Some(spawn_id) = order.exec_spawn_id() {
2939 let spawn_orders = cache.orders_for_exec_spawn(&spawn_id);
2940 for spawned_order in spawn_orders {
2941 if let Some(pos_id) = spawned_order.position_id() {
2942 if self.config.debug {
2943 log::debug!("Found spawned {} for {}", pos_id, fill.client_order_id());
2944 }
2945 return pos_id;
2946 }
2947 }
2948 }
2949
2950 if order.is_reduce_only() {
2951 let mut candidates = cache
2952 .positions_open(
2953 None,
2954 Some(&fill.instrument_id),
2955 Some(&fill.strategy_id),
2956 Some(&fill.account_id),
2957 None,
2958 )
2959 .into_iter()
2960 .filter(|position| position.is_opposite_side(fill.order_side));
2961 let candidate = candidates.next();
2962
2963 if let Some(position) = candidate
2964 && candidates.next().is_none()
2965 && order.would_reduce_only(position.side, position.quantity)
2966 {
2967 if self.config.debug {
2968 log::debug!(
2969 "Assigned reduce-only fill {} to position {}",
2970 fill.client_order_id(),
2971 position.id
2972 );
2973 }
2974 return position.id;
2975 }
2976 }
2977
2978 let position_id = self.pos_id_generator.generate(fill.strategy_id, false);
2980
2981 if self.config.debug {
2982 log::debug!("Generated {} for {}", position_id, fill.client_order_id());
2983 }
2984 position_id
2985 }
2986
2987 fn determine_netting_position_id(&self, fill: &OrderFilled) -> PositionId {
2988 PositionId::new(format!("{}-{}", fill.instrument_id, fill.strategy_id))
2989 }
2990
2991 fn validate_fill_for_order(&self, order: &OrderAny, fill: &OrderFilled) -> anyhow::Result<()> {
2992 if order.is_duplicate_fill(fill) {
2993 log::warn!(
2994 "Duplicate fill: {} trade_id={} already applied, skipping",
2995 order.client_order_id(),
2996 fill.trade_id
2997 );
2998 anyhow::bail!("Duplicate fill");
2999 }
3000
3001 if let Some(position_id) = fill.position_id
3002 && self.position_contains_trade_id(position_id, fill.trade_id)
3003 {
3004 log::warn!(
3005 "Duplicate fill: {} trade_id={} already applied to position {}, skipping",
3006 order.client_order_id(),
3007 fill.trade_id,
3008 position_id
3009 );
3010 anyhow::bail!("Duplicate position fill");
3011 }
3012
3013 self.check_overfill(order, fill)
3014 }
3015
3016 fn validate_fill_for_order_projection(
3017 &self,
3018 order: &OrderAny,
3019 fill: &OrderFilled,
3020 ) -> anyhow::Result<()> {
3021 if order.is_duplicate_fill(fill) {
3022 anyhow::bail!("Duplicate fill");
3023 }
3024
3025 self.check_overfill(order, fill)
3026 }
3027
3028 fn position_contains_trade_id(&self, position_id: PositionId, trade_id: TradeId) -> bool {
3029 self.cache
3030 .borrow()
3031 .position(&position_id)
3032 .is_some_and(|position| position.trade_ids.contains(&trade_id))
3033 }
3034
3035 fn update_cached_order(
3036 &self,
3037 client_order_id: ClientOrderId,
3038 event: &OrderEventAny,
3039 send_portfolio_update: bool,
3040 ) -> Option<OrderAny> {
3041 let result = { self.cache.borrow_mut().update_order(event) };
3042
3043 let order = match result {
3044 Ok(order) => order,
3045 Err(e) => {
3046 if matches!(
3047 e.downcast_ref::<OrderError>(),
3048 Some(OrderError::InvalidStateTransition)
3049 ) {
3050 let already_closed = self
3055 .cache
3056 .borrow()
3057 .order(&client_order_id)
3058 .is_some_and(|o| o.is_closed());
3059
3060 if already_closed && !matches!(event, OrderEventAny::Filled(_)) {
3061 log::debug!("InvalidStateTrigger: {e}, did not apply {event}");
3062 } else {
3063 log::warn!("InvalidStateTrigger: {e}, did not apply {event}");
3064 }
3065 return None;
3066 }
3067
3068 if let Some(OrderError::DuplicateFill(trade_id)) = e.downcast_ref::<OrderError>() {
3069 log::warn!(
3070 "Duplicate fill rejected at order level: trade_id={trade_id}, did not apply {event}"
3071 );
3072 return None;
3073 }
3074
3075 if let Some(OrderError::DuplicateFillVoid(trade_id)) =
3076 e.downcast_ref::<OrderError>()
3077 {
3078 log::warn!(
3079 "Duplicate fill void rejected at order level: trade_id={trade_id}, did not apply {event}"
3080 );
3081 return None;
3082 }
3083
3084 log::error!("Error applying event: {e}, did not apply {event}");
3085
3086 if matches!(
3087 event,
3088 OrderEventAny::Denied(_)
3089 | OrderEventAny::Rejected(_)
3090 | OrderEventAny::Canceled(_)
3091 | OrderEventAny::Expired(_)
3092 ) {
3093 log::warn!(
3094 "Terminal event {event} failed to apply to {client_order_id}, forcing cleanup from own book"
3095 );
3096 self.cache
3097 .borrow_mut()
3098 .force_remove_from_own_order_book(&client_order_id);
3099 } else {
3100 let order = self
3101 .cache
3102 .borrow()
3103 .order(&client_order_id)
3104 .map(|o| o.clone());
3105
3106 if let Some(order) = order {
3107 let should_update_own_book = {
3108 let cache = self.cache.borrow();
3109 let own_book = cache.own_order_book(&order.instrument_id());
3110 (own_book.is_some() && order.is_closed())
3111 || should_handle_own_book_order(&order)
3112 };
3113
3114 if should_update_own_book {
3115 self.cache.borrow_mut().update_own_order_book(&order);
3116 }
3117 }
3118 }
3119 return None;
3120 }
3121 };
3122
3123 if self.config.manage_own_order_books && should_handle_own_book_order(&order) {
3124 let needs_own_book = {
3125 self.cache
3126 .borrow()
3127 .own_order_book(&order.instrument_id())
3128 .is_none()
3129 };
3130
3131 if needs_own_book {
3132 self.cache.borrow_mut().update_own_order_book(&order);
3133 }
3134 }
3135
3136 if self.config.debug {
3137 log::debug!("{SEND}{EVT} {event}");
3138 }
3139
3140 if self.config.snapshot_orders {
3141 self.create_order_state_snapshot(&order);
3142 }
3143
3144 if send_portfolio_update {
3145 self.send_order_update_to_portfolio(event);
3146 }
3147
3148 Some(order)
3149 }
3150
3151 fn send_order_update_to_portfolio(&self, event: &OrderEventAny) {
3152 let send_to_portfolio = match event {
3153 OrderEventAny::Filled(fill) => self
3154 .cache
3155 .borrow()
3156 .account(&fill.account_id)
3157 .is_none_or(|account| !account.is_margin_account()),
3158 OrderEventAny::Accepted(_)
3159 | OrderEventAny::Canceled(_)
3160 | OrderEventAny::Expired(_)
3161 | OrderEventAny::Rejected(_)
3162 | OrderEventAny::Updated(_) => true,
3163 _ => false,
3164 };
3165
3166 if send_to_portfolio {
3167 let portfolio_endpoint = MessagingSwitchboard::portfolio_update_order();
3168 msgbus::send_order_event(portfolio_endpoint, event.clone());
3169 }
3170 }
3171
3172 fn publish_order_event(&self, event: &OrderEventAny) {
3173 let topic = switchboard::get_event_order_topic(event.strategy_id());
3174 msgbus::publish_order_event(topic, event);
3175
3176 let topic = match event {
3177 OrderEventAny::Submitted(_) => {
3178 switchboard::get_order_submitted_topic(event.instrument_id())
3179 }
3180 OrderEventAny::Rejected(_) => {
3181 switchboard::get_order_rejected_topic(event.instrument_id())
3182 }
3183 OrderEventAny::PendingUpdate(_) => {
3184 switchboard::get_order_pending_update_topic(event.instrument_id())
3185 }
3186 OrderEventAny::PendingCancel(_) => {
3187 switchboard::get_order_pending_cancel_topic(event.instrument_id())
3188 }
3189 OrderEventAny::ModifyRejected(_) => {
3190 switchboard::get_order_modify_rejected_topic(event.instrument_id())
3191 }
3192 OrderEventAny::CancelRejected(_) => {
3193 switchboard::get_order_cancel_rejected_topic(event.instrument_id())
3194 }
3195 OrderEventAny::Canceled(_) => {
3196 switchboard::get_order_canceled_topic(event.instrument_id())
3197 }
3198 _ => return,
3201 };
3202
3203 msgbus::publish_order_event(topic, event);
3204 }
3205
3206 fn publish_position_events(&self, events: Vec<PositionEvent>) {
3207 for event in events {
3208 let strategy_id = match &event {
3209 PositionEvent::PositionOpened(event) => event.strategy_id,
3210 PositionEvent::PositionChanged(event) => event.strategy_id,
3211 PositionEvent::PositionClosed(event) => event.strategy_id,
3212 PositionEvent::PositionAdjusted(event) => event.strategy_id,
3213 };
3214 let topic = switchboard::get_event_position_topic(strategy_id);
3215 msgbus::publish_position_event(topic, &event);
3216 }
3217 }
3218
3219 fn check_overfill(&self, order: &OrderAny, fill: &OrderFilled) -> anyhow::Result<()> {
3220 let potential_overfill = order.calculate_overfill(fill.last_qty);
3221
3222 if potential_overfill.is_positive() {
3223 if self.config.allow_overfills {
3224 log::warn!(
3225 "Order overfill detected: {} potential_overfill={}, current_filled={}, last_qty={}, quantity={}",
3226 order.client_order_id(),
3227 potential_overfill,
3228 order.filled_qty(),
3229 fill.last_qty,
3230 order.quantity()
3231 );
3232 } else {
3233 let msg = format!(
3234 "Order overfill rejected: {} potential_overfill={}, current_filled={}, last_qty={}, quantity={}. \
3235 Set `allow_overfills=true` in ExecutionEngineConfig to allow overfills.",
3236 order.client_order_id(),
3237 potential_overfill,
3238 order.filled_qty(),
3239 fill.last_qty,
3240 order.quantity()
3241 );
3242 anyhow::bail!("{msg}");
3243 }
3244 }
3245
3246 Ok(())
3247 }
3248
3249 fn handle_order_fill(
3250 &mut self,
3251 order: &OrderAny,
3252 fill: OrderFilled,
3253 oms_type: OmsType,
3254 ) -> Vec<PositionEvent> {
3255 let instrument =
3256 if let Some(instrument) = self.cache.borrow().instrument(&fill.instrument_id) {
3257 instrument.clone()
3258 } else {
3259 log::error!(
3260 "Cannot handle order fill: no instrument found for {}, {fill}",
3261 fill.instrument_id,
3262 );
3263 return Vec::new();
3264 };
3265
3266 let is_margin_account = {
3267 let cache = self.cache.borrow();
3268 let account = match cache.try_account(&fill.account_id) {
3269 Ok(account) => account,
3270 Err(e) => {
3271 log::error!("Cannot handle order fill: {e}, {fill}");
3272 return Vec::new();
3273 }
3274 };
3275
3276 account.is_margin_account()
3277 };
3278
3279 if !instrument.is_spread() && is_margin_account {
3282 let portfolio_endpoint = MessagingSwitchboard::portfolio_update_order();
3283 msgbus::send_order_event(portfolio_endpoint, OrderEventAny::Filled(fill.clone()));
3284 }
3285
3286 let (position, position_events) = if instrument.is_spread() {
3287 (None, Vec::new())
3288 } else {
3289 let position_events = self.handle_position_update(&instrument, fill.clone(), oms_type);
3290 let position_id = fill.position_id.unwrap();
3291 (
3292 self.cache.borrow().position_owned(&position_id),
3293 position_events,
3294 )
3295 };
3296
3297 if matches!(order.contingency_type(), Some(ContingencyType::Oto)) {
3300 if !instrument.is_spread()
3302 && let Some(ref pos) = position
3303 && pos.is_open()
3304 {
3305 let position_id = pos.id;
3306
3307 for client_order_id in order.linked_order_ids().unwrap_or_default() {
3308 let link = self.cache.borrow_mut().order_mut(client_order_id).and_then(
3312 |mut contingent_order| {
3313 if contingent_order.position_id().is_none() {
3314 contingent_order.set_position_id(Some(position_id));
3315 Some((
3316 contingent_order.instrument_id().venue,
3317 contingent_order.client_order_id(),
3318 contingent_order.strategy_id(),
3319 ))
3320 } else {
3321 None
3322 }
3323 },
3324 );
3325
3326 if let Some((venue, contingent_id, strategy_id)) = link
3327 && let Err(e) = self.cache.borrow_mut().add_position_id(
3328 &position_id,
3329 &venue,
3330 &contingent_id,
3331 &strategy_id,
3332 )
3333 {
3334 log::error!("Failed to add position ID: {e}");
3335 }
3336 }
3337 }
3338 }
3341
3342 let topic = switchboard::get_order_filled_topic(fill.instrument_id);
3343 let event = OrderEventAny::Filled(fill);
3344 msgbus::publish_order_event(topic, &event);
3345
3346 position_events
3347 }
3348
3349 fn prepare_order_fill_void_positions(
3350 &self,
3351 order: &OrderAny,
3352 event: &OrderFillVoided,
3353 ) -> anyhow::Result<Vec<CorrectedPosition>> {
3354 let source_event_id = order
3355 .events()
3356 .into_iter()
3357 .find_map(|order_event| match order_event {
3358 OrderEventAny::Filled(fill) if fill.trade_id == event.trade_id => {
3359 Some(fill.event_id)
3360 }
3361 _ => None,
3362 })
3363 .ok_or_else(|| anyhow::anyhow!("fill {} is not in order history", event.trade_id))?;
3364
3365 let positions: Vec<Position> = {
3366 let cache = self.cache.borrow();
3367 cache
3368 .positions(
3369 None,
3370 Some(&event.instrument_id),
3371 Some(&event.strategy_id),
3372 Some(&event.account_id),
3373 None,
3374 )
3375 .into_iter()
3376 .map(|position| position.cloned())
3377 .collect()
3378 };
3379 let mut fragments = Vec::new();
3380
3381 for position in &positions {
3382 for replay_event in &position.replay_events {
3383 let PositionReplayEvent::Filled(fill) = replay_event else {
3384 continue;
3385 };
3386
3387 if fill.client_order_id != event.client_order_id || fill.trade_id != event.trade_id
3388 {
3389 continue;
3390 }
3391 let split_rank = if fill.event_id == source_event_id {
3392 0
3393 } else if fill.causation_id == Some(source_event_id) {
3394 1
3395 } else {
3396 continue;
3397 };
3398 fragments.push((position.id, split_rank, fill.last_qty, fill.commission));
3399 }
3400 }
3401 anyhow::ensure!(
3402 !fragments.is_empty(),
3403 "no position fragments found for fill {}",
3404 event.trade_id
3405 );
3406 fragments.sort_by_key(|(_, split_rank, _, _)| *split_rank);
3407
3408 let mut allocations = IndexMap::<PositionId, (Quantity, Option<Money>)>::new();
3409 let mut remaining_qty = event.voided_qty;
3410 for (position_id, _, quantity, _) in fragments.iter().rev() {
3411 if remaining_qty.is_zero() {
3412 break;
3413 }
3414 let removed = remaining_qty.min(*quantity);
3415 allocations
3416 .entry(*position_id)
3417 .and_modify(|allocation| allocation.0 = allocation.0 + removed)
3418 .or_insert((removed, None));
3419 remaining_qty = remaining_qty - removed;
3420 }
3421 anyhow::ensure!(
3422 remaining_qty.is_zero(),
3423 "position fragments do not cover voided quantity for fill {}",
3424 event.trade_id
3425 );
3426
3427 if let Some(mut remaining_commission) = event.commission_voided {
3428 for (position_id, _, _, commission) in fragments.iter().rev() {
3429 if remaining_commission.is_zero() {
3430 break;
3431 }
3432 let Some(commission) = commission else {
3433 continue;
3434 };
3435 anyhow::ensure!(
3436 commission.currency == remaining_commission.currency,
3437 "position commission currency differs for fill {}",
3438 event.trade_id
3439 );
3440 let removed_raw = remaining_commission.raw.abs().min(commission.raw.abs());
3441 let removed = Money::from_raw(
3442 removed_raw * remaining_commission.raw.signum(),
3443 remaining_commission.currency,
3444 );
3445 allocations
3446 .entry(*position_id)
3447 .and_modify(|allocation| {
3448 allocation.1 = Some(
3449 allocation
3450 .1
3451 .map_or(removed, |commission| commission + removed),
3452 );
3453 })
3454 .or_insert((Quantity::zero(event.voided_qty.precision), Some(removed)));
3455 remaining_commission = remaining_commission - removed;
3456 }
3457 anyhow::ensure!(
3458 remaining_commission.is_zero(),
3459 "position fragments do not cover voided commission for fill {}",
3460 event.trade_id
3461 );
3462 }
3463
3464 let mut corrected_positions = Vec::new();
3465
3466 for (position_id, (voided_qty, commission_voided)) in allocations {
3467 if voided_qty.is_zero() {
3468 anyhow::bail!(
3469 "commission-only position correction requires authoritative reconciliation for fill {}",
3470 event.trade_id
3471 );
3472 }
3473 let mut position = self
3474 .cache
3475 .borrow()
3476 .position_owned(&position_id)
3477 .ok_or_else(|| anyhow::anyhow!("position {position_id} is not cached"))?;
3478 let previous = position
3479 .fill_voids
3480 .iter()
3481 .rev()
3482 .find(|record| {
3483 record.event.client_order_id == event.client_order_id
3484 && record.event.trade_id == event.trade_id
3485 })
3486 .map(|record| (record.voided_qty, record.commission_voided));
3487 if previous == Some((voided_qty, commission_voided)) {
3488 continue;
3489 }
3490 let corrected_qty = previous.map_or(voided_qty, |(prior_qty, _)| {
3491 voided_qty.saturating_sub(prior_qty)
3492 });
3493
3494 let previously_voided = previous
3503 .map_or(Quantity::zero(position.size_precision), |(prior_qty, _)| {
3504 prior_qty
3505 });
3506 let current_cycle_qty = position
3507 .events
3508 .iter()
3509 .filter(|fill| {
3510 fill.client_order_id == event.client_order_id && fill.trade_id == event.trade_id
3511 })
3512 .fold(previously_voided, |total, fill| total + fill.last_qty);
3513 let absorbed_prior_cycles = voided_qty > current_cycle_qty;
3514 let closed_cycles_pnl =
3515 position.apply_fill_void(event.clone(), voided_qty, commission_voided)?;
3516 corrected_positions.push(CorrectedPosition {
3517 position,
3518 corrected_qty,
3519 absorbed_prior_cycles,
3520 closed_cycles_pnl,
3521 });
3522 }
3523 Ok(corrected_positions)
3524 }
3525
3526 fn create_fill_void_position_event(
3527 position: &Position,
3528 fill_voided: &OrderFillVoided,
3529 corrected_qty: Quantity,
3530 ) -> PositionEvent {
3531 let event_id = UUID4::new();
3532 let ts_init = fill_voided.ts_init;
3533
3534 if position.is_closed() {
3535 PositionEvent::PositionClosed(PositionClosed {
3536 trader_id: position.trader_id,
3537 strategy_id: position.strategy_id,
3538 instrument_id: position.instrument_id,
3539 position_id: position.id,
3540 account_id: position.account_id,
3541 opening_order_id: position.opening_order_id,
3542 closing_order_id: position.closing_order_id,
3543 entry: position.entry,
3544 side: position.side,
3545 signed_qty: position.signed_qty,
3546 quantity: position.quantity,
3547 peak_quantity: position.peak_qty,
3548 last_qty: corrected_qty,
3549 last_px: fill_voided.last_px,
3550 currency: position.quote_currency,
3551 avg_px_open: position.avg_px_open,
3552 avg_px_close: position.avg_px_close,
3553 realized_return: position.realized_return,
3554 realized_pnl: position.realized_pnl,
3555 unrealized_pnl: Money::zero(position.quote_currency),
3556 duration: position.duration_ns,
3557 event_id,
3558 ts_opened: position.ts_opened,
3559 ts_closed: position.ts_closed,
3560 ts_event: fill_voided.ts_event,
3561 ts_init,
3562 })
3563 } else {
3564 PositionEvent::PositionChanged(PositionChanged {
3565 trader_id: position.trader_id,
3566 strategy_id: position.strategy_id,
3567 instrument_id: position.instrument_id,
3568 position_id: position.id,
3569 account_id: position.account_id,
3570 opening_order_id: position.opening_order_id,
3571 entry: position.entry,
3572 side: position.side,
3573 signed_qty: position.signed_qty,
3574 quantity: position.quantity,
3575 peak_quantity: position.peak_qty,
3576 last_qty: corrected_qty,
3577 last_px: fill_voided.last_px,
3578 currency: position.quote_currency,
3579 avg_px_open: position.avg_px_open,
3580 avg_px_close: position.avg_px_close,
3581 realized_return: position.realized_return,
3582 realized_pnl: position.realized_pnl,
3583 unrealized_pnl: Money::zero(position.quote_currency),
3584 event_id,
3585 ts_opened: position.ts_opened,
3586 ts_event: fill_voided.ts_event,
3587 ts_init,
3588 })
3589 }
3590 }
3591
3592 fn handle_position_update(
3596 &mut self,
3597 instrument: &InstrumentAny,
3598 fill: OrderFilled,
3599 oms_type: OmsType,
3600 ) -> Vec<PositionEvent> {
3601 let position_id = if let Some(position_id) = fill.position_id {
3602 position_id
3603 } else {
3604 log::error!("Cannot handle position update: no position ID found for fill {fill}");
3605 return Vec::new();
3606 };
3607
3608 let position_opt = self.cache.borrow().position_owned(&position_id);
3609
3610 match position_opt {
3611 None => {
3612 if self.reject_reduce_only_position_open(&fill, oms_type) {
3613 return Vec::new();
3614 }
3615
3616 self.open_position(instrument, None, fill, oms_type)
3617 .unwrap_or_default()
3618 }
3619 Some(pos) if pos.is_closed() => {
3620 if self.reject_reduce_only_position_open(&fill, oms_type) {
3621 return Vec::new();
3622 }
3623
3624 self.open_position(instrument, Some(&pos), fill, oms_type)
3625 .unwrap_or_default()
3626 }
3627 Some(mut pos) => {
3628 if self.will_flip_position(&pos, &fill) {
3629 self.flip_position(instrument, &mut pos, &fill, oms_type)
3630 } else {
3631 self.update_position(&mut pos, &fill).into_iter().collect()
3632 }
3633 }
3634 }
3635 }
3636
3637 fn reject_reduce_only_position_open(&self, fill: &OrderFilled, oms_type: OmsType) -> bool {
3638 let cache = self.cache.borrow();
3639 let Some(order) = cache.order_owned(&fill.client_order_id) else {
3640 return false;
3641 };
3642
3643 if !order.is_reduce_only() {
3644 return false;
3645 }
3646
3647 let positions_open = cache.positions_open(
3648 None,
3649 Some(&fill.instrument_id),
3650 None,
3651 Some(&fill.account_id),
3652 None,
3653 );
3654 let position_id = fill
3655 .position_id
3656 .map_or_else(|| "None".to_string(), |position_id| position_id.to_string());
3657 let matching_position_details = Self::position_details(
3658 positions_open
3659 .iter()
3660 .filter(|position| position.is_opposite_side(fill.order_side))
3661 .map(|position| &**position),
3662 );
3663 let open_position_details =
3664 Self::position_details(positions_open.iter().map(|position| &**position));
3665
3666 log::error!(
3667 "Cannot open {oms_type} position {position_id} from reduce-only fill {} for {}; \
3668 matching_reduce_positions=[{}], open_positions=[{}]",
3669 fill.trade_id,
3670 fill.instrument_id,
3671 matching_position_details,
3672 open_position_details
3673 );
3674
3675 true
3676 }
3677
3678 #[allow(
3679 clippy::needless_pass_by_value,
3680 reason = "takes the opening fill by value to seed the new position"
3681 )]
3682 fn open_position(
3683 &self,
3684 instrument: &InstrumentAny,
3685 position: Option<&Position>,
3686 fill: OrderFilled,
3687 oms_type: OmsType,
3688 ) -> anyhow::Result<Vec<PositionEvent>> {
3689 if let Some(position) = position {
3690 if Self::is_duplicate_closed_fill(position, &fill) {
3691 log::warn!(
3692 "Ignoring duplicate fill {} for closed position {}; no position reopened (side={:?}, qty={}, px={})",
3693 fill.trade_id,
3694 position.id,
3695 fill.order_side,
3696 fill.last_qty,
3697 fill.last_px
3698 );
3699 return Ok(Vec::new());
3700 }
3701 self.reopen_position(position, oms_type)?;
3702 }
3703
3704 let prior_position = if self.config.carry_replay_events_on_reopen {
3706 position.cloned().or_else(|| {
3707 fill.position_id
3708 .and_then(|position_id| self.cache.borrow().position_owned(&position_id))
3709 })
3710 } else {
3711 None
3712 };
3713 let mut position = Position::new(instrument, fill.clone());
3714 if let Some(prior) = prior_position
3715 && prior.id == position.id
3716 {
3717 let current_replay = position.replay_events.clone();
3718 position.replay_events = prior.replay_events;
3719 position.replay_events.extend(current_replay);
3720 position.fill_voids = prior.fill_voids;
3721 }
3722 self.cache.borrow_mut().add_position(&position, oms_type)?;
3723
3724 if self.config.snapshot_positions {
3725 self.create_position_state_snapshot(&position, true);
3726 }
3727
3728 let ts_init = self.clock.borrow().timestamp_ns();
3729 let event = PositionOpened::create(&position, &fill, UUID4::new(), ts_init);
3730
3731 Ok(vec![PositionEvent::PositionOpened(event)])
3732 }
3733
3734 fn is_duplicate_closed_fill(position: &Position, fill: &OrderFilled) -> bool {
3735 position.replay_events.iter().any(|event| {
3736 matches!(
3737 event,
3738 PositionReplayEvent::Filled(replayed) if replayed.trade_id == fill.trade_id
3739 )
3740 })
3741 }
3742
3743 fn reopen_position(&self, position: &Position, oms_type: OmsType) -> anyhow::Result<()> {
3744 if oms_type == OmsType::Netting {
3745 if position.is_open() {
3746 anyhow::bail!(
3747 "Cannot reopen position {} (oms_type=NETTING): reopening is only valid for closed positions in NETTING mode",
3748 position.id
3749 );
3750 }
3751 self.snapshot_position(position)?;
3753 } else {
3754 log::warn!(
3756 "Received fill for closed position {} in HEDGING mode; creating new position and ignoring previous state",
3757 position.id
3758 );
3759 }
3760 Ok(())
3761 }
3762
3763 fn snapshot_position(&self, position: &Position) -> anyhow::Result<()> {
3768 let mut cache = self.cache.borrow_mut();
3769
3770 let Some(anchorer) = &self.snapshot_anchorer else {
3771 return cache.snapshot_position(position);
3772 };
3773
3774 let snapshot_ref = cache.snapshot_position_encoded(position)?;
3775 drop(cache);
3776
3777 if let Err(e) = anchorer(snapshot_ref) {
3778 log::warn!("Failed to record cache snapshot anchor: {e}");
3779 }
3780
3781 Ok(())
3782 }
3783
3784 fn update_position(
3785 &self,
3786 position: &mut Position,
3787 fill: &OrderFilled,
3788 ) -> Option<PositionEvent> {
3789 position.apply(fill);
3791
3792 let is_closed = position.is_closed();
3794
3795 if let Err(e) = self.cache.borrow_mut().update_position(position) {
3797 log::error!("Failed to update position: {e:?}");
3798 return None;
3799 }
3800
3801 let cache = self.cache.borrow();
3803
3804 drop(cache);
3805
3806 if self.config.snapshot_positions {
3808 self.create_position_state_snapshot(position, false);
3809 }
3810
3811 let ts_init = self.clock.borrow().timestamp_ns();
3812
3813 if is_closed {
3814 let event = PositionClosed::create(position, fill, UUID4::new(), ts_init);
3815 Some(PositionEvent::PositionClosed(event))
3816 } else {
3817 let event = PositionChanged::create(position, fill, UUID4::new(), ts_init);
3818 Some(PositionEvent::PositionChanged(event))
3819 }
3820 }
3821
3822 fn will_flip_position(&self, position: &Position, fill: &OrderFilled) -> bool {
3823 position.is_opposite_side(fill.order_side) && (fill.last_qty.raw > position.quantity.raw)
3824 }
3825
3826 fn position_signed_decimal_qty(position: &Position) -> Decimal {
3827 match position.side {
3828 PositionSide::Long => position.quantity.as_decimal(),
3829 PositionSide::Short => -position.quantity.as_decimal(),
3830 _ => Decimal::ZERO,
3831 }
3832 }
3833
3834 fn position_details<'a>(positions: impl IntoIterator<Item = &'a Position>) -> String {
3835 positions
3836 .into_iter()
3837 .map(|position| {
3838 format!(
3839 "{} strategy_id={} signed_qty={}",
3840 position.id,
3841 position.strategy_id,
3842 Self::position_signed_decimal_qty(position)
3843 )
3844 })
3845 .collect::<Vec<_>>()
3846 .join(", ")
3847 }
3848
3849 fn flip_position(
3850 &mut self,
3851 instrument: &InstrumentAny,
3852 position: &mut Position,
3853 fill: &OrderFilled,
3854 oms_type: OmsType,
3855 ) -> Vec<PositionEvent> {
3856 let mut position_events = Vec::new();
3857 let difference = match position.side {
3858 PositionSide::Long => Quantity::from_raw(
3859 fill.last_qty.raw - position.quantity.raw,
3860 position.size_precision,
3861 ),
3862 PositionSide::Short => Quantity::from_raw(
3863 position.quantity.raw.abs_diff(fill.last_qty.raw), position.size_precision,
3865 ),
3866 _ => fill.last_qty,
3867 };
3868
3869 let fill_percent = position.quantity.as_decimal() / fill.last_qty.as_decimal();
3871 let (commission1, commission2) = if let Some(commission) = fill.commission {
3872 let commission_currency = commission.currency;
3873 let commission1 =
3874 Money::from_decimal(commission.as_decimal() * fill_percent, commission_currency)
3875 .expect("Invalid split commission");
3876 let commission2 = commission - commission1;
3877 (Some(commission1), Some(commission2))
3878 } else {
3879 log::warn!(
3880 "Commission is not available for position flip, splitting with no commission"
3881 );
3882 (None, None)
3883 };
3884
3885 let mut fill_split1: Option<OrderFilled> = None;
3886
3887 if position.is_open() {
3888 let mut split = OrderFilled::new(
3889 fill.trader_id,
3890 fill.strategy_id,
3891 fill.instrument_id,
3892 fill.client_order_id,
3893 fill.venue_order_id,
3894 fill.account_id,
3895 fill.trade_id,
3896 fill.order_side,
3897 fill.order_type,
3898 position.quantity,
3899 fill.last_px,
3900 fill.currency,
3901 fill.liquidity_side,
3902 fill.event_id,
3903 fill.ts_event,
3904 fill.ts_init,
3905 fill.reconciliation,
3906 fill.position_id,
3907 commission1,
3908 fill.info.clone(),
3909 );
3910 split.causation_id = fill.causation_id;
3911 fill_split1 = Some(split);
3912
3913 if let Some(position_event) =
3914 self.update_position(position, fill_split1.as_ref().unwrap())
3915 {
3916 position_events.push(position_event);
3917 }
3918
3919 if oms_type == OmsType::Netting
3921 && let Err(e) = self.snapshot_position(position)
3922 {
3923 log::warn!("Failed to snapshot position during flip: {e:?}");
3924 }
3925 }
3926
3927 if difference.raw == 0 {
3929 log::warn!(
3930 "Zero fill size during position flip calculation, this could be caused by a mismatch between instrument `size_precision` and a quantity `size_precision`"
3931 );
3932 return position_events;
3933 }
3934
3935 let position_id_flip = if oms_type == OmsType::Hedging
3936 && let Some(position_id) = fill.position_id
3937 && position_id.is_virtual()
3938 {
3939 Some(self.pos_id_generator.generate(fill.strategy_id, true))
3941 } else {
3942 fill.position_id
3944 };
3945
3946 let mut fill_split2 = OrderFilled::new(
3947 fill.trader_id,
3948 fill.strategy_id,
3949 fill.instrument_id,
3950 fill.client_order_id,
3951 fill.venue_order_id,
3952 fill.account_id,
3953 fill.trade_id,
3954 fill.order_side,
3955 fill.order_type,
3956 difference,
3957 fill.last_px,
3958 fill.currency,
3959 fill.liquidity_side,
3960 UUID4::new(),
3961 fill.ts_event,
3962 fill.ts_init,
3963 fill.reconciliation,
3964 position_id_flip,
3965 commission2,
3966 fill.info.clone(),
3967 );
3968 fill_split2.causation_id = Some(fill.event_id);
3969
3970 if oms_type == OmsType::Hedging
3971 && let Some(position_id) = fill.position_id
3972 && position_id.is_virtual()
3973 {
3974 log::warn!("Closing position {fill_split1:?}");
3975 log::warn!("Flipping position {fill_split2:?}");
3976 }
3977
3978 match self.open_position(instrument, None, fill_split2, oms_type) {
3980 Ok(opened_events) => position_events.extend(opened_events),
3981 Err(e) => log::error!("Failed to open flipped position: {e:?}"),
3982 }
3983
3984 position_events
3985 }
3986
3987 pub fn set_position_id_counts(&mut self) {
3989 let cache = self.cache.borrow();
3990 let positions = cache.positions(None, None, None, None, None);
3991
3992 let mut counts: HashMap<StrategyId, usize> = HashMap::new();
3994
3995 for position in positions {
3996 *counts.entry(position.strategy_id).or_insert(0) += 1;
3997 }
3998
3999 self.pos_id_generator.reset();
4000
4001 for (strategy_id, count) in counts {
4002 self.pos_id_generator.set_count(count, strategy_id);
4003 log::info!("Set PositionId count for {strategy_id} to {count}");
4004 }
4005 }
4006
4007 fn deny_order(&self, order: &OrderAny, reason: &str) {
4008 let denied = OrderDenied::new(
4009 order.trader_id(),
4010 order.strategy_id(),
4011 order.instrument_id(),
4012 order.client_order_id(),
4013 reason.into(),
4014 UUID4::new(),
4015 self.clock.borrow().timestamp_ns(),
4016 self.clock.borrow().timestamp_ns(),
4017 );
4018
4019 let event = OrderEventAny::Denied(denied);
4020 let order = match self.cache.borrow_mut().update_order(&event) {
4021 Ok(order) => order,
4022 Err(e) => {
4023 log::error!("Failed to apply denied event to order: {e}");
4024 return;
4025 }
4026 };
4027
4028 let topic = switchboard::get_event_order_topic(order.strategy_id());
4029 msgbus::publish_order_event(topic, &event);
4030
4031 if self.config.snapshot_orders {
4032 self.create_order_state_snapshot(&order);
4033 }
4034 }
4035
4036 fn get_or_init_own_order_book(&self, instrument_id: &InstrumentId) -> RefMut<'_, OwnOrderBook> {
4037 let mut cache = self.cache.borrow_mut();
4038 if cache.own_order_book_mut(instrument_id).is_none() {
4039 let own_book = OwnOrderBook::new(*instrument_id);
4040 cache.add_own_order_book(own_book).unwrap();
4041 }
4042
4043 RefMut::map(cache, |c| c.own_order_book_mut(instrument_id).unwrap())
4044 }
4045}
4046
4047#[cfg(test)]
4048mod tests {
4049 use nautilus_common::clock::TestClock;
4050 use nautilus_model::{
4051 enums::{LiquiditySide, OrderSide, OrderType, PositionSideSpecified},
4052 events::order::spec::OrderFilledSpec,
4053 identifiers::{AccountId, ClientOrderId, TradeId, VenueOrderId},
4054 instruments::{InstrumentAny, stubs::audusd_sim},
4055 orders::builder::OrderTestBuilder,
4056 types::Price,
4057 };
4058 use rstest::*;
4059
4060 use super::*;
4061
4062 #[rstest]
4063 fn netting_positions_open_for_report_scopes_positions_by_account() {
4064 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
4065 let account1_id = AccountId::from("SIM-001");
4066 let account2_id = AccountId::from("SIM-002");
4067 let position1 = position_for_account(
4068 &instrument,
4069 account1_id,
4070 StrategyId::from("S-001"),
4071 PositionId::from("P-ACC-1"),
4072 OrderSide::Buy,
4073 Quantity::from(1_000),
4074 );
4075 let position2 = position_for_account(
4076 &instrument,
4077 account2_id,
4078 StrategyId::from("S-002"),
4079 PositionId::from("P-ACC-2"),
4080 OrderSide::Buy,
4081 Quantity::from(2_000),
4082 );
4083 let mut cache = Cache::default();
4084 cache.add_position(&position1, OmsType::Netting).unwrap();
4085 cache.add_position(&position2, OmsType::Netting).unwrap();
4086
4087 let report = PositionStatusReport::new(
4088 account1_id,
4089 instrument.id(),
4090 PositionSideSpecified::Long,
4091 Quantity::from(1_000),
4092 UnixNanos::from(1_000_000),
4093 UnixNanos::from(1_000_000),
4094 None,
4095 None,
4096 None,
4097 );
4098
4099 let positions_open = ExecutionEngine::netting_positions_open_for_report(&cache, &report);
4100 let signed_qty: Decimal = positions_open
4101 .iter()
4102 .map(|position| ExecutionEngine::position_signed_decimal_qty(position))
4103 .sum();
4104
4105 assert_eq!(positions_open.len(), 1);
4106 assert_eq!(positions_open[0].id, position1.id);
4107 assert_eq!(signed_qty, Decimal::from(1_000));
4108 }
4109
4110 #[rstest]
4111 fn netting_split_position_ownership_message_reports_only_split_ownership() {
4112 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
4113 let account_id = AccountId::from("SIM-001");
4114 let external_position = position_for_account(
4115 &instrument,
4116 account_id,
4117 StrategyId::from("EXTERNAL"),
4118 PositionId::from("P-EXTERNAL"),
4119 OrderSide::Buy,
4120 Quantity::from(1_000),
4121 );
4122 let strategy_position = position_for_account(
4123 &instrument,
4124 account_id,
4125 StrategyId::from("S-001"),
4126 PositionId::from("P-STRATEGY"),
4127 OrderSide::Buy,
4128 Quantity::from(500),
4129 );
4130 let same_strategy_position = position_for_account(
4131 &instrument,
4132 account_id,
4133 StrategyId::from("EXTERNAL"),
4134 PositionId::from("P-EXTERNAL-2"),
4135 OrderSide::Buy,
4136 Quantity::from(250),
4137 );
4138 let report = PositionStatusReport::new(
4139 account_id,
4140 instrument.id(),
4141 PositionSideSpecified::Long,
4142 Quantity::from(1_500),
4143 UnixNanos::from(1_000_000),
4144 UnixNanos::from(1_000_000),
4145 None,
4146 None,
4147 None,
4148 );
4149
4150 let message = ExecutionEngine::netting_split_position_ownership_message(
4151 &report,
4152 &[&external_position, &strategy_position],
4153 )
4154 .expect("split ownership should produce a warning message");
4155
4156 assert!(message.contains("account_id=SIM-001"));
4157 assert!(message.contains(&format!("instrument_id={}", instrument.id())));
4158 assert!(message.contains("EXTERNAL"));
4159 assert!(message.contains("S-001"));
4160 assert!(message.contains("P-EXTERNAL"));
4161 assert!(message.contains("P-STRATEGY"));
4162 assert!(message.contains("signed_qty=1000"));
4163 assert!(message.contains("signed_qty=500"));
4164 assert!(
4165 ExecutionEngine::netting_split_position_ownership_message(
4166 &report,
4167 &[&external_position, &same_strategy_position],
4168 )
4169 .is_none()
4170 );
4171 }
4172
4173 #[rstest]
4174 fn materialize_external_order_rejects_venue_id_owned_by_another_order() {
4175 let cache = Rc::new(RefCell::new(Cache::default()));
4176 let venue_order_id = VenueOrderId::from("V-SHARED");
4177 let owner_id = ClientOrderId::from("O-OWNER");
4178 cache
4179 .borrow_mut()
4180 .add_venue_order_id(&owner_id, &venue_order_id, false)
4181 .unwrap();
4182 let engine = ExecutionEngine::new(
4183 Rc::new(RefCell::new(TestClock::new())),
4184 Rc::clone(&cache),
4185 None,
4186 );
4187 let instrument = InstrumentAny::CurrencyPair(audusd_sim());
4188 let claimant_id = ClientOrderId::from("O-CLAIMANT");
4189 let order = OrderTestBuilder::new(OrderType::Limit)
4190 .instrument_id(instrument.id())
4191 .client_order_id(claimant_id)
4192 .side(OrderSide::Buy)
4193 .quantity(Quantity::from(100_000))
4194 .price(Price::from("1.00000"))
4195 .build();
4196 let OrderEventAny::Initialized(initialized) = order.last_event().clone() else {
4197 panic!("Expected initialized order");
4198 };
4199
4200 let result = engine.materialize_external_order(
4201 initialized,
4202 claimant_id,
4203 venue_order_id,
4204 instrument.id(),
4205 order.strategy_id(),
4206 UnixNanos::default(),
4207 None,
4208 );
4209
4210 assert!(result.is_none());
4211 assert!(!cache.borrow().order_exists(&claimant_id));
4212 assert_eq!(
4213 cache.borrow().client_order_id(&venue_order_id),
4214 Some(&owner_id)
4215 );
4216 assert_eq!(cache.borrow().venue_order_id(&claimant_id), None);
4217 }
4218
4219 fn position_for_account(
4220 instrument: &InstrumentAny,
4221 account_id: AccountId,
4222 strategy_id: StrategyId,
4223 position_id: PositionId,
4224 order_side: OrderSide,
4225 quantity: Quantity,
4226 ) -> Position {
4227 let client_order_id = ClientOrderId::from(format!("O-{position_id}"));
4228 let fill = OrderFilledSpec::builder()
4229 .strategy_id(strategy_id)
4230 .instrument_id(instrument.id())
4231 .client_order_id(client_order_id)
4232 .venue_order_id(VenueOrderId::from(format!("V-{position_id}")))
4233 .account_id(account_id)
4234 .trade_id(TradeId::new(format!("T-{position_id}")))
4235 .order_side(order_side)
4236 .last_qty(quantity)
4237 .last_px(Price::from("1.0"))
4238 .currency(instrument.quote_currency())
4239 .liquidity_side(LiquiditySide::Maker)
4240 .position_id(position_id)
4241 .commission(Money::from("2 USD"))
4242 .build();
4243
4244 Position::new(instrument, fill)
4245 }
4246}