1#[path = "core_helpers.rs"]
19mod core_helpers;
20#[path = "core_orders.rs"]
21mod core_orders;
22#[path = "core_updates.rs"]
23mod core_updates;
24#[cfg(test)]
25#[path = "core_tests.rs"]
26mod tests;
27
28use std::{
29 collections::VecDeque,
30 fmt::Debug,
31 str::FromStr,
32 sync::{
33 Arc,
34 atomic::{AtomicBool, Ordering},
35 },
36 time::Duration,
37};
38
39use ahash::AHashMap;
40use anyhow::Context;
41use ibapi::{
42 accounts::PositionUpdate,
43 client::Client,
44 contracts::{Contract, SecurityType},
45 orders::{
46 ExecutionData, ExecutionFilter, Executions, OrderStatus as IBOrderStatus, OrderUpdate,
47 Orders,
48 },
49 prelude::{StreamExt, SubscriptionItemStreamExt},
50};
51use nautilus_common::{
52 cache::{Cache, fifo::FifoCacheMap},
53 clients::ExecutionClient,
54 enums::LogLevel,
55 factories::OrderEventFactory,
56 live::runner::get_exec_event_sender,
57 messages::{
58 ExecutionEvent,
59 execution::{
60 BatchCancelOrders, CancelAllOrders, CancelOrder, ExecutionReport, GenerateFillReports,
61 GenerateFillReportsBuilder, GenerateOrderStatusReport, GenerateOrderStatusReports,
62 GenerateOrderStatusReportsBuilder, GeneratePositionStatusReports,
63 GeneratePositionStatusReportsBuilder, ModifyOrder, QueryAccount, QueryOrder,
64 SubmitOrder, SubmitOrderList,
65 },
66 },
67 msgbus::{send_account_state, switchboard::MessagingSwitchboard},
68};
69use nautilus_core::{
70 Params, UUID4, UnixNanos,
71 time::{AtomicTime, get_atomic_clock_realtime},
72};
73use nautilus_live::{
74 ExecutionClientCore,
75 execution::failure::CommandFailure,
76 task::{TaskGroup, TaskGroupGuard},
77};
78use nautilus_model::{
79 accounts::AccountAny,
80 enums::{
81 LiquiditySide, OmsType, OrderSide, OrderStatus, OrderType, PositionSide, TimeInForce,
82 TrailingOffsetType,
83 },
84 events::{
85 AccountState, OrderAccepted, OrderCancelRejected, OrderCanceled, OrderDenied,
86 OrderEventAny, OrderFilled, OrderModifyRejected, OrderPendingCancel, OrderRejected,
87 OrderSubmitted, OrderUpdated,
88 },
89 identifiers::{
90 AccountId, ClientId, ClientOrderId, InstrumentId, StrategyId, TradeId, TraderId, Venue,
91 VenueOrderId,
92 },
93 instruments::{Instrument, InstrumentAny},
94 orders::{Order, any::OrderAny},
95 reports::{ExecutionMassStatus, FillReport, OrderStatusReport, PositionStatusReport},
96 types::{AccountBalance, Currency, MarginBalance, Money, Price, Quantity},
97};
98use parking_lot::Mutex;
99use rust_decimal::{Decimal, prelude::ToPrimitive};
100use ustr::Ustr;
101
102use super::{
103 account::{PositionTracker, create_position_tracker, raw_ib_account_code},
104 parse::{
105 ib_venue_order_id, parse_execution_time, parse_execution_to_fill_report,
106 parse_order_status_to_report,
107 },
108 transform::nautilus_order_to_ib_order,
109};
110use crate::{
111 common::{
112 parse::{ib_contract_to_instrument_id_simple, is_spread_instrument_id},
113 shared_client::SharedClientHandle,
114 },
115 config::InteractiveBrokersExecutionClientConfig,
116 providers::instruments::InteractiveBrokersInstrumentProvider,
117};
118
119#[cfg_attr(
124 feature = "python",
125 pyo3::pyclass(module = "nautilus_trader.adapters.interactive_brokers", unsendable)
126)]
127pub struct InteractiveBrokersExecutionClient {
128 core: ExecutionClientCore,
129 config: InteractiveBrokersExecutionClientConfig,
130 instrument_provider: Arc<InteractiveBrokersInstrumentProvider>,
131 is_connected: AtomicBool,
132 ib_client: Option<SharedClientHandle>,
133 pending_tasks: TaskGroup,
134 next_order_id: Arc<Mutex<i32>>,
135 order_submit_lock: Arc<tokio::sync::Mutex<()>>,
136 session_tasks: TaskGroup,
137 order_id_map: Arc<Mutex<AHashMap<ClientOrderId, i32>>>,
138 venue_order_id_map: Arc<Mutex<AHashMap<i32, ClientOrderId>>>,
139 commission_cache: Arc<Mutex<CommissionCache>>,
140 pending_execution_cache: Arc<Mutex<PendingExecutionCache>>,
141 instrument_id_map: Arc<Mutex<AHashMap<i32, InstrumentId>>>,
142 trader_id_map: Arc<Mutex<AHashMap<i32, TraderId>>>,
143 strategy_id_map: Arc<Mutex<AHashMap<i32, StrategyId>>>,
144 active_order_contexts: Arc<Mutex<AHashMap<i32, TrackedOrderContext>>>,
145 terminal_order_contexts: Arc<Mutex<FifoCacheMap<i32, TrackedOrderContext, 10_000>>>,
146 spread_fill_tracking: Arc<Mutex<AHashMap<ClientOrderId, ahash::AHashSet<String>>>>,
147 position_tracker: PositionTracker,
148 order_avg_prices: Arc<Mutex<AHashMap<ClientOrderId, Price>>>,
149 pending_combo_fills: Arc<Mutex<AHashMap<ClientOrderId, VecDeque<PendingComboFill>>>>,
150 pending_combo_fill_avgs: Arc<Mutex<AHashMap<ClientOrderId, VecDeque<(Decimal, Price)>>>>,
151 order_fill_progress: Arc<Mutex<AHashMap<ClientOrderId, (Decimal, Decimal)>>>,
152 pending_cancel_orders: Arc<Mutex<ahash::AHashSet<ClientOrderId>>>,
153}
154
155type CommissionCache = FifoCacheMap<String, (f64, String), 10_000>;
156type PendingExecutionCache = FifoCacheMap<String, ExecutionData, 10_000>;
157
158#[derive(Clone, Debug)]
159struct PendingComboFill {
160 trader_id: TraderId,
161 strategy_id: StrategyId,
162 account_id: AccountId,
163 instrument_id: InstrumentId,
164 venue_order_id: VenueOrderId,
165 trade_id: TradeId,
166 order_side: OrderSide,
167 order_type: OrderType,
168 last_qty: Quantity,
169 commission: Money,
170 liquidity_side: LiquiditySide,
171 quote_currency: Currency,
172 client_order_id: ClientOrderId,
173 ts_event: UnixNanos,
174 ts_init: UnixNanos,
175}
176
177#[derive(Clone, Debug)]
178struct TrackedOrderContext {
179 client_order_id: ClientOrderId,
180 trader_id: TraderId,
181 strategy_id: StrategyId,
182 instrument_id: InstrumentId,
183 order_side: OrderSide,
184 order_type: OrderType,
185 accepted: bool,
186 avg_px: Option<Price>,
187}
188
189#[derive(Debug, Clone, Copy, PartialEq, Eq)]
190enum IbOrderSelector {
191 OrderId(i32),
192 PermId(i64),
193}
194
195impl IbOrderSelector {
196 fn from_venue_order_id(venue_order_id: &VenueOrderId) -> anyhow::Result<Self> {
197 let raw = venue_order_id.as_str();
198 if let Some(perm_id) = raw.strip_prefix("PERM-") {
199 return Ok(Self::PermId(perm_id.parse::<i64>().with_context(|| {
200 format!("Failed to parse venue_order_id {raw:?} as IB perm_id")
201 })?));
202 }
203
204 Ok(Self::OrderId(raw.parse::<i32>().with_context(|| {
205 format!("Failed to parse venue_order_id {raw:?} as IB order_id")
206 })?))
207 }
208
209 fn matches(self, order_id: i32, perm_id: i64) -> bool {
210 match self {
211 Self::OrderId(target_order_id) => order_id == target_order_id,
212 Self::PermId(target_perm_id) => perm_id == target_perm_id,
213 }
214 }
215
216 fn venue_order_id(self) -> VenueOrderId {
217 match self {
218 Self::OrderId(order_id) => VenueOrderId::from(order_id.to_string()),
219 Self::PermId(perm_id) => VenueOrderId::from(format!("PERM-{perm_id}")),
220 }
221 }
222
223 fn label(self) -> String {
224 self.venue_order_id().to_string()
225 }
226}
227
228impl Debug for InteractiveBrokersExecutionClient {
229 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
230 f.debug_struct(stringify!(InteractiveBrokersExecutionClient))
231 .field("core", &self.core)
232 .field("config", &self.config)
233 .field("instrument_provider", &self.instrument_provider)
234 .field("is_connected", &self.is_connected.load(Ordering::Relaxed))
235 .field("ib_client", &self.ib_client.is_some())
236 .finish_non_exhaustive()
237 }
238}
239
240impl InteractiveBrokersExecutionClient {
241 pub fn new(
253 mut core: ExecutionClientCore,
254 config: InteractiveBrokersExecutionClientConfig,
255 instrument_provider: Arc<InteractiveBrokersInstrumentProvider>,
256 ) -> anyhow::Result<Self> {
257 anyhow::ensure!(
258 !config.client_id.unsigned_abs().is_multiple_of(1000),
259 "Interactive Brokers execution client_id must not be a multiple of 1000 because order ID partitioning uses client_id % 1000; got {}",
260 config.client_id
261 );
262
263 if let Some(account_id) = &config.account_id {
265 core.account_id = AccountId::from(account_id.clone());
266 }
267
268 let pending_tasks = TaskGroup::new();
269 let session_tasks = TaskGroup::new();
270
271 Ok(Self {
272 core,
273 config,
274 instrument_provider,
275 is_connected: AtomicBool::new(false),
276 ib_client: None,
277 pending_tasks,
278 next_order_id: Arc::new(Mutex::new(0)),
279 order_submit_lock: Arc::new(tokio::sync::Mutex::new(())),
280 session_tasks,
281 order_id_map: Arc::new(Mutex::new(AHashMap::new())),
282 venue_order_id_map: Arc::new(Mutex::new(AHashMap::new())),
283 commission_cache: Arc::new(Mutex::new(CommissionCache::new())),
284 pending_execution_cache: Arc::new(Mutex::new(PendingExecutionCache::new())),
285 instrument_id_map: Arc::new(Mutex::new(AHashMap::new())),
286 trader_id_map: Arc::new(Mutex::new(AHashMap::new())),
287 strategy_id_map: Arc::new(Mutex::new(AHashMap::new())),
288 active_order_contexts: Arc::new(Mutex::new(AHashMap::new())),
289 terminal_order_contexts: Arc::new(Mutex::new(FifoCacheMap::new())),
290 spread_fill_tracking: Arc::new(Mutex::new(AHashMap::new())),
291 position_tracker: create_position_tracker(),
292 order_avg_prices: Arc::new(Mutex::new(AHashMap::new())),
293 pending_combo_fills: Arc::new(Mutex::new(AHashMap::new())),
294 pending_combo_fill_avgs: Arc::new(Mutex::new(AHashMap::new())),
295 order_fill_progress: Arc::new(Mutex::new(AHashMap::new())),
296 pending_cancel_orders: Arc::new(Mutex::new(ahash::AHashSet::new())),
297 })
298 }
299
300 fn submit_order_list_with_orders(
301 &self,
302 cmd: SubmitOrderList,
303 orders: Vec<OrderAny>,
304 ) -> anyhow::Result<()> {
305 let client = self.ib_client.as_ref().context("IB client not connected")?;
306
307 let order_id_map = Arc::clone(&self.order_id_map);
308 let venue_order_id_map = Arc::clone(&self.venue_order_id_map);
309 let instrument_id_map = Arc::clone(&self.instrument_id_map);
310 let trader_id_map = Arc::clone(&self.trader_id_map);
311 let strategy_id_map = Arc::clone(&self.strategy_id_map);
312 let active_order_contexts = Arc::clone(&self.active_order_contexts);
313 let terminal_order_contexts = Arc::clone(&self.terminal_order_contexts);
314 let next_order_id = Arc::clone(&self.next_order_id);
315 let instrument_provider = Arc::clone(&self.instrument_provider);
316 let exec_sender = get_exec_event_sender();
317 let clock = get_atomic_clock_realtime();
318 let account_id = self.core.account_id;
319 let strategy_id = cmd.strategy_id;
320 let client_clone = client.as_arc().clone();
321 let order_submit_lock = Arc::clone(&self.order_submit_lock);
322
323 let future = async move {
324 if let Err(e) = Self::handle_submit_order_list_async(
325 &cmd,
326 &orders,
327 &client_clone,
328 &order_id_map,
329 &venue_order_id_map,
330 &instrument_id_map,
331 &trader_id_map,
332 &strategy_id_map,
333 &active_order_contexts,
334 &terminal_order_contexts,
335 &next_order_id,
336 &instrument_provider,
337 &exec_sender,
338 clock,
339 account_id,
340 strategy_id,
341 &order_submit_lock,
342 )
343 .await
344 {
345 tracing::error!("Error submitting order list: {e}");
346 }
347 };
348
349 self.pending_tasks
350 .spawn(future)
351 .context("failed to register IB execution command task")?;
352
353 Ok(())
354 }
355
356 fn cached_order_for_modify(&self, client_order_id: &ClientOrderId) -> Option<OrderAny> {
357 self.core.cache().order(client_order_id).map(|o| o.clone())
358 }
359
360 fn reserve_next_local_order_id(next_order_id: &Arc<Mutex<i32>>) -> anyhow::Result<i32> {
361 let mut guard = next_order_id.lock();
362 anyhow::ensure!(
363 *guard > 0,
364 "No valid Interactive Brokers order ID available"
365 );
366 let order_id = *guard;
367 *guard += 1;
368 Ok(order_id)
369 }
370
371 fn apply_client_order_id_floor(next_id: i32, client_id: i32) -> i32 {
372 let client_slot = client_id.unsigned_abs() % 1000;
373 if client_slot == 0 {
374 return next_id;
375 }
376
377 let order_id_floor = (client_slot as i32) * 1_000_000;
378 if next_id > order_id_floor {
379 next_id
380 } else {
381 order_id_floor.saturating_add(next_id.max(1))
382 }
383 }
384
385 async fn get_next_order_id(&self) -> anyhow::Result<i32> {
391 let client = self.ib_client.as_ref().context("IB client not connected")?;
392
393 let timeout_dur = Duration::from_secs(self.config.request_timeout);
394 let order_id = tokio::time::timeout(timeout_dur, client.next_valid_order_id())
395 .await
396 .context("Timeout getting next order ID")??;
397 Ok(order_id)
398 }
399
400 async fn get_highest_open_order_id(&self, client: &Client) -> anyhow::Result<Option<i32>> {
401 let timeout_dur = Duration::from_secs(self.config.request_timeout);
402 let subscription = tokio::time::timeout(timeout_dur, client.all_open_orders())
403 .await
404 .context("Timeout requesting open orders for next order ID initialization")??;
405 let mut subscription = subscription.filter_data();
406 let mut highest_order_id = None;
407
408 while let Some(order_result) = subscription.next().await {
409 match order_result {
410 Ok(Orders::OrderData(data)) => {
411 highest_order_id = Some(
412 highest_order_id
413 .map_or(data.order_id, |current: i32| current.max(data.order_id)),
414 );
415 }
416 Ok(_) => {}
417 Err(e) => {
418 tracing::debug!(
419 "Ignoring open-order event while initializing next order ID: {e}"
420 );
421 }
422 }
423 }
424
425 Ok(highest_order_id)
426 }
427
428 fn begin_task_shutdown(&self) {
429 self.pending_tasks.begin_shutdown();
430 self.session_tasks.begin_shutdown();
431 self.is_connected.store(false, Ordering::Release);
432 self.core.set_disconnected();
433 }
434
435 async fn finish_tasks(&self) -> anyhow::Result<()> {
436 let (session_result, pending_result) = tokio::join!(
437 self.session_tasks
438 .finish_shutdown(Duration::from_secs(1), Duration::from_secs(2)),
439 self.pending_tasks
440 .finish_shutdown(Duration::from_secs(2), Duration::from_secs(2)),
441 );
442 session_result.context("failed to finish IB execution session tasks")?;
443 pending_result.context("failed to finish IB execution command tasks")?;
444 Ok(())
445 }
446
447 async fn prepare_task_groups(&mut self) -> anyhow::Result<()> {
448 if !self.session_tasks.is_open() || !self.pending_tasks.is_open() {
449 self.begin_task_shutdown();
450 self.finish_tasks().await?;
451 self.ib_client = None;
452 self.session_tasks
453 .start_generation()
454 .context("failed to start IB execution session task generation")?;
455 self.pending_tasks
456 .start_generation()
457 .context("failed to start IB execution command task generation")?;
458 }
459 Ok(())
460 }
461
462 async fn teardown_partial_connect(&mut self) -> anyhow::Result<()> {
463 self.begin_task_shutdown();
464 self.ib_client = None;
465 let tasks_result = self.finish_tasks().await;
466 self.is_connected.store(false, Ordering::Release);
467 self.core.set_disconnected();
468 tasks_result
469 }
470}
471
472#[async_trait::async_trait(?Send)]
474impl ExecutionClient for InteractiveBrokersExecutionClient {
475 fn is_connected(&self) -> bool {
476 self.is_connected.load(Ordering::Relaxed)
477 }
478
479 fn client_id(&self) -> ClientId {
480 self.core.client_id
481 }
482
483 fn account_id(&self) -> AccountId {
484 self.core.account_id
485 }
486
487 fn venue(&self) -> Venue {
488 self.core.venue
489 }
490
491 fn handles_order_venue(&self, _venue: Venue) -> bool {
494 true
495 }
496
497 fn oms_type(&self) -> OmsType {
498 self.core.oms_type
499 }
500
501 fn get_account(&self) -> Option<AccountAny> {
502 self.core.cache().account_owned(&self.core.account_id)
503 }
504
505 fn generate_account_state(
506 &self,
507 balances: Vec<AccountBalance>,
508 margins: Vec<MarginBalance>,
509 reported: bool,
510 ts_event: UnixNanos,
511 info: Option<Params>,
512 ) -> anyhow::Result<()> {
513 let factory = OrderEventFactory::new(
514 self.core.trader_id,
515 self.core.account_id,
516 self.core.account_type,
517 self.core.base_currency,
518 );
519 let state = factory.generate_account_state(
520 balances,
521 margins,
522 reported,
523 ts_event,
524 get_atomic_clock_realtime().get_time_ns(),
525 info,
526 );
527 get_exec_event_sender()
528 .send(ExecutionEvent::Account(state))
529 .map_err(|e| anyhow::anyhow!("Failed to send account state: {e}"))
530 }
531
532 fn start(&mut self) -> anyhow::Result<()> {
533 Ok(())
535 }
536
537 fn stop(&mut self) -> anyhow::Result<()> {
538 self.begin_task_shutdown();
539 Ok(())
540 }
541
542 fn submit_order(&self, cmd: SubmitOrder) -> anyhow::Result<()> {
543 if let Err(reason) = self.ensure_client_ready_for_order_request("submit order") {
544 self.deny_submit_order_not_ready(&cmd, &reason)?;
545 return Ok(());
546 }
547
548 let client = self.ib_client.as_ref().context("IB client not connected")?;
549
550 let order_id_map = Arc::clone(&self.order_id_map);
551 let venue_order_id_map = Arc::clone(&self.venue_order_id_map);
552 let instrument_id_map = Arc::clone(&self.instrument_id_map);
553 let trader_id_map = Arc::clone(&self.trader_id_map);
554 let strategy_id_map = Arc::clone(&self.strategy_id_map);
555 let active_order_contexts = Arc::clone(&self.active_order_contexts);
556 let terminal_order_contexts = Arc::clone(&self.terminal_order_contexts);
557 let next_order_id = Arc::clone(&self.next_order_id);
558 let instrument_provider = Arc::clone(&self.instrument_provider);
559 let exec_sender = get_exec_event_sender();
560 let clock = get_atomic_clock_realtime();
561 let order_submit_lock = Arc::clone(&self.order_submit_lock);
562
563 let client_clone = client.as_arc().clone();
564
565 let account_id = self.core.account_id;
566
567 let future = async move {
568 if let Err(e) = Self::handle_submit_order_async(
569 &cmd,
570 &client_clone,
571 &order_id_map,
572 &venue_order_id_map,
573 &instrument_id_map,
574 &trader_id_map,
575 &strategy_id_map,
576 &active_order_contexts,
577 &terminal_order_contexts,
578 &next_order_id,
579 &instrument_provider,
580 &exec_sender,
581 clock,
582 account_id,
583 &order_submit_lock,
584 )
585 .await
586 {
587 tracing::error!("Error submitting order: {e}");
588 }
589 };
590
591 self.pending_tasks
592 .spawn(future)
593 .context("failed to register IB execution command task")?;
594
595 Ok(())
596 }
597
598 async fn connect(&mut self) -> anyhow::Result<()> {
599 if self.is_connected.load(Ordering::Relaxed)
600 && self.session_tasks.is_open()
601 && self.pending_tasks.is_open()
602 {
603 log::debug!("Interactive Brokers execution client already connected");
604 return Ok(());
605 }
606
607 self.prepare_task_groups().await?;
608 let setup_guard =
609 TaskGroupGuard::new(&[&self.session_tasks, &self.pending_tasks], move || {});
610
611 tracing::info!("Connecting Interactive Brokers execution client...");
612 log::debug!(
613 "Execution client config host={} port={} client_id={} account_id={:?} request_timeout={} connection_timeout={} fetch_all_open_orders={} track_option_exercise_from_position_update={}",
614 self.config.host,
615 self.config.port,
616 self.config.client_id,
617 self.config.account_id,
618 self.config.request_timeout,
619 self.config.connection_timeout,
620 self.config.fetch_all_open_orders,
621 self.config.track_option_exercise_from_position_update
622 );
623
624 let handle = crate::common::shared_client::get_or_connect(
625 &self.config.host,
626 self.config.port,
627 self.config.client_id,
628 self.config.connection_timeout,
629 )
630 .await
631 .context("Failed to connect to IB Gateway/TWS")?;
632 let client = Arc::clone(handle.as_arc());
633
634 tracing::info!(
635 "Connected to IB Gateway/TWS at {}:{} (client_id: {})",
636 self.config.host,
637 self.config.port,
638 self.config.client_id
639 );
640
641 log::debug!("Initializing IB execution instrument provider");
643
644 if let Err(e) = self
645 .instrument_provider
646 .initialize_with_client(client.as_ref())
647 .await
648 {
649 if !self.config.instrument_provider.load_ids.is_empty()
650 || !self.config.instrument_provider.load_contracts.is_empty()
651 {
652 return Err(e).context("Failed to load configured IB instruments on startup");
653 }
654
655 tracing::warn!("Failed to load instruments on startup: {}", e);
656 }
657
658 self.ib_client = Some(handle);
659
660 let session_result = async {
661
662 log::debug!("Preloading cached spread instruments for execution client");
663 self.preload_cached_spread_instruments(client.as_ref())
664 .await?;
665
666 log::debug!("Requesting next valid IB order ID");
668 let next_id = self.get_next_order_id().await?;
669 log::debug!("Requesting highest open IB order ID");
670 let highest_open_order_id = self.get_highest_open_order_id(client.as_ref()).await?;
671 let client_scoped_next_id =
672 Self::apply_client_order_id_floor(next_id, self.config.client_id);
673 let starting_order_id = highest_open_order_id
674 .map(|order_id| next_id.max(order_id.saturating_add(1)))
675 .unwrap_or(next_id)
676 .max(client_scoped_next_id);
677
678 if starting_order_id != next_id {
679 tracing::debug!(
680 "Adjusted next Interactive Brokers order ID from {} to {} based on client ID/open orders",
681 next_id,
682 starting_order_id
683 );
684 } else {
685 tracing::debug!(
686 "Initialized next Interactive Brokers order ID to {}",
687 starting_order_id
688 );
689 }
690 {
691 let mut id = self
692 .next_order_id
693 .lock();
694 *id = starting_order_id;
695 }
696
697 log::debug!("Starting IB order update stream");
699 self.start_order_updates().await?;
700
701 let client_for_account = Arc::clone(&client);
704 let account_id = self.core.account_id;
705 let _exec_client_core = self.core.clone(); log::debug!("Subscribing to IB account summary for {}", account_id);
707 match crate::execution::account::subscribe_account_summary(&client_for_account, account_id)
708 .await
709 {
710 Ok((balances, margins, info)) => {
711 tracing::debug!(
712 "Received account summary: {} balances, {} margins",
713 balances.len(),
714 margins.len()
715 );
716 let ts_event = get_atomic_clock_realtime().get_time_ns();
718
719 if let Err(e) = ExecutionClient::generate_account_state(
720 self, balances, margins, true, ts_event, info,
722 ) {
723 tracing::warn!("Failed to generate account state: {}", e);
724 }
725 }
726 Err(e) => {
727 tracing::warn!("Failed to subscribe to account summary: {}", e);
728 }
729 }
730
731 let client_for_positions_init = Arc::clone(&client);
734 let position_tracker_init = Arc::clone(&self.position_tracker);
735
736 log::debug!("Initializing IB execution position tracking");
737 if let Err(e) = crate::execution::account::initialize_position_tracking(
738 &client_for_positions_init,
739 self.core.account_id,
740 position_tracker_init,
741 )
742 .await
743 {
744 tracing::warn!("Failed to initialize position tracking: {}", e);
745 }
746
747 let client_for_pnl = Arc::clone(&client); log::debug!("Subscribing to IB PnL updates");
751
752 if let Err(e) = crate::execution::account::subscribe_pnl(
753 &client_for_pnl,
754 self.core.account_id,
755 &self.session_tasks,
756 )
757 .await
758 {
759 tracing::warn!("Failed to subscribe to PnL: {}", e);
760 }
761
762 if self.config.track_option_exercise_from_position_update {
764 let client_for_positions = Arc::clone(&client);
765 let position_tracker_clone = Arc::clone(&self.position_tracker);
766 let instrument_provider_clone = Arc::clone(&self.instrument_provider);
767
768 log::debug!("Subscribing to IB position updates for option exercise tracking");
769
770 if let Err(e) = crate::execution::account::subscribe_positions(
771 &client_for_positions,
772 self.core.account_id,
773 position_tracker_clone,
774 instrument_provider_clone,
775 &self.session_tasks,
776 )
777 .await
778 {
779 tracing::warn!("Failed to subscribe to positions: {}", e);
780 }
781 }
782
783 Ok::<(), anyhow::Error>(())
784 }
785 .await;
786
787 if let Err(e) = session_result {
788 if let Err(teardown_error) = self.teardown_partial_connect().await {
789 return Err(e.context(format!(
790 "IB execution startup teardown failed: {teardown_error}"
791 )));
792 }
793 return Err(e);
794 }
795
796 self.is_connected.store(true, Ordering::Relaxed);
797 self.core.set_connected();
798 setup_guard.disarm();
799
800 tracing::info!("Connected Interactive Brokers execution client");
801 Ok(())
802 }
803
804 async fn disconnect(&mut self) -> anyhow::Result<()> {
805 if !self.is_connected.load(Ordering::Relaxed)
806 && self.ib_client.is_none()
807 && self.session_tasks.is_open()
808 && self.session_tasks.is_empty()
809 && self.pending_tasks.is_open()
810 && self.pending_tasks.is_empty()
811 {
812 log::debug!("Interactive Brokers execution client already disconnected");
813 return Ok(());
814 }
815
816 tracing::info!("Disconnecting Interactive Brokers execution client...");
817
818 self.teardown_partial_connect().await?;
819
820 tracing::info!("Disconnected Interactive Brokers execution client");
821 Ok(())
822 }
823
824 async fn generate_order_status_report(
825 &self,
826 cmd: &GenerateOrderStatusReport,
827 ) -> anyhow::Result<Option<OrderStatusReport>> {
828 let plural_cmd = GenerateOrderStatusReports {
829 command_id: cmd.command_id,
830 ts_init: cmd.ts_init,
831 open_only: false,
832 instrument_id: cmd.instrument_id,
833 start: None,
834 end: None,
835 params: cmd.params.clone(),
836 log_receipt_level: LogLevel::Info,
837 correlation_id: cmd.correlation_id,
838 causation_id: cmd.causation_id,
839 };
840
841 let reports = self.generate_order_status_reports(&plural_cmd).await?;
842
843 let report = reports.into_iter().find(|r| {
845 let matches_client = if let Some(filter_client_id) = cmd.client_order_id {
846 r.client_order_id == Some(filter_client_id)
847 } else {
848 true
849 };
850 let matches_venue = if let Some(filter_venue_id) = cmd.venue_order_id {
851 r.venue_order_id == filter_venue_id
852 } else {
853 true
854 };
855 matches_client && matches_venue
856 });
857
858 Ok(report)
859 }
860
861 async fn generate_order_status_reports(
862 &self,
863 cmd: &GenerateOrderStatusReports,
864 ) -> anyhow::Result<Vec<OrderStatusReport>> {
865 let client = self.ib_client.as_ref().context("IB client not connected")?;
866
867 let timeout_dur = Duration::from_secs(self.config.request_timeout);
868 let subscription = tokio::time::timeout(timeout_dur, client.all_open_orders())
869 .await
870 .context("Timeout requesting open orders")??;
871 let mut subscription = subscription.filter_data();
872 let mut reports = Vec::new();
873 let mut open_order_fills: AHashMap<InstrumentId, Decimal> = AHashMap::new();
874 let ts_init = get_atomic_clock_realtime().get_time_ns();
875 let raw_account_id = raw_ib_account_code(&self.core.account_id);
876
877 while let Some(order_result) = subscription.next().await {
878 match order_result {
879 Ok(Orders::OrderData(data)) => {
880 if !data.order.account.is_empty() && data.order.account != raw_account_id {
881 continue;
882 }
883
884 let instrument_id =
886 match self.resolve_report_contract_instrument_id(&data.contract) {
887 Ok(instrument_id) => instrument_id,
888 Err(e) => {
889 tracing::warn!(
890 order_id = data.order_id,
891 sec_type = ?data.contract.security_type,
892 symbol = data.contract.symbol.as_str(),
893 con_id = data.contract.contract_id,
894 error = %e,
895 "Failed to resolve IBKR order status report instrument ID",
896 );
897 continue;
898 }
899 };
900
901 if let Some(filter_id) = cmd.instrument_id {
903 if instrument_id != filter_id {
904 continue;
905 }
906 }
907
908 match parse_order_status_to_report(
911 &IBOrderStatus {
912 order_id: data.order_id,
913 status: data.order_state.status,
914 filled: data.order.filled_quantity,
915 remaining: (data.order.total_quantity - data.order.filled_quantity)
916 .max(0.0),
917 average_fill_price: None, perm_id: data.order.perm_id,
919 parent_id: 0, last_fill_price: None, client_id: data.order.client_id,
922 why_held: String::new(), market_cap_price: None, },
925 Some(&data.order),
926 instrument_id,
927 self.core.account_id,
928 &self.instrument_provider,
929 ts_init,
930 ) {
931 Ok(report) => {
932 if !cmd.open_only && report.filled_qty.as_decimal() > Decimal::ZERO {
933 let signed_filled = if report.order_side == Some(OrderSide::Buy) {
934 report.filled_qty.as_decimal()
935 } else {
936 -report.filled_qty.as_decimal()
937 };
938 open_order_fills
939 .entry(report.instrument_id)
940 .and_modify(|qty| *qty += signed_filled)
941 .or_insert(signed_filled);
942 }
943 reports.push(report);
944 }
945 Err(e) => {
946 tracing::warn!("Failed to parse order status report: {e}");
947 }
948 }
949 }
950 Ok(_) => {
951 }
953 Err(e) => {
954 tracing::warn!("Error receiving order data: {e}");
955 }
956 }
957 }
958
959 if !cmd.open_only {
960 let positions = tokio::time::timeout(timeout_dur, client.positions())
961 .await
962 .context("Timeout requesting positions for synthetic order reports")??;
963 let mut positions = positions.filter_data();
964
965 while let Some(position_result) = positions.next().await {
966 match position_result {
967 Ok(PositionUpdate::Position(position)) => {
968 if position.account != raw_account_id {
969 continue;
970 }
971
972 let instrument = match self
973 .instrument_provider
974 .get_instrument(client.as_arc().as_ref(), &position.contract)
975 .await
976 {
977 Ok(Some(instrument)) => instrument,
978 Ok(None) => {
979 tracing::warn!(
980 con_id = position.contract.contract_id,
981 sec_type = ?position.contract.security_type,
982 "Cannot generate synthetic order report: instrument not found",
983 );
984 continue;
985 }
986 Err(e) => {
987 tracing::warn!(
988 con_id = position.contract.contract_id,
989 sec_type = ?position.contract.security_type,
990 error = %e,
991 "Failed to resolve instrument for synthetic order report",
992 );
993 continue;
994 }
995 };
996
997 let instrument_id = instrument.id();
998 if let Some(filter_id) = cmd.instrument_id
999 && instrument_id != filter_id
1000 {
1001 continue;
1002 }
1003
1004 let position_qty =
1005 Decimal::from_f64_retain(position.position).unwrap_or_default();
1006 let open_fills = open_order_fills
1007 .get(&instrument_id)
1008 .copied()
1009 .unwrap_or_default();
1010 let adjusted_qty = position_qty - open_fills;
1011 if adjusted_qty.is_zero() {
1012 continue;
1013 }
1014
1015 let quantity = Quantity::new(
1016 adjusted_qty.abs().to_f64().unwrap_or_default(),
1017 instrument.size_precision(),
1018 );
1019 let order_side = if adjusted_qty > Decimal::ZERO {
1020 OrderSide::Buy
1021 } else {
1022 OrderSide::Sell
1023 };
1024 let id = instrument_id.to_string();
1025 let mut report = OrderStatusReport::new(
1026 self.core.account_id,
1027 instrument_id,
1028 Some(ClientOrderId::new(id.clone())),
1029 VenueOrderId::new(id),
1030 order_side.into(),
1031 OrderType::Market,
1032 TimeInForce::Fok,
1033 OrderStatus::Filled,
1034 quantity,
1035 quantity,
1036 ts_init,
1037 ts_init,
1038 ts_init,
1039 Some(UUID4::new()),
1040 );
1041 report.avg_px = self.position_avg_px_open(
1042 &instrument_id,
1043 &instrument,
1044 position.average_cost,
1045 );
1046 reports.push(report);
1047 }
1048 Ok(PositionUpdate::PositionEnd) => break,
1049 Err(e) => tracing::warn!(
1050 "Error receiving position data for synthetic order report: {e}"
1051 ),
1052 }
1053 }
1054 }
1055
1056 Ok(reports)
1057 }
1058
1059 async fn generate_fill_reports(
1060 &self,
1061 cmd: GenerateFillReports,
1062 ) -> anyhow::Result<Vec<FillReport>> {
1063 let client = self.ib_client.as_ref().context("IB client not connected")?;
1064
1065 let account_code = self.core.account_id.to_string();
1067
1068 let time_filter = if let Some(start) = cmd.start {
1070 let start_dt = start.to_datetime_utc();
1071 start_dt.strftime("%Y%m%d-%H:%M:%S").to_string()
1072 } else {
1073 String::new()
1074 };
1075
1076 let filter = ExecutionFilter {
1077 client_id: None,
1078 account_code,
1079 time: time_filter,
1080 symbol: String::new(),
1081 security_type: String::new(),
1082 exchange: String::new(),
1083 side: None,
1084 last_n_days: 0,
1085 specific_dates: Vec::new(),
1086 };
1087
1088 let timeout_dur = Duration::from_secs(self.config.request_timeout);
1089 let subscription = tokio::time::timeout(timeout_dur, client.executions(filter))
1090 .await
1091 .context("Timeout requesting executions")??;
1092 let mut subscription = subscription.filter_data();
1093 let mut reports = Vec::new();
1094 let ts_init = get_atomic_clock_realtime().get_time_ns();
1095 let mut pending_exec_data: AHashMap<String, ExecutionData> = AHashMap::new();
1096 let mut pending_commissions: AHashMap<String, (f64, String)> = AHashMap::new();
1097
1098 while let Some(exec_result) = subscription.next().await {
1099 match exec_result {
1100 Ok(Executions::ExecutionData(exec_data)) => {
1101 let execution_id = exec_data.execution.execution_id.clone();
1102 if let Some((commission, commission_currency)) =
1103 pending_commissions.remove(&execution_id)
1104 {
1105 if let Some(report) = self.parse_historical_fill_report(
1106 &cmd,
1107 &exec_data,
1108 commission,
1109 &commission_currency,
1110 ts_init,
1111 ) {
1112 reports.push(report);
1113 }
1114 } else {
1115 pending_exec_data.insert(execution_id, exec_data);
1116 }
1117 }
1118 Ok(Executions::CommissionReport(commission)) => {
1119 if let Some(exec_data) = pending_exec_data.remove(&commission.execution_id) {
1120 if let Some(report) = self.parse_historical_fill_report(
1121 &cmd,
1122 &exec_data,
1123 commission.commission,
1124 &commission.currency,
1125 ts_init,
1126 ) {
1127 reports.push(report);
1128 }
1129 } else {
1130 pending_commissions.insert(
1131 commission.execution_id,
1132 (commission.commission, commission.currency),
1133 );
1134 }
1135 }
1136 Err(e) => {
1137 tracing::warn!("Error receiving execution data: {e}");
1138 }
1139 }
1140 }
1141
1142 if !pending_exec_data.is_empty() {
1143 tracing::warn!(
1144 "Skipped {} historical fill reports because IB did not provide matching commission reports",
1145 pending_exec_data.len()
1146 );
1147 }
1148
1149 Ok(reports)
1150 }
1151
1152 async fn generate_position_status_reports(
1153 &self,
1154 cmd: &GeneratePositionStatusReports,
1155 ) -> anyhow::Result<Vec<PositionStatusReport>> {
1156 let client = self.ib_client.as_ref().context("IB client not connected")?;
1157
1158 let timeout_dur = Duration::from_secs(self.config.request_timeout);
1159 let subscription = tokio::time::timeout(timeout_dur, client.positions())
1160 .await
1161 .context("Timeout requesting positions")??;
1162 let mut subscription = subscription.filter_data();
1163 let mut reports = Vec::new();
1164 let ts_init = get_atomic_clock_realtime().get_time_ns();
1165 let raw_account_id = raw_ib_account_code(&self.core.account_id);
1166
1167 while let Some(position_result) = subscription.next().await {
1170 match position_result {
1171 Ok(PositionUpdate::Position(position)) => {
1172 if position.account != raw_account_id {
1174 continue;
1175 }
1176
1177 let instrument = match self
1178 .instrument_provider
1179 .get_instrument(client.as_arc().as_ref(), &position.contract)
1180 .await
1181 {
1182 Ok(Some(instrument)) => instrument,
1183 Ok(None) => {
1184 tracing::warn!(
1185 con_id = position.contract.contract_id,
1186 sec_type = ?position.contract.security_type,
1187 "Cannot generate position status report: instrument not found",
1188 );
1189 continue;
1190 }
1191 Err(e) => {
1192 tracing::warn!(
1193 con_id = position.contract.contract_id,
1194 sec_type = ?position.contract.security_type,
1195 error = %e,
1196 "Failed to resolve position instrument",
1197 );
1198 continue;
1199 }
1200 };
1201 let instrument_id = instrument.id();
1202
1203 if let Some(filter_id) = cmd.instrument_id
1205 && instrument_id != filter_id
1206 {
1207 continue;
1208 }
1209
1210 let position_side = if position.position == 0.0 {
1212 PositionSide::Flat
1213 } else if position.position > 0.0 {
1214 PositionSide::Long
1215 } else {
1216 PositionSide::Short
1217 };
1218
1219 let quantity =
1220 Quantity::new(position.position.abs(), instrument.size_precision());
1221
1222 let avg_px_open = self.position_avg_px_open(
1225 &instrument_id,
1226 &instrument,
1227 position.average_cost,
1228 );
1229
1230 let report = PositionStatusReport::new(
1231 self.core.account_id,
1232 instrument_id,
1233 position_side,
1234 quantity,
1235 ts_init, ts_init, None, None, avg_px_open,
1240 );
1241
1242 reports.push(report);
1243 }
1244 Ok(PositionUpdate::PositionEnd) => {
1245 break;
1247 }
1248 Err(e) => {
1249 tracing::warn!("Error receiving position data: {e}");
1250 }
1251 }
1252 }
1253
1254 if reports.is_empty()
1255 && let Some(instrument_id) = cmd.instrument_id
1256 {
1257 let precision = self
1258 .instrument_provider
1259 .find(&instrument_id)
1260 .map_or(0, |instrument| instrument.size_precision());
1261 reports.push(PositionStatusReport::new(
1262 self.core.account_id,
1263 instrument_id,
1264 PositionSide::Flat,
1265 Quantity::zero(precision),
1266 ts_init,
1267 ts_init,
1268 None,
1269 None,
1270 None,
1271 ));
1272 }
1273
1274 Ok(reports)
1275 }
1276
1277 async fn generate_mass_status(
1278 &self,
1279 lookback_mins: Option<u64>,
1280 ) -> anyhow::Result<Option<ExecutionMassStatus>> {
1281 let ts_now = get_atomic_clock_realtime().get_time_ns();
1282 let start = lookback_mins.map(|mins| {
1283 let lookback_ns = mins * 60 * 1_000_000_000;
1284 UnixNanos::from(ts_now.as_u64().saturating_sub(lookback_ns))
1285 });
1286
1287 let order_cmd = GenerateOrderStatusReportsBuilder::default()
1288 .ts_init(ts_now)
1289 .open_only(false)
1290 .start(start)
1291 .build()
1292 .map_err(|e| anyhow::anyhow!("{e}"))?;
1293
1294 let fill_cmd = GenerateFillReportsBuilder::default()
1295 .ts_init(ts_now)
1296 .start(start)
1297 .build()
1298 .map_err(|e| anyhow::anyhow!("{e}"))?;
1299
1300 let position_cmd = GeneratePositionStatusReportsBuilder::default()
1301 .ts_init(ts_now)
1302 .start(start)
1303 .build()
1304 .map_err(|e| anyhow::anyhow!("{e}"))?;
1305
1306 let (order_reports, fill_reports, position_reports) = tokio::try_join!(
1307 self.generate_order_status_reports(&order_cmd),
1308 self.generate_fill_reports(fill_cmd),
1309 self.generate_position_status_reports(&position_cmd),
1310 )?;
1311
1312 tracing::info!(
1313 "generate_mass_status: {} order reports, {} fill reports, {} position reports",
1314 order_reports.len(),
1315 fill_reports.len(),
1316 position_reports.len()
1317 );
1318
1319 let mut mass_status = ExecutionMassStatus::new(
1320 self.core.client_id,
1321 self.core.account_id,
1322 self.core.venue,
1323 ts_now,
1324 Some(UUID4::new()),
1325 );
1326
1327 mass_status.add_order_reports(order_reports);
1328 mass_status.add_fill_reports(fill_reports);
1329 mass_status.add_position_reports(position_reports);
1330
1331 Ok(Some(mass_status))
1332 }
1333
1334 fn query_account(&self, _cmd: QueryAccount) -> anyhow::Result<()> {
1335 let client = self.ib_client.as_ref().context("IB client not connected")?;
1336
1337 let client_clone = client.as_arc().clone();
1338 let account_id = self.core.account_id;
1339 let account_type = self.core.account_type;
1340 let base_currency = self.core.base_currency;
1341 let clock = get_atomic_clock_realtime();
1342 let request_timeout_secs = self.config.request_timeout;
1343
1344 let future = async move {
1345 let timeout_dur = Duration::from_secs(request_timeout_secs);
1346 let result = tokio::time::timeout(
1347 timeout_dur,
1348 crate::execution::account::subscribe_account_summary(&client_clone, account_id),
1349 )
1350 .await;
1351
1352 match result {
1353 Ok(Ok((balances, margins, info))) => {
1354 let ts_event = clock.get_time_ns();
1355 let ts_now = clock.get_time_ns();
1356
1357 let account_state = AccountState::new(
1358 account_id,
1359 account_type,
1360 balances,
1361 margins,
1362 true,
1363 UUID4::new(),
1364 ts_event,
1365 ts_now,
1366 base_currency,
1367 )
1368 .with_info(info);
1369
1370 let endpoint = MessagingSwitchboard::portfolio_update_account();
1371 send_account_state(endpoint, &account_state);
1372 }
1373 Ok(Err(e)) => {
1374 tracing::error!("Failed to query account state: {e}");
1375 }
1376 Err(_) => {
1377 tracing::error!("Timeout waiting for account summary");
1378 }
1379 }
1380 };
1381
1382 self.pending_tasks
1383 .spawn(future)
1384 .context("failed to register IB execution command task")?;
1385
1386 Ok(())
1387 }
1388
1389 fn query_order(&self, cmd: QueryOrder) -> anyhow::Result<()> {
1390 let client = self.ib_client.as_ref().context("IB client not connected")?;
1391 let client_order_id = cmd.client_order_id;
1392 let trader_id = cmd.trader_id;
1393 let strategy_id = cmd.strategy_id;
1394 let instrument_id = cmd.instrument_id;
1395
1396 let target_order = if let Some(venue_order_id) = &cmd.venue_order_id {
1397 IbOrderSelector::from_venue_order_id(venue_order_id)?
1398 } else {
1399 let map = self.order_id_map.lock();
1400 IbOrderSelector::OrderId(
1401 *map.get(&cmd.client_order_id)
1402 .context("No venue order id for client_order_id")?,
1403 )
1404 };
1405
1406 let client_clone = client.as_arc().clone();
1407 let instrument_id_map = Arc::clone(&self.instrument_id_map);
1408 let instrument_provider = Arc::clone(&self.instrument_provider);
1409 let account_id = self.core.account_id;
1410 let exec_sender = get_exec_event_sender();
1411 let ts_init = get_atomic_clock_realtime().get_time_ns();
1412 let request_timeout_secs = self.config.request_timeout;
1413 let pending_cancel_orders = Arc::clone(&self.pending_cancel_orders);
1414 let raw_account_id = raw_ib_account_code(&self.core.account_id);
1415
1416 let future = async move {
1417 let timeout_dur = Duration::from_secs(request_timeout_secs);
1418 let subscription =
1419 match tokio::time::timeout(timeout_dur, client_clone.all_open_orders()).await {
1420 Ok(Ok(s)) => s,
1421 Ok(Err(e)) => {
1422 tracing::error!("query_order: failed to request open orders: {e}");
1423 return;
1424 }
1425 Err(_) => {
1426 tracing::error!("query_order: timeout requesting open orders");
1427 return;
1428 }
1429 };
1430 let mut subscription = subscription.filter_data();
1431
1432 while let Some(order_result) = subscription.next().await {
1433 if let Ok(Orders::OrderData(data)) = order_result {
1434 if !data.order.account.is_empty() && data.order.account != raw_account_id {
1435 continue;
1436 }
1437
1438 if !target_order.matches(data.order_id, data.order.perm_id) {
1439 continue;
1440 }
1441
1442 let instrument_id = instrument_id_map.lock().get(&data.order_id).copied();
1443 let instrument_id = match instrument_id {
1444 Some(id) => id,
1445 None => match instrument_provider
1446 .resolve_instrument_id_for_contract(&data.contract)
1447 {
1448 Ok(id) => id,
1449 Err(e) => {
1450 tracing::warn!("query_order: failed to convert contract: {e}");
1451 return;
1452 }
1453 },
1454 };
1455
1456 let report = match parse_order_status_to_report(
1457 &IBOrderStatus {
1458 order_id: data.order_id,
1459 status: data.order_state.status,
1460 filled: data.order.filled_quantity,
1461 remaining: (data.order.total_quantity - data.order.filled_quantity)
1462 .max(0.0),
1463 average_fill_price: None,
1464 perm_id: data.order.perm_id,
1465 parent_id: 0,
1466 last_fill_price: None,
1467 client_id: data.order.client_id,
1468 why_held: String::new(),
1469 market_cap_price: None,
1470 },
1471 Some(&data.order),
1472 instrument_id,
1473 account_id,
1474 &instrument_provider,
1475 ts_init,
1476 ) {
1477 Ok(r) => r,
1478 Err(e) => {
1479 tracing::warn!("query_order: failed to parse order status: {e}");
1480 return;
1481 }
1482 };
1483
1484 if exec_sender
1485 .send(ExecutionEvent::Report(ExecutionReport::Order(Box::new(
1486 report,
1487 ))))
1488 .is_err()
1489 {
1490 tracing::error!("query_order: failed to send order status report");
1491 }
1492 return;
1493 }
1494 }
1495
1496 let was_pending_cancel = pending_cancel_orders.lock().remove(&client_order_id);
1497
1498 if was_pending_cancel {
1499 let event = OrderCanceled::new(
1500 trader_id,
1501 strategy_id,
1502 instrument_id,
1503 client_order_id,
1504 UUID4::new(),
1505 ts_init,
1506 ts_init,
1507 false,
1508 Some(target_order.venue_order_id()),
1509 Some(account_id),
1510 );
1511
1512 if exec_sender
1513 .send(ExecutionEvent::Order(OrderEventAny::Canceled(event)))
1514 .is_err()
1515 {
1516 tracing::error!("query_order: failed to send inferred order canceled event");
1517 } else {
1518 tracing::debug!(
1519 "query_order: inferred cancel for {} from missing open order {}",
1520 client_order_id,
1521 target_order.label()
1522 );
1523 }
1524 return;
1525 }
1526
1527 tracing::debug!(
1528 "query_order: order {} not found in open orders (may be filled or canceled)",
1529 target_order.label()
1530 );
1531 };
1532
1533 self.pending_tasks
1534 .spawn(future)
1535 .context("failed to register IB execution command task")?;
1536
1537 Ok(())
1538 }
1539
1540 fn submit_order_list(&self, cmd: SubmitOrderList) -> anyhow::Result<()> {
1541 if let Err(reason) = self.ensure_client_ready_for_order_request("submit order list") {
1542 self.deny_submit_order_list_not_ready(&cmd, &reason)?;
1543 return Ok(());
1544 }
1545
1546 let orders = self.core.get_orders_for_list(&cmd.order_list)?;
1547 self.submit_order_list_with_orders(cmd, orders)
1548 }
1549
1550 fn modify_order(&self, cmd: ModifyOrder) -> anyhow::Result<()> {
1551 if let Err(reason) = self.ensure_client_ready_for_order_request("modify order") {
1552 Self::send_order_modify_rejected(
1553 &cmd,
1554 &reason,
1555 &get_exec_event_sender(),
1556 get_atomic_clock_realtime().get_time_ns(),
1557 self.core.account_id,
1558 )?;
1559 return Ok(());
1560 }
1561
1562 let client = self.ib_client.as_ref().context("IB client not connected")?;
1563
1564 let order_id_map = Arc::clone(&self.order_id_map);
1565 let venue_order_id_map = Arc::clone(&self.venue_order_id_map);
1566 let instrument_id_map = Arc::clone(&self.instrument_id_map);
1567 let instrument_provider = Arc::clone(&self.instrument_provider);
1568 let exec_sender = get_exec_event_sender();
1569 let clock = get_atomic_clock_realtime();
1570 let account_id = self.core.account_id;
1571 let client_clone = client.as_arc().clone();
1572 let request_timeout_secs = self.config.request_timeout;
1573 let original_order = self
1574 .cached_order_for_modify(&cmd.client_order_id)
1575 .map(Arc::new);
1576
1577 if original_order.is_none() {
1578 tracing::debug!(
1579 "Order {} not found in cache for modify; querying IB open orders",
1580 cmd.client_order_id
1581 );
1582 }
1583
1584 let future = async move {
1585 if let Err(e) = Self::handle_modify_order_async(
1586 &cmd,
1587 &client_clone,
1588 &order_id_map,
1589 &venue_order_id_map,
1590 &instrument_id_map,
1591 &instrument_provider,
1592 &exec_sender,
1593 clock,
1594 account_id,
1595 original_order.as_ref(),
1596 request_timeout_secs,
1597 )
1598 .await
1599 {
1600 let reason = format!("Failed to route modify order to IB: {e:#}");
1601
1602 if let Err(send_error) = Self::send_order_modify_rejected(
1603 &cmd,
1604 &reason,
1605 &exec_sender,
1606 clock.get_time_ns(),
1607 account_id,
1608 ) {
1609 tracing::error!("{reason}; failed to emit OrderModifyRejected: {send_error}");
1610 }
1611 }
1612 };
1613
1614 self.pending_tasks
1615 .spawn(future)
1616 .context("failed to register IB execution command task")?;
1617
1618 Ok(())
1619 }
1620
1621 fn cancel_order(&self, cmd: CancelOrder) -> anyhow::Result<()> {
1622 let target_order = Arc::new(self.core.get_order(&cmd.client_order_id)?);
1623 let exec_sender = get_exec_event_sender();
1624 let clock = get_atomic_clock_realtime();
1625 let account_id = self.core.account_id;
1626
1627 if let Err(e) = Self::validate_cancel_order_target(&cmd, &target_order) {
1628 let reason = format!("Failed to resolve cancel order target: {e:#}");
1629 Self::send_order_cancel_rejected(
1630 &target_order,
1631 &reason,
1632 &exec_sender,
1633 clock.get_time_ns(),
1634 account_id,
1635 )?;
1636 return Ok(());
1637 }
1638
1639 if let Err(reason) = self.ensure_client_ready_for_order_request("cancel order") {
1640 Self::send_order_cancel_rejected(
1641 &target_order,
1642 &reason,
1643 &exec_sender,
1644 clock.get_time_ns(),
1645 account_id,
1646 )?;
1647 return Ok(());
1648 }
1649
1650 let client = self.ib_client.as_ref().context("IB client not connected")?;
1651
1652 let order_id_map = Arc::clone(&self.order_id_map);
1653 let venue_order_id_map = Arc::clone(&self.venue_order_id_map);
1654 let instrument_id_map = Arc::clone(&self.instrument_id_map);
1655 let trader_id_map = Arc::clone(&self.trader_id_map);
1656 let strategy_id_map = Arc::clone(&self.strategy_id_map);
1657 let pending_cancel_orders = Arc::clone(&self.pending_cancel_orders);
1658 let client_clone = client.as_arc().clone();
1659 let request_timeout_secs = self.config.request_timeout;
1660
1661 let future = async move {
1662 if let Err(e) = Self::handle_cancel_order_async(
1663 &cmd,
1664 &target_order,
1665 &client_clone,
1666 &order_id_map,
1667 &venue_order_id_map,
1668 &instrument_id_map,
1669 &trader_id_map,
1670 &strategy_id_map,
1671 &pending_cancel_orders,
1672 &exec_sender,
1673 clock.get_time_ns(),
1674 account_id,
1675 request_timeout_secs,
1676 )
1677 .await
1678 {
1679 let reason = format!("Failed to route cancel order to IB: {e:#}");
1680
1681 if let Err(send_error) = Self::send_order_cancel_rejected(
1682 &target_order,
1683 &reason,
1684 &exec_sender,
1685 clock.get_time_ns(),
1686 account_id,
1687 ) {
1688 tracing::error!("{reason}; failed to emit OrderCancelRejected: {send_error}");
1689 }
1690 }
1691 };
1692
1693 self.pending_tasks
1694 .spawn(future)
1695 .context("failed to register IB execution command task")?;
1696
1697 Ok(())
1698 }
1699
1700 fn cancel_all_orders(&self, cmd: CancelAllOrders) -> anyhow::Result<()> {
1701 if cmd.order_side.is_some() {
1703 tracing::warn!(
1704 "Interactive Brokers does not support order_side filtering for cancel all orders; \
1705 ignoring order_side={:?} and canceling all orders",
1706 cmd.order_side
1707 );
1708 }
1709
1710 if self
1713 .ensure_client_ready_for_order_request("cancel orders")
1714 .is_err()
1715 {
1716 return Ok(());
1717 }
1718
1719 let client = self.ib_client.as_ref().context("IB client not connected")?;
1720
1721 let orders_to_cancel: Vec<(ClientOrderId, Option<VenueOrderId>)> = {
1724 let cache = self.core.cache();
1725 let mut orders_to_cancel: Vec<(ClientOrderId, Option<VenueOrderId>)> = cache
1726 .orders_open(
1727 None, Some(&cmd.instrument_id), None, None, None, )
1733 .iter()
1734 .map(|order| (order.client_order_id(), order.venue_order_id()))
1735 .collect();
1736
1737 if orders_to_cancel.is_empty() {
1738 let ib_order_ids: Vec<i32> = {
1739 let instrument_id_map = self.instrument_id_map.lock();
1740 instrument_id_map
1741 .iter()
1742 .filter_map(|(order_id, instrument_id)| {
1743 (*instrument_id == cmd.instrument_id).then_some(*order_id)
1744 })
1745 .collect()
1746 };
1747
1748 let venue_map = self.venue_order_id_map.lock();
1749
1750 orders_to_cancel.extend(ib_order_ids.into_iter().filter_map(|ib_order_id| {
1751 venue_map.get(&ib_order_id).copied().map(|client_order_id| {
1752 (
1753 client_order_id,
1754 Some(VenueOrderId::from(ib_order_id.to_string())),
1755 )
1756 })
1757 }));
1758 }
1759
1760 orders_to_cancel.sort_by_key(|(client_order_id, _)| client_order_id.to_string());
1761 orders_to_cancel.dedup_by_key(|(client_order_id, _)| *client_order_id);
1762 orders_to_cancel
1763 };
1764
1765 if orders_to_cancel.is_empty() {
1766 tracing::debug!("No open orders to cancel");
1767 return Ok(());
1768 }
1769
1770 tracing::debug!(
1771 "Canceling {} open order(s) for instrument {}",
1772 orders_to_cancel.len(),
1773 cmd.instrument_id
1774 );
1775
1776 let client_clone = client.as_arc().clone();
1777 let order_id_map = Arc::clone(&self.order_id_map);
1778 let instrument_id_map = Arc::clone(&self.instrument_id_map);
1779 let trader_id_map = Arc::clone(&self.trader_id_map);
1780 let strategy_id_map = Arc::clone(&self.strategy_id_map);
1781 let pending_cancel_orders = Arc::clone(&self.pending_cancel_orders);
1782 let exec_sender = get_exec_event_sender();
1783 let clock = get_atomic_clock_realtime();
1784 let account_id = self.core.account_id;
1785 let request_timeout_secs = self.config.request_timeout;
1786
1787 let future = async move {
1788 if let Err(e) = Self::handle_cancel_all_orders_async(
1789 &client_clone,
1790 &order_id_map,
1791 &instrument_id_map,
1792 &trader_id_map,
1793 &strategy_id_map,
1794 &pending_cancel_orders,
1795 &exec_sender,
1796 clock.get_time_ns(),
1797 account_id,
1798 request_timeout_secs,
1799 orders_to_cancel,
1800 )
1801 .await
1802 {
1803 tracing::error!("Error canceling all orders: {e}");
1804 }
1805 };
1806
1807 self.pending_tasks
1808 .spawn(future)
1809 .context("failed to register IB execution command task")?;
1810
1811 Ok(())
1812 }
1813
1814 fn batch_cancel_orders(&self, cmd: BatchCancelOrders) -> anyhow::Result<()> {
1815 for cancel_cmd in cmd.cancels {
1817 self.cancel_order(cancel_cmd)?;
1818 }
1819 Ok(())
1820 }
1821}
1822
1823impl InteractiveBrokersExecutionClient {
1824 fn is_ready_for_order_request(&self) -> bool {
1825 if !self.is_connected.load(Ordering::Relaxed) {
1826 return false;
1827 }
1828
1829 if !self
1830 .ib_client
1831 .as_ref()
1832 .is_some_and(|client| client.is_connected())
1833 {
1834 return false;
1835 }
1836
1837 *self.next_order_id.lock() > 0
1838 }
1839
1840 fn ensure_client_ready_for_order_request(&self, request: &str) -> Result<(), String> {
1841 if self.is_ready_for_order_request() {
1842 return Ok(());
1843 }
1844
1845 let reason = format!("Interactive Brokers client is not ready; refusing to {request}");
1846 tracing::warn!("{reason}");
1847 Err(reason)
1848 }
1849
1850 fn deny_submit_order_not_ready(&self, cmd: &SubmitOrder, reason: &str) -> anyhow::Result<()> {
1851 Self::send_order_denied(
1852 cmd.order_init.trader_id,
1853 cmd.strategy_id,
1854 cmd.instrument_id,
1855 cmd.order_init.client_order_id,
1856 reason,
1857 )
1858 }
1859
1860 fn deny_submit_order_list_not_ready(
1861 &self,
1862 cmd: &SubmitOrderList,
1863 reason: &str,
1864 ) -> anyhow::Result<()> {
1865 for order_init in &cmd.order_inits {
1866 Self::send_order_denied(
1867 order_init.trader_id,
1868 cmd.strategy_id,
1869 cmd.instrument_id,
1870 order_init.client_order_id,
1871 reason,
1872 )?;
1873 }
1874
1875 Ok(())
1876 }
1877
1878 fn send_order_denied(
1879 trader_id: TraderId,
1880 strategy_id: StrategyId,
1881 instrument_id: InstrumentId,
1882 client_order_id: ClientOrderId,
1883 reason: &str,
1884 ) -> anyhow::Result<()> {
1885 let ts_event = get_atomic_clock_realtime().get_time_ns();
1886 let event = OrderDenied::new(
1887 trader_id,
1888 strategy_id,
1889 instrument_id,
1890 client_order_id,
1891 Ustr::from(reason),
1892 UUID4::new(),
1893 ts_event,
1894 ts_event,
1895 );
1896
1897 get_exec_event_sender()
1898 .send(ExecutionEvent::Order(OrderEventAny::Denied(event)))
1899 .map_err(|e| anyhow::anyhow!("Failed to send order denied event: {e}"))
1900 }
1901
1902 fn send_order_modify_rejected(
1903 cmd: &ModifyOrder,
1904 reason: &str,
1905 exec_sender: &tokio::sync::mpsc::UnboundedSender<ExecutionEvent>,
1906 ts_event: UnixNanos,
1907 account_id: AccountId,
1908 ) -> anyhow::Result<()> {
1909 let event = OrderModifyRejected::new(
1910 cmd.trader_id,
1911 cmd.strategy_id,
1912 cmd.instrument_id,
1913 cmd.client_order_id,
1914 Ustr::from(reason),
1915 UUID4::new(),
1916 ts_event,
1917 ts_event,
1918 false,
1919 cmd.venue_order_id,
1920 Some(account_id),
1921 );
1922 exec_sender
1923 .send(ExecutionEvent::Order(OrderEventAny::ModifyRejected(event)))
1924 .map_err(|e| anyhow::anyhow!("Failed to send order modify rejected event: {e}"))
1925 }
1926
1927 fn send_order_cancel_rejected(
1928 target_order: &OrderAny,
1929 reason: &str,
1930 exec_sender: &tokio::sync::mpsc::UnboundedSender<ExecutionEvent>,
1931 ts_event: UnixNanos,
1932 account_id: AccountId,
1933 ) -> anyhow::Result<()> {
1934 let event = OrderCancelRejected::new(
1935 target_order.trader_id(),
1936 target_order.strategy_id(),
1937 target_order.instrument_id(),
1938 target_order.client_order_id(),
1939 Ustr::from(reason),
1940 UUID4::new(),
1941 ts_event,
1942 ts_event,
1943 false,
1944 target_order.venue_order_id(),
1945 Some(account_id),
1946 );
1947 exec_sender
1948 .send(ExecutionEvent::Order(OrderEventAny::CancelRejected(event)))
1949 .map_err(|e| anyhow::anyhow!("Failed to send order cancel rejected event: {e}"))
1950 }
1951}
1952
1953#[allow(dead_code)]
1954impl InteractiveBrokersExecutionClient {
1955 fn parse_historical_fill_report(
1956 &self,
1957 cmd: &GenerateFillReports,
1958 exec_data: &ExecutionData,
1959 commission: f64,
1960 commission_currency: &str,
1961 ts_init: UnixNanos,
1962 ) -> Option<FillReport> {
1963 let instrument_id = match self.resolve_historical_execution_instrument_id(exec_data) {
1964 Ok(instrument_id) => instrument_id,
1965 Err(e) => {
1966 Self::warn_historical_fill_report_parse_error(exec_data, &e);
1967 return None;
1968 }
1969 };
1970
1971 if let Some(filter_id) = cmd.instrument_id
1972 && instrument_id != filter_id
1973 {
1974 return None;
1975 }
1976
1977 if let Some(filter_venue_order_id) = cmd.venue_order_id
1978 && ib_venue_order_id(exec_data.execution.order_id, exec_data.execution.perm_id)
1979 != filter_venue_order_id
1980 {
1981 return None;
1982 }
1983
1984 if let Some(end) = cmd.end {
1985 match parse_execution_time(&exec_data.execution.time) {
1986 Ok(ts_event) if ts_event > end => return None,
1987 Ok(_) => {}
1988 Err(e) => {
1989 Self::warn_historical_fill_report_parse_error(exec_data, &e);
1990 return None;
1991 }
1992 }
1993 }
1994
1995 match parse_execution_to_fill_report(
1996 &exec_data.execution,
1997 &exec_data.contract,
1998 commission,
1999 commission_currency,
2000 instrument_id,
2001 self.core.account_id,
2002 &self.instrument_provider,
2003 ts_init,
2004 None, ) {
2006 Ok(report) => Some(report),
2007 Err(e) => {
2008 Self::warn_historical_fill_report_parse_error(exec_data, &e);
2009 None
2010 }
2011 }
2012 }
2013
2014 fn resolve_historical_execution_instrument_id(
2015 &self,
2016 exec_data: &ExecutionData,
2017 ) -> anyhow::Result<InstrumentId> {
2018 self.resolve_report_contract_instrument_id(&exec_data.contract)
2019 }
2020
2021 fn resolve_report_contract_instrument_id(
2022 &self,
2023 contract: &Contract,
2024 ) -> anyhow::Result<InstrumentId> {
2025 match self
2026 .instrument_provider
2027 .resolve_instrument_id_for_contract(contract)
2028 {
2029 Ok(instrument_id) => Ok(instrument_id),
2030 Err(provider_error) if contract.security_type != SecurityType::Spread => {
2031 ib_contract_to_instrument_id_simple(contract).with_context(|| {
2032 format!(
2033 "Failed to resolve IBKR contract to instrument ID using provider ({provider_error}) or simple conversion",
2034 )
2035 })
2036 }
2037 Err(provider_error) => Err(provider_error)
2038 .context("Failed to resolve BAG contract to spread instrument ID"),
2039 }
2040 }
2041
2042 fn position_avg_px_open(
2043 &self,
2044 instrument_id: &InstrumentId,
2045 instrument: &InstrumentAny,
2046 average_cost: f64,
2047 ) -> Option<Decimal> {
2048 if average_cost <= 0.0 {
2049 return None;
2050 }
2051
2052 let price_magnifier = self.instrument_provider.get_price_magnifier(instrument_id) as f64;
2053 let multiplier = instrument.multiplier().as_f64();
2054 let converted_avg_cost = average_cost / (multiplier * price_magnifier);
2055 Decimal::from_f64_retain(converted_avg_cost)
2056 .map(|price| price.round_dp(instrument.price_precision() as u32))
2057 }
2058
2059 fn warn_historical_fill_report_parse_error(exec_data: &ExecutionData, error: &anyhow::Error) {
2060 tracing::warn!(
2061 symbol = exec_data.contract.symbol.as_str(),
2062 sec_type = ?exec_data.contract.security_type,
2063 exchange = exec_data.contract.exchange.as_str(),
2064 primary_exchange = exec_data.contract.primary_exchange.as_str(),
2065 local_symbol = exec_data.contract.local_symbol.as_str(),
2066 con_id = exec_data.contract.contract_id,
2067 order_id = exec_data.execution.order_id,
2068 order_ref = exec_data.execution.order_reference.as_str(),
2069 execution_id = exec_data.execution.execution_id.as_str(),
2070 error = %error,
2071 "Failed to parse IBKR historical fill report",
2072 );
2073 }
2074
2075 fn validate_cancel_order_target(
2076 cmd: &CancelOrder,
2077 target_order: &OrderAny,
2078 ) -> anyhow::Result<()> {
2079 anyhow::ensure!(
2080 cmd.client_order_id == target_order.client_order_id(),
2081 "command client order ID {} does not match cached order {}",
2082 cmd.client_order_id,
2083 target_order.client_order_id()
2084 );
2085 anyhow::ensure!(
2086 cmd.instrument_id == target_order.instrument_id(),
2087 "command instrument ID {} does not match cached order {}",
2088 cmd.instrument_id,
2089 target_order.instrument_id()
2090 );
2091
2092 if let (Some(command_venue_order_id), Some(target_venue_order_id)) =
2094 (cmd.venue_order_id.as_ref(), target_order.venue_order_id())
2095 {
2096 anyhow::ensure!(
2097 command_venue_order_id == &target_venue_order_id,
2098 "command venue order ID {command_venue_order_id} does not match cached order {target_venue_order_id}"
2099 );
2100 }
2101
2102 Ok(())
2103 }
2104
2105 async fn handle_cancel_order_async(
2111 cmd: &CancelOrder,
2112 target_order: &OrderAny,
2113 client: &Arc<Client>,
2114 order_id_map: &Arc<Mutex<AHashMap<ClientOrderId, i32>>>,
2115 venue_order_id_map: &Arc<Mutex<AHashMap<i32, ClientOrderId>>>,
2116 instrument_id_map: &Arc<Mutex<AHashMap<i32, InstrumentId>>>,
2117 trader_id_map: &Arc<Mutex<AHashMap<i32, TraderId>>>,
2118 strategy_id_map: &Arc<Mutex<AHashMap<i32, StrategyId>>>,
2119 pending_cancel_orders: &Arc<Mutex<ahash::AHashSet<ClientOrderId>>>,
2120 exec_sender: &tokio::sync::mpsc::UnboundedSender<ExecutionEvent>,
2121 ts_init: UnixNanos,
2122 account_id: AccountId,
2123 request_timeout_secs: u64,
2124 ) -> anyhow::Result<()> {
2125 let order_selector = if let Some(venue_order_id) = &cmd.venue_order_id {
2126 IbOrderSelector::from_venue_order_id(venue_order_id)?
2127 } else {
2128 let map = order_id_map.lock();
2129 IbOrderSelector::OrderId(
2130 *map.get(&cmd.client_order_id)
2131 .context("No IB order ID mapping found for client order ID")?,
2132 )
2133 };
2134 let ib_order_id =
2135 Self::resolve_ib_order_id(client, order_selector, account_id, request_timeout_secs)
2136 .await?;
2137 Self::cache_cancel_order_tracking(
2138 ib_order_id,
2139 cmd,
2140 target_order,
2141 order_id_map,
2142 venue_order_id_map,
2143 instrument_id_map,
2144 trader_id_map,
2145 strategy_id_map,
2146 )?;
2147
2148 if let Err(e) = client.cancel_order(ib_order_id, "").await {
2149 tracing::error!(
2150 "Cancel outcome is unknown after attempting to send order {} to IB: {e}",
2151 cmd.client_order_id
2152 );
2153 return Ok(());
2154 }
2155
2156 let venue_order_id = target_order
2157 .venue_order_id()
2158 .unwrap_or_else(|| VenueOrderId::from(ib_order_id.to_string()));
2159 if let Err(e) = Self::emit_order_pending_cancel(
2160 ib_order_id,
2161 cmd.client_order_id,
2162 venue_order_id,
2163 instrument_id_map,
2164 trader_id_map,
2165 strategy_id_map,
2166 pending_cancel_orders,
2167 exec_sender,
2168 ts_init,
2169 account_id,
2170 ) {
2171 tracing::error!(
2172 "Cancel request for order {} was sent, but OrderPendingCancel emission failed: {e}",
2173 cmd.client_order_id
2174 );
2175 }
2176
2177 Ok(())
2178 }
2179
2180 async fn resolve_ib_order_id(
2181 client: &Arc<Client>,
2182 order_selector: IbOrderSelector,
2183 account_id: AccountId,
2184 request_timeout_secs: u64,
2185 ) -> anyhow::Result<i32> {
2186 let target_perm_id = match order_selector {
2187 IbOrderSelector::OrderId(order_id) => return Ok(order_id),
2188 IbOrderSelector::PermId(perm_id) => perm_id,
2189 };
2190
2191 let timeout_dur = Duration::from_secs(request_timeout_secs);
2192 let raw_account_id = raw_ib_account_code(&account_id);
2193 let subscription = match tokio::time::timeout(timeout_dur, client.all_open_orders()).await {
2194 Ok(Ok(subscription)) => subscription,
2195 Ok(Err(e)) => anyhow::bail!("Failed to request open orders for perm_id lookup: {e}"),
2196 Err(_) => anyhow::bail!("Timed out requesting open orders for perm_id lookup"),
2197 };
2198 let mut subscription = subscription.filter_data();
2199
2200 while let Some(order_result) = subscription.next().await {
2201 let Orders::OrderData(data) = order_result? else {
2202 continue;
2203 };
2204
2205 if !Self::is_active_open_order(&data.order) {
2206 continue;
2207 }
2208
2209 if !data.order.account.is_empty() && data.order.account != raw_account_id {
2210 continue;
2211 }
2212
2213 if data.order.perm_id != target_perm_id {
2214 continue;
2215 }
2216
2217 if data.order_id == 0 {
2218 anyhow::bail!(
2219 "Cannot resolve PERM-{target_perm_id}: matching open order has no IB order_id"
2220 );
2221 }
2222
2223 return Ok(data.order_id);
2224 }
2225
2226 anyhow::bail!("Cannot resolve PERM-{target_perm_id}: no matching open order found")
2227 }
2228
2229 fn is_active_open_order(order: &ibapi::orders::Order) -> bool {
2230 !order.deactivate
2231 }
2232
2233 fn is_definitive_order_submit_error(error: &ibapi::Error) -> bool {
2234 matches!(
2235 error,
2236 ibapi::Error::InvalidArgument(_) | ibapi::Error::ServerVersion(_, _, _)
2237 )
2238 }
2239
2240 fn classify_order_submit_error(error: &ibapi::Error) -> CommandFailure {
2241 let reason = error.to_string();
2242
2243 if Self::is_definitive_order_submit_error(error) {
2244 CommandFailure::not_sent(reason)
2245 } else if matches!(
2246 error,
2247 ibapi::Error::Notice(notice)
2248 if notice.category() == ibapi::NoticeCategory::OrderRejection
2249 ) {
2250 CommandFailure::venue_rejected(reason)
2251 } else {
2252 CommandFailure::ambiguous(reason)
2253 }
2254 }
2255
2256 async fn handle_cancel_all_orders_async(
2257 client: &Arc<Client>,
2258 order_id_map: &Arc<Mutex<AHashMap<ClientOrderId, i32>>>,
2259 instrument_id_map: &Arc<Mutex<AHashMap<i32, InstrumentId>>>,
2260 trader_id_map: &Arc<Mutex<AHashMap<i32, TraderId>>>,
2261 strategy_id_map: &Arc<Mutex<AHashMap<i32, StrategyId>>>,
2262 pending_cancel_orders: &Arc<Mutex<ahash::AHashSet<ClientOrderId>>>,
2263 exec_sender: &tokio::sync::mpsc::UnboundedSender<ExecutionEvent>,
2264 ts_init: UnixNanos,
2265 account_id: AccountId,
2266 request_timeout_secs: u64,
2267 orders_to_cancel: Vec<(ClientOrderId, Option<VenueOrderId>)>,
2268 ) -> anyhow::Result<()> {
2269 let order_selectors: Vec<(ClientOrderId, IbOrderSelector, Option<VenueOrderId>)> = {
2271 let order_id_map_guard = order_id_map.lock();
2272
2273 orders_to_cancel
2274 .into_iter()
2275 .filter_map(|(client_order_id, venue_order_id)| {
2276 if let Some(venue_order_id) = venue_order_id {
2277 match IbOrderSelector::from_venue_order_id(&venue_order_id) {
2278 Ok(order_selector) => {
2279 return Some((client_order_id, order_selector, Some(venue_order_id)));
2280 }
2281 Err(e) => {
2282 tracing::error!(
2283 "Failed resolve cancel-all order {} from venue order ID {}: {e}",
2284 client_order_id,
2285 venue_order_id
2286 );
2287 return None;
2288 }
2289 }
2290 }
2291
2292 order_id_map_guard
2293 .get(&client_order_id)
2294 .copied()
2295 .map(|ib_order_id| {
2296 (
2297 client_order_id,
2298 IbOrderSelector::OrderId(ib_order_id),
2299 None,
2300 )
2301 })
2302 })
2303 .collect()
2304 };
2305
2306 for (client_order_id, order_selector, venue_order_id) in order_selectors {
2308 let ib_order_id = match Self::resolve_ib_order_id(
2309 client,
2310 order_selector,
2311 account_id,
2312 request_timeout_secs,
2313 )
2314 .await
2315 {
2316 Ok(ib_order_id) => ib_order_id,
2317 Err(e) => {
2318 tracing::error!("Failed resolve cancel-all order {client_order_id}: {e}");
2319 continue;
2320 }
2321 };
2322 let venue_order_id =
2323 venue_order_id.unwrap_or_else(|| VenueOrderId::from(ib_order_id.to_string()));
2324
2325 if let Err(e) = client.cancel_order(ib_order_id, "").await {
2326 tracing::error!(
2327 "Failed to cancel order {} (IB order ID: {}): {e}",
2328 client_order_id,
2329 ib_order_id
2330 );
2331 } else {
2332 if let Err(e) = Self::emit_order_pending_cancel(
2333 ib_order_id,
2334 client_order_id,
2335 venue_order_id,
2336 instrument_id_map,
2337 trader_id_map,
2338 strategy_id_map,
2339 pending_cancel_orders,
2340 exec_sender,
2341 ts_init,
2342 account_id,
2343 ) {
2344 tracing::error!(
2345 "Failed to emit pending cancel for order {} (IB order ID: {}): {e}",
2346 client_order_id,
2347 ib_order_id
2348 );
2349 }
2350 tracing::debug!(
2351 "Canceled order {} (IB order ID: {})",
2352 client_order_id,
2353 ib_order_id
2354 );
2355 }
2356 }
2357
2358 tracing::debug!("Finished canceling all orders");
2359
2360 Ok(())
2361 }
2362
2363 #[allow(clippy::too_many_arguments)]
2364 fn emit_order_pending_cancel(
2365 order_id: i32,
2366 client_order_id: ClientOrderId,
2367 venue_order_id: VenueOrderId,
2368 instrument_id_map: &Arc<Mutex<AHashMap<i32, InstrumentId>>>,
2369 trader_id_map: &Arc<Mutex<AHashMap<i32, TraderId>>>,
2370 strategy_id_map: &Arc<Mutex<AHashMap<i32, StrategyId>>>,
2371 pending_cancel_orders: &Arc<Mutex<ahash::AHashSet<ClientOrderId>>>,
2372 exec_sender: &tokio::sync::mpsc::UnboundedSender<ExecutionEvent>,
2373 ts_init: UnixNanos,
2374 account_id: AccountId,
2375 ) -> anyhow::Result<()> {
2376 let mut pending = pending_cancel_orders.lock();
2377 if !pending.insert(client_order_id) {
2378 return Ok(());
2379 }
2380 drop(pending);
2381
2382 let instrument_id = Self::get_mapped_instrument_id(order_id, instrument_id_map)
2383 .context("Instrument ID not found for pending cancel order")?;
2384 let (trader_id, strategy_id) =
2385 Self::get_required_order_actor_ids(order_id, trader_id_map, strategy_id_map)?;
2386
2387 let event = OrderPendingCancel::new(
2388 trader_id,
2389 strategy_id,
2390 instrument_id,
2391 client_order_id,
2392 Some(account_id),
2393 UUID4::new(),
2394 ts_init,
2395 ts_init,
2396 false,
2397 Some(venue_order_id),
2398 );
2399
2400 exec_sender
2401 .send(ExecutionEvent::Order(OrderEventAny::PendingCancel(event)))
2402 .map_err(|e| anyhow::anyhow!("Failed to send order pending cancel event: {e}"))?;
2403
2404 Ok(())
2405 }
2406}