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