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