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