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