1#[cfg(test)]
19mod tests;
20
21pub(super) use std::{
22 fmt::Debug,
23 str::FromStr,
24 sync::{
25 Arc,
26 atomic::{AtomicBool, Ordering},
27 },
28 time::Duration,
29};
30
31pub(super) use ahash::{AHashMap, AHashSet};
32pub(super) use anyhow::Context;
33pub(super) use ibapi::{
34 Notice, NoticeCategory,
35 accounts::PositionUpdate,
36 client::Client,
37 contracts::{Contract, Exchange, SecurityType},
38 orders::{
39 ExecutionData, ExecutionFilter, Executions, OrderStatus as IBOrderStatus, OrderStatusKind,
40 OrderUpdate, Orders,
41 },
42 prelude::{StreamExt, SubscriptionItemStreamExt},
43 subscriptions::SubscriptionItem,
44};
45pub(super) use nautilus_common::{
46 cache::{Cache, fifo::FifoCacheMap},
47 clients::{ExecutionClient, ExecutionReportTask},
48 enums::LogLevel,
49 factories::OrderEventFactory,
50 live::{
51 runner::{get_data_event_sender, get_exec_event_sender},
52 sender::EventSender,
53 },
54 messages::{
55 DataEvent, ExecutionEvent,
56 execution::{
57 BatchCancelOrders, CancelAllOrders, CancelOrder, ExecutionReport, GenerateFillReports,
58 GenerateFillReportsBuilder, GenerateOrderStatusReport, GenerateOrderStatusReports,
59 GenerateOrderStatusReportsBuilder, GeneratePositionStatusReports,
60 GeneratePositionStatusReportsBuilder, ModifyOrder, QueryAccount, QueryOrder,
61 SubmitOrder, SubmitOrderList,
62 },
63 },
64};
65pub(super) use nautilus_core::{
66 DurationNanos, Params, UUID4, UnixNanos,
67 time::{AtomicTime, get_atomic_clock_realtime},
68};
69pub(super) use nautilus_live::{
70 ExecutionClientCore,
71 execution::failure::CommandFailure,
72 task::{TaskGroup, TaskGroupGuard, TaskSpawner},
73};
74pub(super) use nautilus_model::{
75 accounts::AccountAny,
76 enums::{
77 LiquiditySide, OmsType, OrderSide, OrderStatus, OrderType, PositionSide, TrailingOffsetType,
78 },
79 events::{
80 OrderAccepted, OrderCancelRejected, OrderCanceled, OrderDenied, OrderDeniedReason,
81 OrderEventAny, OrderFilled, OrderModifyRejected, OrderPendingCancel, OrderRejected,
82 OrderSubmitted, OrderUpdated,
83 },
84 identifiers::{
85 AccountId, ClientId, ClientOrderId, InstrumentId, StrategyId, TradeId, TraderId, Venue,
86 VenueOrderId,
87 },
88 instruments::{Instrument, InstrumentAny},
89 orders::{Order, any::OrderAny},
90 reports::{ExecutionMassStatus, FillReport, OrderStatusReport, PositionStatusReport},
91 types::{AccountBalance, Currency, MarginBalance, Money, Price, Quantity},
92};
93pub(super) use parking_lot::Mutex;
94pub(super) use rust_decimal::Decimal;
95pub(super) use tokio::sync::Mutex as AsyncMutex;
96pub(super) use tokio_util::sync::CancellationToken;
97pub(super) use ustr::Ustr;
98
99pub(super) use super::{
100 account::{
101 AccountSnapshot, PositionTracker, create_position_tracker, ib_account_code,
102 initialize_position_tracking, record_own_fill, stream_account_summary,
103 subscribe_account_summary, subscribe_positions,
104 },
105 parse,
106 parse::{
107 ib_venue_order_id, parse_execution_time, parse_execution_to_fill_report,
108 parse_order_status_to_report,
109 },
110 reports::{IbReportClient, IbReportContext},
111 transform::nautilus_order_to_ib_order,
112};
113use super::{incarnations::OrderIncarnationGroup, parse::parse_order_data_to_report};
114pub(super) use crate::{
115 common::shared_client::{SharedClientHandle, get_or_connect},
116 config::InteractiveBrokersExecutionClientConfig,
117 providers::instruments::InteractiveBrokersInstrumentProvider,
118};
119
120pub(super) const MODIFY_TRAILING_OFFSET_PARAM: &str = "trailing_offset";
121
122pub(super) const DENIAL_CLIENT_NOT_READY: &str = "IB_CLIENT_NOT_READY";
123pub(super) const DENIAL_ORDER_INVALID: &str = "ORDER_INVALID";
124pub(super) const DENIAL_ORDER_LIST_INVALID: &str = "ORDER_LIST_INVALID";
125pub(super) const DENIAL_ORDER_LIST_SIBLING_SUBMIT_FAILED: &str = "ORDER_LIST_SIBLING_SUBMIT_FAILED";
126pub(super) const DENIAL_POST_ONLY_UNSUPPORTED: &str = "UNSUPPORTED_POST_ONLY";
127pub(super) const DENIAL_QUOTE_QUANTITY_UNSUPPORTED: &str = "UNSUPPORTED_QUOTE_QUANTITY";
128pub(super) const DENIAL_TRAILING_OFFSET_TYPE_UNSUPPORTED: &str = "UNSUPPORTED_TRAILING_OFFSET_TYPE";
129const ORDER_ID_PARTITION_SIZE: i32 = 1_000_000;
130
131pub(super) fn coded_denial_reason(code: &str, detail: &str) -> String {
132 format!("{code}: {detail}")
133}
134
135#[cfg_attr(
140 feature = "python",
141 pyo3::pyclass(module = "nautilus_trader.adapters.interactive_brokers", unsendable)
142)]
143pub struct InteractiveBrokersExecutionClient {
144 pub(super) core: ExecutionClientCore,
146 pub(super) config: InteractiveBrokersExecutionClientConfig,
148 pub(super) ib_account: Ustr,
150 pub(super) instrument_provider: Arc<InteractiveBrokersInstrumentProvider>,
152 pub(super) is_connected: Arc<AtomicBool>,
154 pub(super) ib_client: Option<SharedClientHandle>,
156 pub(super) pending_tasks: TaskGroup,
158 pub(super) session_tasks: TaskGroup,
160 pub(super) next_order_id: Arc<Mutex<i32>>,
162 pub(super) order_submit_lock: Arc<AsyncMutex<()>>,
164 pub(super) orders: OrderTracker,
166 pub(super) commission_cache: Arc<Mutex<CommissionCache>>,
168 pub(super) pending_execution_cache: Arc<Mutex<PendingExecutionCache>>,
170 pub(super) position_tracker: PositionTracker,
172}
173
174pub(super) type CommissionCache = FifoCacheMap<String, (f64, String), 10_000>;
175pub(super) type PendingExecutionCache =
176 FifoCacheMap<String, (tokio::time::Instant, ExecutionData), 10_000>;
177
178#[derive(Clone)]
179pub(super) struct OrderTracker {
180 pub(super) state: Arc<Mutex<OrderTrackerState>>,
181}
182
183pub(super) struct OrderTrackerState {
184 pub(super) groups: AHashMap<ClientOrderId, OrderIncarnationGroup>,
185 pub(super) group_history: FifoCacheMap<ClientOrderId, OrderIncarnationGroup, 10_000>,
186 pub(super) incarnation_parents: AHashMap<i64, ClientOrderId>,
187 pub(super) order_snapshots: FifoCacheMap<i64, ibapi::orders::OrderData, 10_000>,
188 pub(super) auxiliary_orders: AHashMap<ClientOrderId, TrackedOrder>,
189 pub(super) client_id: i32,
190 pub(super) order_id_map: AHashMap<ClientOrderId, i32>,
191 pub(super) venue_order_id_map: AHashMap<i32, ClientOrderId>,
192 pub(super) active_orders: AHashMap<i32, TrackedOrder>,
193 pub(super) terminal_orders: FifoCacheMap<i32, TrackedOrder, 10_000>,
194 pub(super) fills_held: AHashMap<IbOrderSelector, Vec<FillReport>>,
197}
198
199impl OrderTrackerState {
200 pub(super) fn order(&self, order_id: i32) -> Option<&TrackedOrder> {
201 self.active_orders
202 .get(&order_id)
203 .or_else(|| self.terminal_orders.get(&order_id))
204 }
205
206 pub(super) fn order_mut(&mut self, order_id: i32) -> Option<&mut TrackedOrder> {
207 if self.active_orders.contains_key(&order_id) {
208 self.active_orders.get_mut(&order_id)
209 } else {
210 self.terminal_orders.get_mut(&order_id)
211 }
212 }
213
214 pub(super) fn order_by_client(&self, client_order_id: ClientOrderId) -> Option<&TrackedOrder> {
215 if let Some(order) = self.auxiliary_orders.get(&client_order_id) {
216 return Some(order);
217 }
218
219 self.order_id_map
220 .get(&client_order_id)
221 .and_then(|order_id| self.order(*order_id))
222 }
223
224 pub(super) fn order_by_client_mut(
225 &mut self,
226 client_order_id: ClientOrderId,
227 ) -> Option<&mut TrackedOrder> {
228 if self.auxiliary_orders.contains_key(&client_order_id) {
229 return self.auxiliary_orders.get_mut(&client_order_id);
230 }
231
232 let order_id = self.order_id_map.get(&client_order_id).copied()?;
233 self.order_mut(order_id)
234 }
235
236 pub(super) fn cancel_selector(
237 &self,
238 client_order_id: ClientOrderId,
239 venue_order_id: Option<&VenueOrderId>,
240 ) -> anyhow::Result<Option<IbOrderSelector>> {
241 if let Some(order) = self.auxiliary_orders.get(&client_order_id)
244 && order.perm_id > 0
245 {
246 return Ok(Some(IbOrderSelector::PermId(order.perm_id)));
247 }
248
249 if let Some(order_id) = self.order_id_map.get(&client_order_id) {
250 return Ok(Some(IbOrderSelector::OrderId(*order_id)));
251 }
252
253 venue_order_id
254 .map(IbOrderSelector::from_venue_order_id)
255 .transpose()
256 }
257}
258
259impl OrderTracker {
260 pub(super) fn new(client_id: i32) -> Self {
261 Self {
262 state: Arc::new(Mutex::new(OrderTrackerState {
263 groups: AHashMap::new(),
264 group_history: FifoCacheMap::new(),
265 incarnation_parents: AHashMap::new(),
266 order_snapshots: FifoCacheMap::new(),
267 auxiliary_orders: AHashMap::new(),
268 client_id,
269 order_id_map: AHashMap::new(),
270 venue_order_id_map: AHashMap::new(),
271 active_orders: AHashMap::new(),
272 terminal_orders: FifoCacheMap::new(),
273 fills_held: AHashMap::new(),
274 })),
275 }
276 }
277
278 #[expect(
279 clippy::unnecessary_wraps,
280 reason = "Retain the fallible tracker interface used throughout execution callbacks"
281 )]
282 pub(super) fn lock(&self) -> anyhow::Result<parking_lot::MutexGuard<'_, OrderTrackerState>> {
283 Ok(self.state.lock())
284 }
285}
286
287#[derive(Clone, Copy)]
288pub(super) struct SpreadFillContext<'a> {
289 pub(super) client_order_id: ClientOrderId,
290 pub(super) spread_instrument_id: InstrumentId,
291 pub(super) commission: f64,
292 pub(super) commission_currency: &'a str,
293 pub(super) ts_init: UnixNanos,
294 pub(super) account_id: AccountId,
295}
296
297#[derive(Debug)]
298pub(super) enum OrderCorrelation {
299 Tracked {
300 order_id: i32,
301 context: TrackedOrder,
302 },
303 Duplicate {
304 order_id: i32,
305 context: TrackedOrder,
306 },
307 Untracked {
308 client_order_id: Option<ClientOrderId>,
309 },
310}
311
312#[derive(Clone, Debug)]
313pub(super) struct TrackedOrder {
314 pub(super) client_order_id: ClientOrderId,
315 pub(super) trader_id: TraderId,
316 pub(super) strategy_id: StrategyId,
317 pub(super) instrument_id: InstrumentId,
318 pub(super) order_side: OrderSide,
319 pub(super) order_type: OrderType,
320 pub(super) accepted: bool,
321 pub(super) avg_px: Option<Price>,
322 pub(super) pending_cancel: bool,
323 pub(super) pending_modify: Option<PendingModifyValues>,
324 pub(super) perm_id: i64,
325 pub(super) spread_fill_ids: ahash::AHashSet<String>,
326 pub(super) spread_fills_held: HeldSpreadFills,
327 pub(super) last_update: Option<(Quantity, Option<Price>, Option<Price>)>,
329}
330
331#[derive(Clone, Debug, Default)]
336pub(super) struct HeldSpreadFills {
337 pub(super) fills: std::collections::VecDeque<(tokio::time::Instant, OrderFilled)>,
338 pub(super) leg_quantities: AHashMap<InstrumentId, f64>,
340}
341
342#[derive(Clone, Copy, Debug, PartialEq)]
345pub(super) struct PendingModifyValues {
346 pub(super) total_quantity: f64,
347 pub(super) limit_price: Option<f64>,
348 pub(super) aux_price: Option<f64>,
349 pub(super) trail_stop_price: Option<f64>,
351}
352
353impl PendingModifyValues {
354 pub(super) fn matches(&self, order: &ibapi::orders::Order) -> bool {
355 const EPS: f64 = 1e-9;
356 let approx = |a: f64, b: f64| (a - b).abs() <= EPS;
357
358 let approx_opt = |a: Option<f64>, b: Option<f64>| match (a, b) {
359 (Some(a), Some(b)) => approx(a, b),
360 (None, None) => true,
361 _ => false,
362 };
363
364 approx(self.total_quantity, order.total_quantity)
365 && approx_opt(self.limit_price, order.limit_price)
366 && approx_opt(self.aux_price, order.aux_price)
367 && self.trail_stop_price.is_none_or(|requested| {
368 order
369 .trail_stop_price
370 .is_some_and(|current| approx(requested, current))
371 })
372 }
373}
374
375#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
376pub(super) enum IbOrderSelector {
377 OrderId(i32),
378 PermId(i64),
379}
380
381impl IbOrderSelector {
382 pub(super) fn from_venue_order_id(venue_order_id: &VenueOrderId) -> anyhow::Result<Self> {
383 let raw = venue_order_id.as_str();
384 if let Some(perm_id) = raw.strip_prefix("PERM-") {
385 return Ok(Self::PermId(perm_id.parse::<i64>().with_context(|| {
386 format!("Failed to parse venue_order_id {raw:?} as IB perm_id")
387 })?));
388 }
389
390 Ok(Self::OrderId(raw.parse::<i32>().with_context(|| {
391 format!("Failed to parse venue_order_id {raw:?} as IB order_id")
392 })?))
393 }
394
395 pub(super) fn matches(self, order_id: i32, perm_id: i64) -> bool {
396 match self {
397 Self::OrderId(target_order_id) => order_id == target_order_id,
398 Self::PermId(target_perm_id) => perm_id == target_perm_id,
399 }
400 }
401
402 pub(super) fn venue_order_id(self) -> VenueOrderId {
403 match self {
404 Self::OrderId(order_id) => VenueOrderId::from(order_id.to_string()),
405 Self::PermId(perm_id) => VenueOrderId::from(format!("PERM-{perm_id}")),
406 }
407 }
408
409 pub(super) fn label(self) -> String {
410 self.venue_order_id().to_string()
411 }
412}
413
414impl Debug for InteractiveBrokersExecutionClient {
415 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
416 f.debug_struct(stringify!(InteractiveBrokersExecutionClient))
417 .field("core", &self.core)
418 .field("config", &self.config)
419 .field("instrument_provider", &self.instrument_provider)
420 .field("is_connected", &self.is_connected.load(Ordering::Relaxed))
421 .field("ib_client", &self.ib_client.is_some())
422 .finish_non_exhaustive()
423 }
424}
425
426impl InteractiveBrokersExecutionClient {
427 pub fn new(
433 core: ExecutionClientCore,
434 config: InteractiveBrokersExecutionClientConfig,
435 instrument_provider: Arc<InteractiveBrokersInstrumentProvider>,
436 ) -> anyhow::Result<Self> {
437 anyhow::ensure!(
438 !config.client_id.unsigned_abs().is_multiple_of(1000),
439 "Interactive Brokers execution client_id must not be a multiple of 1000 because order ID partitioning uses client_id % 1000, was {}",
440 config.client_id
441 );
442
443 let ib_account = ib_account_code(config.account_id.as_deref(), core.account_id);
444 let pending_tasks = TaskGroup::new();
445 let session_tasks = TaskGroup::new();
446
447 let orders = OrderTracker::new(config.client_id);
448 Ok(Self {
449 core,
450 config,
451 ib_account,
452 instrument_provider,
453 is_connected: Arc::new(AtomicBool::new(false)),
454 ib_client: None,
455 pending_tasks,
456 session_tasks,
457 next_order_id: Arc::new(Mutex::new(0)),
458 order_submit_lock: Arc::new(AsyncMutex::new(())),
459 orders,
460 commission_cache: Arc::new(Mutex::new(CommissionCache::new())),
461 pending_execution_cache: Arc::new(Mutex::new(PendingExecutionCache::new())),
462 position_tracker: create_position_tracker(),
463 })
464 }
465
466 pub(super) async fn order_details_with_fills(
467 client: &Client,
468 target: IbOrderSelector,
469 instrument_id: InstrumentId,
470 account_id: AccountId,
471 ib_account: Ustr,
472 provider: &InteractiveBrokersInstrumentProvider,
473 ts_init: UnixNanos,
474 orders: &OrderTracker,
475 ) -> anyhow::Result<Option<(OrderStatusReport, Vec<FillReport>)>> {
476 let mut found = None;
477 let mut found_completed = false;
478
479 for completed in [false, true] {
480 let subscription = if completed {
481 client.completed_orders(false).await?
482 } else {
483 client.all_open_orders().await?
484 };
485
486 let mut subscription = subscription.filter_data();
487 while let Some(item) = subscription.next().await {
488 let Orders::OrderData(data) = item? else {
489 continue;
490 };
491
492 if data.order.account != ib_account
493 || !target.matches(data.order_id, data.order.perm_id)
494 {
495 continue;
496 }
497
498 if let Some(previous) = &found {
499 let previous: &ibapi::orders::OrderData = previous;
500 anyhow::ensure!(
501 previous.order.perm_id == data.order.perm_id,
502 "IB API order ID identifies multiple permanent IDs"
503 );
504 }
505
506 found_completed = completed;
507 found = Some(data);
508 }
509
510 if found.is_some() {
511 break;
512 }
513 }
514
515 let Some(data) = found else {
516 return Ok(None);
517 };
518
519 let resolved = provider.resolve_instrument_id_for_contract(&data.contract)?;
520 anyhow::ensure!(
521 resolved == instrument_id,
522 "IB order query resolved an unexpected instrument {resolved}"
523 );
524 orders
525 .lock()?
526 .observe_order_data(&data, account_id, found_completed)?;
527 let mut report =
528 parse_order_data_to_report(&data, instrument_id, account_id, provider, ts_init)?;
529 let mut subscription = client
530 .executions(Self::execution_filter(ib_account, None))
531 .await?
532 .filter_data();
533 let mut executions = AHashMap::new();
534 let mut commissions = AHashMap::new();
535
536 while let Some(item) = subscription.next().await {
537 match item? {
538 Executions::ExecutionData(data)
539 if data.execution.account_number == ib_account
540 && target.matches(data.execution.order_id, data.execution.perm_id) =>
541 {
542 executions.insert(data.execution.execution_id.clone(), data);
543 }
544 Executions::CommissionReport(commission) => {
545 commissions.insert(
546 commission.execution_id,
547 (commission.commission, commission.currency),
548 );
549 }
550 _ => {}
551 }
552 }
553
554 let mut fills = Vec::with_capacity(executions.len());
555 for (id, data) in executions {
556 let (commission, currency) = commissions
557 .remove(&id)
558 .with_context(|| format!("IB execution {id} has no commission report"))?;
559 fills.push(parse_execution_to_fill_report(
560 &data.execution,
561 &data.contract,
562 commission,
563 ¤cy,
564 instrument_id,
565 account_id,
566 provider,
567 ts_init,
568 None,
569 )?);
570 }
571
572 fills.sort_by_key(|fill| (fill.ts_event, fill.trade_id));
573
574 let filled = fills.iter().try_fold(Decimal::ZERO, |sum, fill| {
575 sum.checked_add(fill.last_qty.as_decimal())
576 .context("IB execution quantity overflow")
577 })?;
578
579 let filled = Quantity::from_decimal_dp(filled, report.quantity.precision)?;
580 report.filled_qty = report.filled_qty.max(filled);
581 if report.quantity.is_positive() && report.filled_qty >= report.quantity {
582 report.order_status = OrderStatus::Filled;
583 }
584
585 Ok(Some((report, fills)))
586 }
587
588 fn submit_order_list_with_orders(
589 &self,
590 cmd: SubmitOrderList,
591 orders: Vec<OrderAny>,
592 ) -> anyhow::Result<()> {
593 let client = self.ib_client.as_ref().context("IB client not connected")?;
594
595 let order_tracker = self.orders.clone();
596 let next_order_id = Arc::clone(&self.next_order_id);
597 let instrument_provider = Arc::clone(&self.instrument_provider);
598 let exec_sender = get_exec_event_sender();
599 let clock = get_atomic_clock_realtime();
600 let account_id = self.core.account_id;
601 let ib_account = self.ib_account;
602 let strategy_id = cmd.strategy_id;
603 let client_clone = client.as_arc().clone();
604 let order_submit_lock = Arc::clone(&self.order_submit_lock);
605
606 let future = async move {
607 if let Err(e) = Self::handle_submit_order_list_async(
608 &cmd,
609 &orders,
610 &client_clone,
611 &order_tracker,
612 &next_order_id,
613 &instrument_provider,
614 &exec_sender,
615 clock,
616 account_id,
617 ib_account,
618 strategy_id,
619 &order_submit_lock,
620 )
621 .await
622 {
623 tracing::error!("Error submitting order list: {e}");
624 }
625 };
626
627 self.pending_tasks
628 .spawn(future)
629 .context("failed to register IB execution command task")?;
630
631 Ok(())
632 }
633
634 pub(super) fn reserve_next_local_order_id(
635 next_order_id: &Arc<Mutex<i32>>,
636 ) -> anyhow::Result<i32> {
637 let mut guard = next_order_id.lock();
638 anyhow::ensure!(
639 *guard > 0,
640 "No valid Interactive Brokers order ID available"
641 );
642 let order_id = *guard;
643 *guard += 1;
644 Ok(order_id)
645 }
646
647 fn apply_client_order_id_floor(next_id: i32, client_id: i32) -> anyhow::Result<i32> {
650 let client_slot = client_id.unsigned_abs() % 1000;
651 if client_slot == 0 {
652 return Ok(next_id);
653 }
654
655 let order_id_floor = (client_slot as i32) * ORDER_ID_PARTITION_SIZE;
656 let order_id_ceiling = order_id_floor + ORDER_ID_PARTITION_SIZE;
657 anyhow::ensure!(
658 next_id < order_id_ceiling,
659 "IB next valid order ID {next_id} for client_id {client_id} is above its partition ({} to {}), so IB would refuse every order with error 103; use another client_id",
660 order_id_floor + 1,
661 order_id_ceiling - 1,
662 );
663
664 if next_id > order_id_floor {
665 Ok(next_id)
666 } else {
667 Ok(order_id_floor + next_id.rem_euclid(ORDER_ID_PARTITION_SIZE).max(1))
668 }
669 }
670
671 fn is_order_id_in_client_partition(order_id: i32, client_id: i32) -> bool {
672 let client_slot = client_id.unsigned_abs() % 1000;
673 if client_slot == 0 {
674 return true;
675 }
676
677 let order_id_floor = (client_slot as i32) * ORDER_ID_PARTITION_SIZE;
678 order_id > order_id_floor && order_id < order_id_floor + ORDER_ID_PARTITION_SIZE
679 }
680
681 fn initial_order_id(
685 next_id: i32,
686 client_id: i32,
687 highest_used_order_id: Option<i32>,
688 ) -> anyhow::Result<i32> {
689 let client_scoped_next_id = Self::apply_client_order_id_floor(next_id, client_id)?;
690 Ok(
691 highest_used_order_id.map_or(client_scoped_next_id, |order_id| {
692 client_scoped_next_id.max(order_id.saturating_add(1))
693 }),
694 )
695 }
696
697 async fn get_next_order_id(&self) -> anyhow::Result<i32> {
703 let client = self.ib_client.as_ref().context("IB client not connected")?;
704
705 let timeout_dur = Duration::from_secs(self.config.request_timeout);
706 let order_id = tokio::time::timeout(timeout_dur, client.next_valid_order_id())
707 .await
708 .context("Timeout getting next order ID")??;
709 Ok(order_id)
710 }
711
712 async fn get_highest_open_order_id(&self, client: &Client) -> anyhow::Result<Option<i32>> {
713 let timeout_dur = Duration::from_secs(self.config.request_timeout);
714 let subscription = tokio::time::timeout(timeout_dur, client.all_open_orders())
715 .await
716 .context("Timeout requesting open orders for next order ID initialization")??;
717 let mut subscription = subscription.filter_data();
718 let mut highest_order_id = None;
719 let mut snapshots = Vec::new();
720
721 while let Some(order_result) = subscription.next().await {
722 match order_result {
723 Ok(Orders::OrderData(data)) => {
724 if data.order.account == self.ib_account {
725 snapshots.push(data.clone());
726 }
727
728 if data.order.client_id != self.config.client_id
729 || !Self::is_order_id_in_client_partition(
730 data.order_id,
731 self.config.client_id,
732 )
733 {
734 continue;
735 }
736
737 highest_order_id = Some(
738 highest_order_id
739 .map_or(data.order_id, |current: i32| current.max(data.order_id)),
740 );
741 }
742 Ok(_) => {}
743 Err(e) => {
744 tracing::debug!(
745 "Ignoring open-order event while initializing next order ID: {e}"
746 );
747 }
748 }
749 }
750
751 {
752 let cache = self.core.cache();
753 let mut state = self.orders.lock()?;
754
755 for data in &snapshots {
756 let venue_id = parse::ib_venue_order_id(data.order_id, data.order.perm_id);
757
758 let Some(id) = cache.client_order_id(&venue_id) else {
759 continue;
760 };
761
762 let Some(order) = cache.order(id) else {
763 continue;
764 };
765
766 if cache.client_id(id) != Some(&self.core.client_id)
767 || order.account_id() != Some(self.core.account_id)
768 || data.order.client_id != self.config.client_id
769 {
770 continue;
771 }
772
773 if let Some(existing) = state.venue_order_id_map.get(&data.order_id) {
774 if *existing != *id {
775 continue;
776 }
777 }
778
779 state.order_id_map.insert(*id, data.order_id);
780 state.venue_order_id_map.insert(data.order_id, *id);
781 state
782 .active_orders
783 .entry(data.order_id)
784 .or_insert_with(|| TrackedOrder {
785 client_order_id: *id,
786 trader_id: order.trader_id(),
787 strategy_id: order.strategy_id(),
788 instrument_id: order.instrument_id(),
789 order_side: order.order_side(),
790 order_type: order.order_type(),
791 accepted: true,
792 avg_px: None,
793 pending_cancel: false,
794 pending_modify: None,
795 perm_id: data.order.perm_id,
796 spread_fill_ids: AHashSet::new(),
797 spread_fills_held: HeldSpreadFills::default(),
798 last_update: Some((order.quantity(), order.price(), order.trigger_price())),
799 });
800 }
801
802 for data in &snapshots {
803 state.observe_order_data(data, self.core.account_id, false)?;
804 }
805 }
806
807 Ok(highest_order_id)
808 }
809
810 async fn get_highest_completed_order_id(&self, client: &Client) -> anyhow::Result<Option<i32>> {
811 let deadline =
812 tokio::time::Instant::now() + Duration::from_secs(self.config.request_timeout);
813 let subscription = tokio::time::timeout_at(deadline, client.completed_orders(false))
814 .await
815 .context("Timeout requesting completed orders for next order ID initialization")??;
816
817 Self::highest_completed_order_id(subscription, self.config.client_id, deadline).await
818 }
819
820 pub(super) async fn highest_completed_order_id<S>(
825 mut subscription: S,
826 client_id: i32,
827 deadline: tokio::time::Instant,
828 ) -> anyhow::Result<Option<i32>>
829 where
830 S: futures_util::Stream<Item = Result<SubscriptionItem<Orders>, ibapi::Error>> + Unpin,
831 {
832 let read_snapshot = async {
833 let mut highest_order_id = None;
834
835 while let Some(item) = subscription.next().await {
836 let data = match item? {
837 SubscriptionItem::Data(Orders::OrderData(data)) => data,
838 SubscriptionItem::Data(_) => continue,
839 SubscriptionItem::Notice(notice)
840 if notice.code == ibapi::SUBSCRIPTION_LAG_CODE =>
841 {
842 anyhow::bail!("Completed orders snapshot dropped records: {notice}");
843 }
844 SubscriptionItem::Notice(notice) => {
845 tracing::warn!("IB notice on completed orders snapshot: {notice}");
846 continue;
847 }
848 };
849
850 let order_id = data.order.order_id;
851 if data.order.client_id == client_id
852 && Self::is_order_id_in_client_partition(order_id, client_id)
853 {
854 highest_order_id = highest_order_id.max(Some(order_id));
855 }
856 }
857
858 Ok(highest_order_id)
859 };
860
861 tokio::time::timeout_at(deadline, read_snapshot)
862 .await
863 .context("Timeout reading completed orders for next order ID initialization")?
864 .context("Failed to read completed orders for next order ID initialization")
865 }
866
867 fn begin_task_shutdown(&self) {
868 self.pending_tasks.begin_shutdown();
869 self.session_tasks.begin_shutdown();
870 self.is_connected.store(false, Ordering::Release);
871 self.core.set_disconnected();
872 }
873
874 async fn finish_tasks(&self) -> anyhow::Result<()> {
875 let (session_result, pending_result) = tokio::join!(
876 self.session_tasks
877 .finish_shutdown(Duration::from_secs(1), Duration::from_secs(2)),
878 self.pending_tasks
879 .finish_shutdown(Duration::from_secs(2), Duration::from_secs(2)),
880 );
881 session_result.context("failed to finish IB execution session tasks")?;
882 pending_result.context("failed to finish IB execution command tasks")?;
883 Ok(())
884 }
885
886 async fn prepare_task_groups(&mut self) -> anyhow::Result<()> {
887 if !self.session_tasks.is_open() || !self.pending_tasks.is_open() {
888 self.begin_task_shutdown();
889 self.finish_tasks().await?;
890 self.ib_client = None;
891 self.session_tasks
892 .start_generation()
893 .context("failed to start IB execution session task generation")?;
894 self.pending_tasks
895 .start_generation()
896 .context("failed to start IB execution command task generation")?;
897 }
898
899 Ok(())
900 }
901
902 async fn teardown_partial_connect(&mut self) -> anyhow::Result<()> {
903 self.begin_task_shutdown();
904 self.ib_client = None;
905 let tasks_result = self.finish_tasks().await;
906 self.is_connected.store(false, Ordering::Release);
907 self.core.set_disconnected();
908 tasks_result
909 }
910}
911
912#[async_trait::async_trait(?Send)]
914impl ExecutionClient for InteractiveBrokersExecutionClient {
915 fn is_connected(&self) -> bool {
916 self.is_connected.load(Ordering::Relaxed)
917 }
918
919 fn client_id(&self) -> ClientId {
920 self.core.client_id
921 }
922
923 fn account_id(&self) -> AccountId {
924 self.core.account_id
925 }
926
927 fn venue(&self) -> Venue {
928 self.core.venue
929 }
930
931 fn handles_order_venue(&self, _venue: Venue) -> bool {
934 true
935 }
936
937 fn oms_type(&self) -> OmsType {
938 self.core.oms_type
939 }
940
941 fn get_account(&self) -> Option<AccountAny> {
942 self.core.cache().account_owned(&self.core.account_id)
943 }
944
945 fn generate_account_state(
946 &self,
947 balances: Vec<AccountBalance>,
948 margins: Vec<MarginBalance>,
949 reported: bool,
950 ts_event: UnixNanos,
951 info: Option<Params>,
952 ) -> anyhow::Result<()> {
953 let factory = OrderEventFactory::new(
954 self.core.trader_id,
955 self.core.account_id,
956 self.core.account_type,
957 self.core.base_currency,
958 );
959 let state = factory.generate_account_state(
960 balances,
961 margins,
962 reported,
963 ts_event,
964 get_atomic_clock_realtime().get_time_ns(),
965 info,
966 );
967 get_exec_event_sender()
968 .send(ExecutionEvent::Account(state))
969 .map_err(|e| anyhow::anyhow!("Failed to send account state: {e}"))
970 }
971
972 fn start(&mut self) -> anyhow::Result<()> {
973 Ok(())
975 }
976
977 fn stop(&mut self) -> anyhow::Result<()> {
978 self.begin_task_shutdown();
979 Ok(())
980 }
981
982 fn submit_order(&self, cmd: SubmitOrder) -> anyhow::Result<()> {
983 let order = self.core.get_order(&cmd.client_order_id)?;
984 if let Err(reason) = validate_order(&order) {
985 let reason = reason.to_string();
986 Self::send_order_denied(
987 cmd.order_init.trader_id,
988 cmd.strategy_id,
989 cmd.instrument_id,
990 cmd.order_init.client_order_id,
991 &reason,
992 )?;
993 return Ok(());
994 }
995
996 if let Err(reason) = self.ensure_client_ready_for_order_request("submit order") {
997 self.deny_submit_order_not_ready(&cmd, &reason)?;
998 return Ok(());
999 }
1000
1001 let client = self.ib_client.as_ref().context("IB client not connected")?;
1002
1003 let orders = self.orders.clone();
1004 let next_order_id = Arc::clone(&self.next_order_id);
1005 let instrument_provider = Arc::clone(&self.instrument_provider);
1006 let exec_sender = get_exec_event_sender();
1007 let clock = get_atomic_clock_realtime();
1008 let order_submit_lock = Arc::clone(&self.order_submit_lock);
1009
1010 let client_clone = client.as_arc().clone();
1011
1012 let account_id = self.core.account_id;
1013 let ib_account = self.ib_account;
1014
1015 let future = async move {
1016 if let Err(e) = Self::handle_submit_order_async(
1017 &cmd,
1018 &client_clone,
1019 &orders,
1020 &next_order_id,
1021 &instrument_provider,
1022 &exec_sender,
1023 clock,
1024 account_id,
1025 ib_account,
1026 &order_submit_lock,
1027 )
1028 .await
1029 {
1030 tracing::error!("Error submitting order: {e}");
1031 }
1032 };
1033
1034 self.pending_tasks
1035 .spawn(future)
1036 .context("failed to register IB execution command task")?;
1037
1038 Ok(())
1039 }
1040
1041 async fn connect(&mut self) -> anyhow::Result<()> {
1042 if self.is_connected.load(Ordering::Relaxed)
1043 && self.session_tasks.is_open()
1044 && self.pending_tasks.is_open()
1045 {
1046 log::debug!("Interactive Brokers execution client already connected");
1047 return Ok(());
1048 }
1049
1050 self.prepare_task_groups().await?;
1051 let setup_guard =
1052 TaskGroupGuard::new(&[&self.session_tasks, &self.pending_tasks], move || {});
1053
1054 tracing::info!("Connecting Interactive Brokers execution client...");
1055
1056 log::debug!(
1057 "Execution client config host={} port={} client_id={} account_id={:?} request_timeout={} connection_timeout={} fetch_all_open_orders={} track_option_exercise_from_position_update={}",
1058 self.config.host,
1059 self.config.port,
1060 self.config.client_id,
1061 self.config.account_id,
1062 self.config.request_timeout,
1063 self.config.connection_timeout,
1064 self.config.fetch_all_open_orders,
1065 self.config.track_option_exercise_from_position_update
1066 );
1067
1068 let handle = get_or_connect(
1069 &self.config.host,
1070 self.config.port,
1071 self.config.client_id,
1072 self.config.connection_timeout,
1073 )
1074 .await
1075 .context("Failed to connect to IB Gateway/TWS")?;
1076 let client = Arc::clone(handle.as_arc());
1077
1078 tracing::info!(
1079 "Connected to IB Gateway/TWS at {}:{} (client_id: {})",
1080 self.config.host,
1081 self.config.port,
1082 self.config.client_id
1083 );
1084
1085 log::debug!("Initializing IB execution instrument provider");
1087
1088 if let Err(e) = self
1089 .instrument_provider
1090 .initialize_with_client(client.as_ref())
1091 .await
1092 {
1093 if !self.config.instrument_provider.load_ids.is_empty()
1094 || !self.config.instrument_provider.load_contracts.is_empty()
1095 {
1096 return Err(e).context("Failed to load configured IB instruments on startup");
1097 }
1098
1099 tracing::warn!("Failed to load instruments on startup: {}", e);
1100 }
1101
1102 self.ib_client = Some(handle);
1103
1104 let session_result = async {
1105
1106 log::debug!("Preloading cached instruments for execution client");
1107 self.preload_cached_instruments(client.as_ref()).await;
1108
1109 log::debug!("Requesting next valid IB order ID");
1111 let next_id = self.get_next_order_id().await?;
1112 log::debug!("Requesting highest open IB order ID");
1113 let highest_open_order_id = self.get_highest_open_order_id(client.as_ref()).await?;
1114 log::debug!("Requesting highest completed IB order ID");
1115 let highest_completed_order_id =
1116 self.get_highest_completed_order_id(client.as_ref()).await?;
1117
1118 let starting_order_id = Self::initial_order_id(
1119 next_id,
1120 self.config.client_id,
1121 highest_open_order_id.max(highest_completed_order_id),
1122 )?;
1123
1124 if starting_order_id == next_id {
1125 tracing::debug!(
1126 "Initialized next Interactive Brokers order ID to {}",
1127 starting_order_id
1128 );
1129 } else {
1130 tracing::debug!(
1131 "Adjusted next Interactive Brokers order ID from {} to {} based on client ID/open and completed orders",
1132 next_id,
1133 starting_order_id
1134 );
1135 }
1136
1137 {
1138 let mut id = self
1139 .next_order_id
1140 .lock();
1141 *id = starting_order_id;
1142 }
1143
1144 log::debug!("Starting IB order update stream");
1146 self.start_order_updates().await?;
1147
1148 let account_id = self.core.account_id;
1151 log::debug!("Subscribing to IB account summary for {account_id}");
1152
1153 let send_account_state = self.account_state_sender();
1154
1155 if let Err(e) = stream_account_summary(
1156 &client,
1157 self.ib_account,
1158 &self.session_tasks,
1159 send_account_state,
1160 )
1161 .await
1162 {
1163 tracing::warn!("Failed to subscribe to account summary: {}", e);
1164 }
1165
1166 let client_for_positions_init = Arc::clone(&client);
1169 let position_tracker_init = Arc::clone(&self.position_tracker);
1170
1171 log::debug!("Initializing IB execution position tracking");
1172
1173 match initialize_position_tracking(
1174 &client_for_positions_init,
1175 self.ib_account,
1176 position_tracker_init,
1177 )
1178 .await
1179 {
1180 Ok(contracts) => {
1181 self.publish_position_instruments(client.as_ref(), contracts)
1182 .await;
1183 }
1184 Err(e) => tracing::warn!("Failed to initialize position tracking: {}", e),
1185 }
1186
1187 self.publish_combo_order_instruments(client.as_ref()).await;
1188
1189 let client_for_pnl = Arc::clone(&client); log::debug!("Subscribing to IB PnL updates");
1193
1194 if let Err(e) = crate::execution::account::subscribe_pnl(
1195 &client_for_pnl,
1196 self.ib_account,
1197 &self.session_tasks,
1198 )
1199 .await
1200 {
1201 tracing::warn!("Failed to subscribe to PnL: {}", e);
1202 }
1203
1204 if self.config.track_option_exercise_from_position_update {
1206 let client_for_positions = Arc::clone(&client);
1207 let position_tracker_clone = Arc::clone(&self.position_tracker);
1208 let instrument_provider_clone = Arc::clone(&self.instrument_provider);
1209
1210 log::debug!("Subscribing to IB position updates for option exercise tracking");
1211
1212 if let Err(e) = subscribe_positions(
1213 &client_for_positions,
1214 self.core.account_id,
1215 self.ib_account,
1216 position_tracker_clone,
1217 instrument_provider_clone,
1218 &self.session_tasks,
1219 )
1220 .await
1221 {
1222 tracing::warn!("Failed to subscribe to positions: {}", e);
1223 }
1224 }
1225
1226 Ok::<(), anyhow::Error>(())
1227 }
1228 .await;
1229
1230 if let Err(e) = session_result {
1231 if let Err(teardown_error) = self.teardown_partial_connect().await {
1232 return Err(e.context(format!(
1233 "IB execution startup teardown failed: {teardown_error}"
1234 )));
1235 }
1236
1237 return Err(e);
1238 }
1239
1240 self.is_connected.store(true, Ordering::Relaxed);
1241 self.core.set_connected();
1242 setup_guard.disarm();
1243
1244 tracing::info!("Connected Interactive Brokers execution client");
1245 Ok(())
1246 }
1247
1248 async fn disconnect(&mut self) -> anyhow::Result<()> {
1249 if !self.is_connected.load(Ordering::Relaxed)
1250 && self.ib_client.is_none()
1251 && self.session_tasks.is_open()
1252 && self.session_tasks.is_empty()
1253 && self.pending_tasks.is_open()
1254 && self.pending_tasks.is_empty()
1255 {
1256 log::debug!("Interactive Brokers execution client already disconnected");
1257 return Ok(());
1258 }
1259
1260 tracing::info!("Disconnecting Interactive Brokers execution client...");
1261
1262 self.teardown_partial_connect().await?;
1263
1264 tracing::info!("Disconnected Interactive Brokers execution client");
1265 Ok(())
1266 }
1267
1268 fn generate_order_status_report_task(
1269 &self,
1270 cmd: &GenerateOrderStatusReport,
1271 ) -> Option<ExecutionReportTask<Option<OrderStatusReport>>> {
1272 let report_client = self.report_client().ok()?;
1273 let command = cmd.clone();
1274 Some(ExecutionReportTask::new(
1275 async move { report_client.generate_order_status_report(&command).await },
1276 Ok,
1277 ))
1278 }
1279
1280 async fn generate_order_status_report(
1281 &self,
1282 cmd: &GenerateOrderStatusReport,
1283 ) -> anyhow::Result<Option<OrderStatusReport>> {
1284 self.report_client()?
1285 .generate_order_status_report(cmd)
1286 .await
1287 }
1288
1289 fn generate_order_status_reports_task(
1290 &self,
1291 cmd: &GenerateOrderStatusReports,
1292 ) -> Option<ExecutionReportTask<Vec<OrderStatusReport>>> {
1293 let report_client = self.report_client().ok()?;
1294 let command = cmd.clone();
1295 Some(ExecutionReportTask::new(
1296 async move { report_client.generate_order_status_reports(&command).await },
1297 Ok,
1298 ))
1299 }
1300
1301 async fn generate_order_status_reports(
1302 &self,
1303 cmd: &GenerateOrderStatusReports,
1304 ) -> anyhow::Result<Vec<OrderStatusReport>> {
1305 self.report_client()?
1306 .generate_order_status_reports(cmd)
1307 .await
1308 }
1309
1310 fn generate_fill_reports_task(
1311 &self,
1312 cmd: &GenerateFillReports,
1313 ) -> Option<ExecutionReportTask<Vec<FillReport>>> {
1314 let report_client = self.report_client().ok()?;
1315 let command = cmd.clone();
1316 Some(ExecutionReportTask::new(
1317 async move { report_client.generate_fill_reports(command).await },
1318 Ok,
1319 ))
1320 }
1321
1322 async fn generate_fill_reports(
1323 &self,
1324 cmd: GenerateFillReports,
1325 ) -> anyhow::Result<Vec<FillReport>> {
1326 self.report_client()?.generate_fill_reports(cmd).await
1327 }
1328
1329 fn generate_position_status_reports_task(
1330 &self,
1331 cmd: &GeneratePositionStatusReports,
1332 ) -> Option<ExecutionReportTask<Vec<PositionStatusReport>>> {
1333 let report_client = self.report_client().ok()?;
1334 let command = cmd.clone();
1335
1336 Some(ExecutionReportTask::new(
1337 async move {
1338 report_client
1339 .generate_position_status_reports(&command)
1340 .await
1341 },
1342 Ok,
1343 ))
1344 }
1345
1346 async fn generate_position_status_reports(
1347 &self,
1348 cmd: &GeneratePositionStatusReports,
1349 ) -> anyhow::Result<Vec<PositionStatusReport>> {
1350 self.report_client()?
1351 .generate_position_status_reports(cmd)
1352 .await
1353 }
1354
1355 async fn generate_mass_status(
1356 &self,
1357 lookback_mins: Option<u64>,
1358 ) -> anyhow::Result<Option<ExecutionMassStatus>> {
1359 let ts_now = get_atomic_clock_realtime().get_time_ns();
1360 let start = lookback_mins
1361 .map(DurationNanos::try_from_mins)
1362 .transpose()?
1363 .map(|lookback| ts_now.saturating_sub(lookback));
1364
1365 let order_cmd = GenerateOrderStatusReportsBuilder::default()
1366 .ts_init(ts_now)
1367 .open_only(false)
1368 .start(start)
1369 .build()
1370 .map_err(|e| anyhow::anyhow!("{e}"))?;
1371
1372 let fill_cmd = GenerateFillReportsBuilder::default()
1373 .ts_init(ts_now)
1374 .start(start)
1375 .build()
1376 .map_err(|e| anyhow::anyhow!("{e}"))?;
1377
1378 let position_cmd = GeneratePositionStatusReportsBuilder::default()
1379 .ts_init(ts_now)
1380 .start(start)
1381 .build()
1382 .map_err(|e| anyhow::anyhow!("{e}"))?;
1383
1384 let report_client = self.report_client()?;
1385 let (mut order_reports, fill_evidence, mut position_reports) = tokio::try_join!(
1386 self.generate_order_status_reports(&order_cmd),
1387 report_client.generate_fill_evidence(fill_cmd),
1388 self.generate_position_status_reports(&position_cmd),
1389 )?;
1390
1391 tracing::info!(
1392 "generate_mass_status: {} order reports, {} fill reports, {} position reports",
1393 order_reports.len(),
1394 fill_evidence.reports.len(),
1395 position_reports.len()
1396 );
1397
1398 let mut mass_status = ExecutionMassStatus::new(
1399 self.core.client_id,
1400 self.core.account_id,
1401 self.core.venue,
1402 ts_now,
1403 Some(UUID4::new()),
1404 );
1405 mass_status.set_report_window(start, start.is_some());
1409
1410 parse::fill_missing_avg_px(&mut order_reports, &fill_evidence.reports);
1411 parse::fill_position_avg_px_from_fills(
1412 &mut position_reports,
1413 &fill_evidence,
1414 &self.instrument_provider,
1415 );
1416 let fill_reports = fill_evidence.reports;
1417 mass_status.add_order_reports(order_reports);
1418 mass_status.add_fill_reports(fill_reports);
1419 mass_status.add_position_reports(position_reports);
1420
1421 Ok(Some(mass_status))
1422 }
1423
1424 fn query_account(&self, _cmd: QueryAccount) -> anyhow::Result<()> {
1425 let client = self.ib_client.as_ref().context("IB client not connected")?;
1426
1427 let client_clone = client.as_arc().clone();
1428 let ib_account = self.ib_account;
1429 let request_timeout_secs = self.config.request_timeout;
1430 let send_account_state = self.account_state_sender();
1431
1432 let future = async move {
1433 let timeout_dur = Duration::from_secs(request_timeout_secs);
1434 let result = tokio::time::timeout(
1435 timeout_dur,
1436 subscribe_account_summary(&client_clone, ib_account),
1437 )
1438 .await;
1439
1440 match result {
1441 Ok(Ok(snapshot)) => send_account_state(snapshot),
1442 Ok(Err(e)) => {
1443 tracing::error!("Failed to query account state: {e}");
1444 }
1445 Err(_) => {
1446 tracing::error!("Timeout waiting for account summary");
1447 }
1448 }
1449 };
1450
1451 self.pending_tasks
1452 .spawn(future)
1453 .context("failed to register IB execution command task")?;
1454
1455 Ok(())
1456 }
1457
1458 fn query_order(&self, cmd: QueryOrder) -> anyhow::Result<()> {
1459 let client = self.ib_client.as_ref().context("IB client not connected")?;
1460 let client_order_id = cmd.client_order_id;
1461 let trader_id = cmd.trader_id;
1462 let strategy_id = cmd.strategy_id;
1463 let instrument_id = cmd.instrument_id;
1464
1465 let target_order = if let Some(venue_order_id) = &cmd.venue_order_id {
1466 IbOrderSelector::from_venue_order_id(venue_order_id)?
1467 } else {
1468 let state = self.orders.lock()?;
1469 IbOrderSelector::OrderId(
1470 *state
1471 .order_id_map
1472 .get(&cmd.client_order_id)
1473 .context("No venue order id for client_order_id")?,
1474 )
1475 };
1476
1477 let order_quantity = self
1478 .core
1479 .cache()
1480 .order(&cmd.client_order_id)
1481 .map(|order| order.quantity().as_decimal());
1482
1483 let client_clone = client.as_arc().clone();
1484 let orders = self.orders.clone();
1485 let instrument_provider = Arc::clone(&self.instrument_provider);
1486 let account_id = self.core.account_id;
1487 let exec_sender = get_exec_event_sender();
1488 let ts_init = get_atomic_clock_realtime().get_time_ns();
1489 let request_timeout_secs = self.config.request_timeout;
1490 let ib_account = self.ib_account;
1491
1492 let future = async move {
1493 let timeout_dur = Duration::from_secs(request_timeout_secs);
1494
1495 let subscription =
1496 match tokio::time::timeout(timeout_dur, client_clone.all_open_orders()).await {
1497 Ok(Ok(s)) => s,
1498 Ok(Err(e)) => {
1499 tracing::warn!("query_order: failed to request open orders: {e}");
1500 return;
1501 }
1502 Err(_) => {
1503 tracing::warn!("query_order: timeout requesting open orders");
1504 return;
1505 }
1506 };
1507
1508 let mut subscription = subscription.filter_data();
1509
1510 while let Some(order_result) = subscription.next().await {
1511 if let Ok(Orders::OrderData(data)) = order_result {
1512 if !data.order.account.is_empty() && data.order.account != ib_account {
1513 continue;
1514 }
1515
1516 if !target_order.matches(data.order_id, data.order.perm_id) {
1517 continue;
1518 }
1519
1520 let instrument_id = match orders.lock() {
1521 Ok(state) => state.order(data.order_id).map(|order| order.instrument_id),
1522 Err(_) => None,
1523 };
1524
1525 let instrument_id = match instrument_id {
1526 Some(id) => id,
1527 None => match instrument_provider
1528 .resolve_instrument_id_for_contract(&data.contract)
1529 {
1530 Ok(id) => id,
1531 Err(e) => {
1532 tracing::warn!("query_order: failed to convert contract: {e}");
1533 return;
1534 }
1535 },
1536 };
1537
1538 let report = match parse_order_data_to_report(
1539 &data,
1540 instrument_id,
1541 account_id,
1542 &instrument_provider,
1543 ts_init,
1544 ) {
1545 Ok(r) => r,
1546 Err(e) => {
1547 tracing::warn!("query_order: failed to parse order status: {e}");
1548 return;
1549 }
1550 };
1551
1552 if exec_sender
1553 .send(ExecutionEvent::Report(ExecutionReport::Order(Box::new(
1554 report,
1555 ))))
1556 .is_err()
1557 {
1558 tracing::error!("query_order: failed to send order status report");
1559 }
1560
1561 return;
1562 }
1563 }
1564
1565 let is_pending_cancel = orders.lock().is_ok_and(|state| {
1566 state
1567 .order_by_client(client_order_id)
1568 .is_some_and(|order| order.pending_cancel)
1569 });
1570
1571 if is_pending_cancel {
1572 let filter = Self::execution_filter(ib_account, None);
1573
1574 let executions = match tokio::time::timeout(
1575 timeout_dur,
1576 client_clone.executions(filter),
1577 )
1578 .await
1579 {
1580 Ok(Ok(subscription)) => subscription,
1581 Ok(Err(e)) => {
1582 tracing::error!(
1583 "query_order: failed to request executions before inferring cancel: {e}"
1584 );
1585 return;
1586 }
1587 Err(_) => {
1588 tracing::error!(
1589 "query_order: timeout requesting executions before inferring cancel"
1590 );
1591 return;
1592 }
1593 };
1594
1595 let mut executions = executions.filter_data();
1596 let mut execution_data = AHashMap::new();
1597 let mut commissions = AHashMap::new();
1598
1599 while let Some(result) = executions.next().await {
1600 match result {
1601 Ok(Executions::ExecutionData(data))
1602 if target_order
1603 .matches(data.execution.order_id, data.execution.perm_id) =>
1604 {
1605 execution_data.insert(data.execution.execution_id.clone(), data);
1606 }
1607 Ok(Executions::CommissionReport(commission)) => {
1608 commissions.insert(
1609 commission.execution_id,
1610 (commission.commission, commission.currency),
1611 );
1612 }
1613 Ok(_) => {}
1614 Err(e) => {
1615 tracing::error!(
1616 "query_order: execution query failed before cancel resolution: {e}"
1617 );
1618 return;
1619 }
1620 }
1621 }
1622
1623 if !execution_data.is_empty() {
1624 let cumulative_filled = execution_data
1625 .values()
1626 .map(|data| data.execution.cumulative_quantity)
1627 .fold(0.0_f64, f64::max);
1628
1629 for (execution_id, data) in execution_data {
1630 let Some((commission, currency)) = commissions.remove(&execution_id) else {
1631 tracing::error!(
1632 "query_order: execution {execution_id} has no commission report; cancel remains unresolved"
1633 );
1634 return;
1635 };
1636
1637 let report = match parse_execution_to_fill_report(
1638 &data.execution,
1639 &data.contract,
1640 commission,
1641 ¤cy,
1642 instrument_id,
1643 account_id,
1644 &instrument_provider,
1645 ts_init,
1646 None,
1647 ) {
1648 Ok(report) => report,
1649 Err(e) => {
1650 tracing::error!(
1651 "query_order: failed to parse execution {execution_id}: {e}"
1652 );
1653 return;
1654 }
1655 };
1656
1657 let context =
1658 Self::get_tracked_order_context(data.execution.order_id, &orders)
1659 .ok()
1660 .flatten();
1661
1662 let event = if let Some(context) = context {
1663 let quote_currency = instrument_provider
1664 .find(&context.instrument_id)
1665 .map_or(report.commission.currency, |instrument| {
1666 instrument.quote_currency()
1667 });
1668
1669 ExecutionEvent::Order(OrderEventAny::Filled(OrderFilled::new(
1670 context.trader_id,
1671 context.strategy_id,
1672 context.instrument_id,
1673 context.client_order_id,
1674 report.venue_order_id,
1675 report.account_id,
1676 report.trade_id,
1677 context.order_side,
1678 context.order_type,
1679 report.last_qty,
1680 report.last_px,
1681 quote_currency,
1682 report.liquidity_side,
1683 UUID4::new(),
1684 report.ts_event,
1685 report.ts_init,
1686 false,
1687 report.venue_position_id,
1688 Some(report.commission),
1689 None,
1690 )))
1691 } else {
1692 ExecutionEvent::Report(ExecutionReport::Fill(Box::new(report)))
1693 };
1694
1695 if exec_sender.send(event).is_err() {
1696 tracing::error!(
1697 "query_order: failed to send fill resolved from execution history"
1698 );
1699 return;
1700 }
1701 }
1702
1703 if let Ok(mut state) = orders.lock()
1704 && let Some(order) = state.order_by_client_mut(client_order_id)
1705 {
1706 order.pending_cancel = false;
1707 }
1708
1709 let size_precision = instrument_provider
1714 .find(&instrument_id)
1715 .map(|instrument| u32::from(instrument.size_precision()));
1716
1717 let remainder = order_quantity.and_then(|total| {
1718 Decimal::from_f64_retain(cumulative_filled).map(|filled| {
1719 let remainder = total - filled;
1720 size_precision.map_or(remainder, |dp| remainder.round_dp(dp))
1721 })
1722 });
1723
1724 match remainder {
1725 Some(remainder) if remainder > Decimal::ZERO => {
1726 Self::send_inferred_order_canceled(
1727 trader_id,
1728 strategy_id,
1729 instrument_id,
1730 client_order_id,
1731 target_order.venue_order_id(),
1732 account_id,
1733 ts_init,
1734 &exec_sender,
1735 "cancelled remainder after partial-fill recovery",
1736 );
1737 }
1738 Some(_) => {}
1739 None => {
1740 tracing::debug!(
1741 "query_order: cannot determine cancelled remainder for {} (order not in cache)",
1742 client_order_id
1743 );
1744 }
1745 }
1746
1747 return;
1748 }
1749 }
1750
1751 let was_pending_cancel = orders.lock().is_ok_and(|mut state| {
1752 state
1753 .order_by_client_mut(client_order_id)
1754 .is_some_and(|order| std::mem::replace(&mut order.pending_cancel, false))
1755 });
1756
1757 if was_pending_cancel {
1758 Self::send_inferred_order_canceled(
1759 trader_id,
1760 strategy_id,
1761 instrument_id,
1762 client_order_id,
1763 target_order.venue_order_id(),
1764 account_id,
1765 ts_init,
1766 &exec_sender,
1767 "missing open order",
1768 );
1769 return;
1770 }
1771
1772 tracing::debug!(
1773 "query_order: order {} not found in open orders (may be filled or canceled)",
1774 target_order.label()
1775 );
1776 };
1777
1778 self.pending_tasks
1779 .spawn(future)
1780 .context("failed to register IB execution command task")?;
1781
1782 Ok(())
1783 }
1784
1785 fn submit_order_list(&self, cmd: SubmitOrderList) -> anyhow::Result<()> {
1786 let orders = self.core.get_orders_for_list(&cmd.order_list)?;
1787 if let Some(reason) = orders.iter().find_map(|order| validate_order(order).err()) {
1788 self.deny_submit_order_list(&cmd, &reason.to_string())?;
1789 return Ok(());
1790 }
1791
1792 if let Err(reason) = self.ensure_client_ready_for_order_request("submit order list") {
1793 let reason = coded_denial_reason(DENIAL_CLIENT_NOT_READY, &reason);
1794 self.deny_submit_order_list(&cmd, &reason)?;
1795 return Ok(());
1796 }
1797
1798 self.submit_order_list_with_orders(cmd, orders)
1799 }
1800
1801 fn modify_order(&self, cmd: ModifyOrder) -> anyhow::Result<()> {
1802 if self
1803 .orders
1804 .lock()?
1805 .group_for_client(cmd.client_order_id)
1806 .is_some()
1807 {
1808 Self::send_order_modify_rejected(
1809 &cmd,
1810 "Cannot modify an order with unresolved duplicate broker identities; cancel and reconcile it first",
1811 &get_exec_event_sender(),
1812 get_atomic_clock_realtime().get_time_ns(),
1813 self.core.account_id,
1814 )?;
1815 return Ok(());
1816 }
1817
1818 if let Err(reason) = self.ensure_client_ready_for_order_request("modify order") {
1819 Self::send_order_modify_rejected(
1820 &cmd,
1821 &reason,
1822 &get_exec_event_sender(),
1823 get_atomic_clock_realtime().get_time_ns(),
1824 self.core.account_id,
1825 )?;
1826 return Ok(());
1827 }
1828
1829 let client = self.ib_client.as_ref().context("IB client not connected")?;
1830
1831 let orders = self.orders.clone();
1832 let instrument_provider = Arc::clone(&self.instrument_provider);
1833 let exec_sender = get_exec_event_sender();
1834 let clock = get_atomic_clock_realtime();
1835 let account_id = self.core.account_id;
1836 let ib_account = self.ib_account;
1837 let client_clone = client.as_arc().clone();
1838 let request_timeout_secs = self.config.request_timeout;
1839
1840 let future = async move {
1841 if let Err(e) = Self::handle_modify_order_async(
1842 &cmd,
1843 &client_clone,
1844 &orders,
1845 &instrument_provider,
1846 ib_account,
1847 request_timeout_secs,
1848 )
1849 .await
1850 {
1851 let reason = format!("Failed to route modify order to IB: {e:#}");
1852
1853 if let Err(send_error) = Self::send_order_modify_rejected(
1854 &cmd,
1855 &reason,
1856 &exec_sender,
1857 clock.get_time_ns(),
1858 account_id,
1859 ) {
1860 tracing::error!("{reason}; failed to emit OrderModifyRejected: {send_error}");
1861 }
1862 }
1863 };
1864
1865 self.pending_tasks
1866 .spawn(future)
1867 .context("failed to register IB execution command task")?;
1868
1869 Ok(())
1870 }
1871
1872 fn cancel_order(&self, cmd: CancelOrder) -> anyhow::Result<()> {
1873 let target_order = Arc::new(self.core.get_order(&cmd.client_order_id)?);
1874 let exec_sender = get_exec_event_sender();
1875 let clock = get_atomic_clock_realtime();
1876 let account_id = self.core.account_id;
1877
1878 if let Err(e) = Self::validate_cancel_order_target(&cmd, &target_order) {
1879 let reason = format!("Failed to resolve cancel order target: {e:#}");
1880 Self::send_order_cancel_rejected(
1881 &target_order,
1882 &reason,
1883 &exec_sender,
1884 clock.get_time_ns(),
1885 account_id,
1886 )?;
1887 return Ok(());
1888 }
1889
1890 if let Err(reason) = self.ensure_client_ready_for_order_request("cancel order") {
1891 Self::send_order_cancel_rejected(
1892 &target_order,
1893 &reason,
1894 &exec_sender,
1895 clock.get_time_ns(),
1896 account_id,
1897 )?;
1898 return Ok(());
1899 }
1900
1901 let client = self.ib_client.as_ref().context("IB client not connected")?;
1902
1903 let orders = self.orders.clone();
1904 let instrument_provider = Arc::clone(&self.instrument_provider);
1905 let exec_sender = get_exec_event_sender();
1906 let clock = get_atomic_clock_realtime();
1907 let account_id = self.core.account_id;
1908 let ib_account = self.ib_account;
1909 let client_clone = client.as_arc().clone();
1910 let request_timeout_secs = self.config.request_timeout;
1911
1912 let future = async move {
1913 if let Err(e) = Self::handle_cancel_order_async(
1914 &cmd,
1915 &target_order,
1916 &client_clone,
1917 &orders,
1918 &instrument_provider,
1919 &exec_sender,
1920 clock.get_time_ns(),
1921 account_id,
1922 ib_account,
1923 request_timeout_secs,
1924 )
1925 .await
1926 {
1927 let reason = format!("Failed to route cancel order to IB: {e:#}");
1928
1929 if let Err(send_error) = Self::send_order_cancel_rejected(
1930 &target_order,
1931 &reason,
1932 &exec_sender,
1933 clock.get_time_ns(),
1934 account_id,
1935 ) {
1936 tracing::error!("{reason}; failed to emit OrderCancelRejected: {send_error}");
1937 }
1938 }
1939 };
1940
1941 self.pending_tasks
1942 .spawn(future)
1943 .context("failed to register IB execution command task")?;
1944
1945 Ok(())
1946 }
1947
1948 fn cancel_all_orders(&self, cmd: CancelAllOrders) -> anyhow::Result<()> {
1949 if self
1950 .ensure_client_ready_for_order_request("cancel orders")
1951 .is_err()
1952 {
1953 return Ok(());
1954 }
1955
1956 let client = self.ib_client.as_ref().context("IB client not connected")?;
1957 let orders_to_cancel = self.cancel_all_targets(&cmd)?;
1958
1959 if orders_to_cancel.is_empty() {
1960 tracing::debug!("No open orders to cancel");
1961 return Ok(());
1962 }
1963
1964 tracing::debug!(
1965 "Canceling {} open order(s) for instrument {}",
1966 orders_to_cancel.len(),
1967 cmd.instrument_id
1968 );
1969
1970 let client_clone = client.as_arc().clone();
1971 let orders = self.orders.clone();
1972 let exec_sender = get_exec_event_sender();
1973 let clock = get_atomic_clock_realtime();
1974 let account_id = self.core.account_id;
1975 let ib_account = self.ib_account;
1976 let request_timeout_secs = self.config.request_timeout;
1977
1978 let provider = Arc::clone(&self.instrument_provider);
1979
1980 let future = async move {
1981 if let Err(e) = Self::handle_cancel_all_orders_async(
1982 &client_clone,
1983 &orders,
1984 &exec_sender,
1985 clock.get_time_ns(),
1986 account_id,
1987 ib_account,
1988 request_timeout_secs,
1989 orders_to_cancel,
1990 &provider,
1991 cmd.order_side,
1992 )
1993 .await
1994 {
1995 tracing::error!("Error canceling all orders: {e}");
1996 }
1997 };
1998
1999 self.pending_tasks
2000 .spawn(future)
2001 .context("failed to register IB execution command task")?;
2002
2003 Ok(())
2004 }
2005
2006 fn batch_cancel_orders(&self, cmd: BatchCancelOrders) -> anyhow::Result<()> {
2007 for cancel_cmd in cmd.cancels {
2009 self.cancel_order(cancel_cmd)?;
2010 }
2011
2012 Ok(())
2013 }
2014
2015 fn register_external_order(
2018 &self,
2019 client_order_id: ClientOrderId,
2020 venue_order_id: VenueOrderId,
2021 _instrument_id: InstrumentId,
2022 _strategy_id: StrategyId,
2023 _ts_init: UnixNanos,
2024 ) {
2025 let order = match self.core.get_order(&client_order_id) {
2026 Ok(order) if order.is_open() => order,
2027 Ok(_) => return,
2028 Err(e) => {
2029 tracing::warn!("Cannot track external IB order {client_order_id}: {e}");
2030 return;
2031 }
2032 };
2033
2034 let order_selector = match IbOrderSelector::from_venue_order_id(&venue_order_id) {
2035 Ok(order_selector) => order_selector,
2036 Err(e) => {
2037 tracing::warn!("Cannot track external IB order {client_order_id}: {e}");
2038 return;
2039 }
2040 };
2041
2042 let Some(client) = self.ib_client.as_ref() else {
2043 tracing::warn!(
2044 "Cannot track external IB order {client_order_id}: IB client not connected"
2045 );
2046 return;
2047 };
2048
2049 let client = client.as_arc().clone();
2050 let orders = self.orders.clone();
2051 let ib_account = self.ib_account;
2052 let request_timeout_secs = self.config.request_timeout;
2053
2054 let future = async move {
2055 let result = Self::resolve_ib_order_id(
2056 &client,
2057 order_selector,
2058 ib_account,
2059 request_timeout_secs,
2060 )
2061 .await
2062 .and_then(|ib_order_id| {
2063 Self::cache_recovered_order_tracking(ib_order_id, &order, &orders)
2064 });
2065
2066 if let Err(e) = result {
2069 tracing::debug!("Cannot track external IB order {client_order_id}: {e:#}");
2070 }
2071 };
2072
2073 if let Err(e) = self.pending_tasks.spawn(future) {
2074 tracing::warn!("Cannot track external IB order {client_order_id}: {e}");
2075 }
2076 }
2077}
2078
2079fn validate_order(order: &impl Order) -> Result<(), OrderDeniedReason> {
2080 if order.is_reduce_only() {
2081 return Err(OrderDeniedReason::UnsupportedReduceOnly);
2082 }
2083
2084 Ok(())
2085}
2086
2087impl InteractiveBrokersExecutionClient {
2088 fn account_state_sender(&self) -> impl Fn(AccountSnapshot) + Send + 'static {
2091 let factory = OrderEventFactory::new(
2092 self.core.trader_id,
2093 self.core.account_id,
2094 self.core.account_type,
2095 self.core.base_currency,
2096 );
2097 let exec_sender = get_exec_event_sender();
2098 let clock = get_atomic_clock_realtime();
2099 move |(balances, margins, info)| {
2100 let ts_now = clock.get_time_ns();
2101 let state =
2102 factory.generate_account_state(balances, margins, true, ts_now, ts_now, info);
2103
2104 if let Err(e) = exec_sender.send(ExecutionEvent::Account(state)) {
2105 tracing::warn!("Failed to send account state: {e}");
2106 }
2107 }
2108 }
2109
2110 fn report_client(&self) -> anyhow::Result<IbReportClient> {
2113 let client = self.ib_client.as_ref().context("IB client not connected")?;
2114
2115 Ok(IbReportClient::new(
2116 client.as_arc().clone(),
2117 IbReportContext::new(Arc::clone(&self.instrument_provider), self.core.account_id),
2118 self.ib_account,
2119 Duration::from_secs(self.config.request_timeout),
2120 ))
2121 }
2122
2123 pub(super) fn execution_filter(ib_account: Ustr, start: Option<UnixNanos>) -> ExecutionFilter {
2124 let time = start.map_or_else(String::new, |start| {
2125 start
2126 .to_datetime_utc()
2127 .strftime("%Y%m%d-%H:%M:%S")
2128 .to_string()
2129 });
2130
2131 ExecutionFilter {
2132 client_id: None,
2133 account_code: ib_account.to_string(),
2134 time,
2135 symbol: String::new(),
2136 security_type: String::new(),
2137 exchange: String::new(),
2138 side: None,
2139 last_n_days: 0,
2140 specific_dates: Vec::new(),
2141 }
2142 }
2143
2144 fn is_ready_for_order_request(&self) -> bool {
2145 if !self.is_connected.load(Ordering::Relaxed) {
2146 return false;
2147 }
2148
2149 if !self
2150 .ib_client
2151 .as_ref()
2152 .is_some_and(|client| client.is_connected())
2153 {
2154 return false;
2155 }
2156
2157 *self.next_order_id.lock() > 0
2158 }
2159
2160 fn cancel_all_targets(
2163 &self,
2164 cmd: &CancelAllOrders,
2165 ) -> anyhow::Result<Vec<(ClientOrderId, Option<VenueOrderId>)>> {
2166 let cache = self.core.cache();
2167 let mut selected: Vec<_> = cache
2168 .orders_open(
2169 None,
2170 Some(&cmd.instrument_id),
2171 None,
2172 Some(&self.core.account_id),
2173 cmd.order_side,
2174 )
2175 .iter()
2176 .map(|order| (order.client_order_id(), order.venue_order_id()))
2177 .collect();
2178 let state = self.orders.lock()?;
2179 selected.extend(state.active_orders.iter().filter_map(|(order_id, order)| {
2180 (order.instrument_id == cmd.instrument_id
2181 && cmd.order_side.is_none_or(|side| side == order.order_side))
2182 .then_some((
2183 order.client_order_id,
2184 Some(parse::ib_venue_order_id(*order_id, order.perm_id)),
2185 ))
2186 }));
2187
2188 selected.extend(
2189 state
2190 .group_cancel_candidates(cmd.instrument_id, self.core.account_id, cmd.order_side)
2191 .map(|parent| (parent, None)),
2192 );
2193 selected.sort_by_key(|(id, _)| id.to_string());
2194 selected.dedup_by_key(|(id, _)| *id);
2195 Ok(selected)
2196 }
2197
2198 fn ensure_client_ready_for_order_request(&self, request: &str) -> Result<(), String> {
2199 if self.is_ready_for_order_request() {
2200 return Ok(());
2201 }
2202
2203 let reason = format!("Interactive Brokers client is not ready; refusing to {request}");
2204 tracing::warn!("{reason}");
2205 Err(reason)
2206 }
2207
2208 fn deny_submit_order_not_ready(&self, cmd: &SubmitOrder, reason: &str) -> anyhow::Result<()> {
2209 let reason = coded_denial_reason(DENIAL_CLIENT_NOT_READY, reason);
2210 Self::send_order_denied(
2211 cmd.order_init.trader_id,
2212 cmd.strategy_id,
2213 cmd.instrument_id,
2214 cmd.order_init.client_order_id,
2215 &reason,
2216 )
2217 }
2218
2219 fn deny_submit_order_list(&self, cmd: &SubmitOrderList, reason: &str) -> anyhow::Result<()> {
2220 for order_init in &cmd.order_inits {
2221 Self::send_order_denied(
2222 order_init.trader_id,
2223 cmd.strategy_id,
2224 cmd.instrument_id,
2225 order_init.client_order_id,
2226 reason,
2227 )?;
2228 }
2229
2230 Ok(())
2231 }
2232
2233 #[allow(clippy::too_many_arguments)] fn send_inferred_order_canceled(
2235 trader_id: TraderId,
2236 strategy_id: StrategyId,
2237 instrument_id: InstrumentId,
2238 client_order_id: ClientOrderId,
2239 venue_order_id: VenueOrderId,
2240 account_id: AccountId,
2241 ts_init: UnixNanos,
2242 exec_sender: &EventSender<ExecutionEvent>,
2243 context: &str,
2244 ) {
2245 let event = OrderCanceled::new(
2246 trader_id,
2247 strategy_id,
2248 instrument_id,
2249 client_order_id,
2250 UUID4::new(),
2251 ts_init,
2252 ts_init,
2253 false,
2254 Some(venue_order_id),
2255 Some(account_id),
2256 None,
2257 );
2258
2259 if exec_sender
2260 .send(ExecutionEvent::Order(OrderEventAny::Canceled(event)))
2261 .is_err()
2262 {
2263 tracing::error!("query_order: failed to send inferred order canceled event");
2264 } else {
2265 tracing::debug!(
2266 "query_order: inferred cancel for {} ({})",
2267 client_order_id,
2268 context
2269 );
2270 }
2271 }
2272
2273 fn send_order_denied(
2274 trader_id: TraderId,
2275 strategy_id: StrategyId,
2276 instrument_id: InstrumentId,
2277 client_order_id: ClientOrderId,
2278 reason: &str,
2279 ) -> anyhow::Result<()> {
2280 let ts_event = get_atomic_clock_realtime().get_time_ns();
2281 let exec_sender = get_exec_event_sender();
2282 Self::send_order_denied_to(
2283 trader_id,
2284 strategy_id,
2285 instrument_id,
2286 client_order_id,
2287 reason,
2288 &exec_sender,
2289 ts_event,
2290 )
2291 }
2292
2293 pub(super) fn send_order_denied_to(
2294 trader_id: TraderId,
2295 strategy_id: StrategyId,
2296 instrument_id: InstrumentId,
2297 client_order_id: ClientOrderId,
2298 reason: &str,
2299 exec_sender: &EventSender<ExecutionEvent>,
2300 ts_event: UnixNanos,
2301 ) -> anyhow::Result<()> {
2302 let event = OrderDenied::new(
2303 trader_id,
2304 strategy_id,
2305 instrument_id,
2306 client_order_id,
2307 Ustr::from(reason),
2308 UUID4::new(),
2309 ts_event,
2310 ts_event,
2311 );
2312
2313 exec_sender
2314 .send(ExecutionEvent::Order(OrderEventAny::Denied(event)))
2315 .map_err(|e| anyhow::anyhow!("Failed to send order denied event: {e}"))
2316 }
2317
2318 fn send_order_modify_rejected(
2319 cmd: &ModifyOrder,
2320 reason: &str,
2321 exec_sender: &EventSender<ExecutionEvent>,
2322 ts_event: UnixNanos,
2323 account_id: AccountId,
2324 ) -> anyhow::Result<()> {
2325 let event = OrderModifyRejected::new(
2326 cmd.trader_id,
2327 cmd.strategy_id,
2328 cmd.instrument_id,
2329 cmd.client_order_id,
2330 Ustr::from(reason),
2331 UUID4::new(),
2332 ts_event,
2333 ts_event,
2334 false,
2335 cmd.venue_order_id,
2336 Some(account_id),
2337 );
2338 exec_sender
2339 .send(ExecutionEvent::Order(OrderEventAny::ModifyRejected(event)))
2340 .map_err(|e| anyhow::anyhow!("Failed to send order modify rejected event: {e}"))
2341 }
2342
2343 fn send_order_cancel_rejected(
2344 target_order: &OrderAny,
2345 reason: &str,
2346 exec_sender: &EventSender<ExecutionEvent>,
2347 ts_event: UnixNanos,
2348 account_id: AccountId,
2349 ) -> anyhow::Result<()> {
2350 let event = OrderCancelRejected::new(
2351 target_order.trader_id(),
2352 target_order.strategy_id(),
2353 target_order.instrument_id(),
2354 target_order.client_order_id(),
2355 Ustr::from(reason),
2356 UUID4::new(),
2357 ts_event,
2358 ts_event,
2359 false,
2360 target_order.venue_order_id(),
2361 Some(account_id),
2362 );
2363 exec_sender
2364 .send(ExecutionEvent::Order(OrderEventAny::CancelRejected(event)))
2365 .map_err(|e| anyhow::anyhow!("Failed to send order cancel rejected event: {e}"))
2366 }
2367}
2368
2369impl InteractiveBrokersExecutionClient {
2370 fn validate_cancel_order_target(
2371 cmd: &CancelOrder,
2372 target_order: &OrderAny,
2373 ) -> anyhow::Result<()> {
2374 anyhow::ensure!(
2375 cmd.client_order_id == target_order.client_order_id(),
2376 "command client order ID {} does not match cached order {}",
2377 cmd.client_order_id,
2378 target_order.client_order_id()
2379 );
2380 anyhow::ensure!(
2381 cmd.instrument_id == target_order.instrument_id(),
2382 "command instrument ID {} does not match cached order {}",
2383 cmd.instrument_id,
2384 target_order.instrument_id()
2385 );
2386
2387 if let (Some(command_venue_order_id), Some(target_venue_order_id)) =
2389 (cmd.venue_order_id.as_ref(), target_order.venue_order_id())
2390 {
2391 anyhow::ensure!(
2392 command_venue_order_id == &target_venue_order_id,
2393 "command venue order ID {command_venue_order_id} does not match cached order {target_venue_order_id}"
2394 );
2395 }
2396
2397 Ok(())
2398 }
2399
2400 #[allow(
2406 clippy::too_many_arguments,
2407 reason = "cancel routing requires the request, target order, venue client, and event context"
2408 )]
2409 async fn handle_cancel_order_async(
2410 cmd: &CancelOrder,
2411 target_order: &OrderAny,
2412 client: &Arc<Client>,
2413 orders: &OrderTracker,
2414 instrument_provider: &Arc<InteractiveBrokersInstrumentProvider>,
2415 exec_sender: &EventSender<ExecutionEvent>,
2416 ts_init: UnixNanos,
2417 account_id: AccountId,
2418 ib_account: Ustr,
2419 request_timeout_secs: u64,
2420 ) -> anyhow::Result<()> {
2421 if orders.lock()?.groups.contains_key(&cmd.client_order_id) {
2422 return Self::cancel_incarnation_group(
2423 cmd.client_order_id,
2424 client,
2425 orders,
2426 instrument_provider,
2427 exec_sender,
2428 ts_init,
2429 account_id,
2430 ib_account,
2431 request_timeout_secs,
2432 None,
2433 target_order.is_pending_cancel(),
2434 )
2435 .await;
2436 }
2437
2438 let order_selector = if let Some(venue_order_id) = &cmd.venue_order_id {
2439 IbOrderSelector::from_venue_order_id(venue_order_id)?
2440 } else {
2441 let state = orders.lock()?;
2442 IbOrderSelector::OrderId(
2443 *state
2444 .order_id_map
2445 .get(&cmd.client_order_id)
2446 .context("No IB order ID mapping found for client order ID")?,
2447 )
2448 };
2449
2450 let ib_order_id =
2451 Self::resolve_ib_order_id(client, order_selector, ib_account, request_timeout_secs)
2452 .await?;
2453 Self::cache_recovered_order_tracking(ib_order_id, target_order, orders)?;
2454 let venue_order_id = target_order
2455 .venue_order_id()
2456 .unwrap_or_else(|| VenueOrderId::from(ib_order_id.to_string()));
2457
2458 let timeout_dur = Duration::from_secs(request_timeout_secs);
2459
2460 let send_error =
2461 match tokio::time::timeout(timeout_dur, client.cancel_order(ib_order_id, "")).await {
2462 Ok(Ok(_subscription)) => None,
2463 Ok(Err(e)) => match Self::classify_order_submit_error(&e) {
2464 CommandFailure::NotSent(reason) | CommandFailure::VenueRejected(reason) => {
2465 anyhow::bail!(reason)
2466 }
2467 CommandFailure::Ambiguous(reason) => Some(reason),
2468 },
2469 Err(_) => Some(format!(
2470 "cancel request timed out after {request_timeout_secs} seconds"
2471 )),
2472 };
2473
2474 if let Some(send_error) = send_error {
2475 tracing::warn!(
2476 "Cancel outcome is unknown after attempting to send order {} to IB: {}; querying venue state",
2477 cmd.client_order_id,
2478 send_error
2479 );
2480 return Self::resolve_ambiguous_cancel(
2481 target_order,
2482 client,
2483 order_selector,
2484 account_id,
2485 ib_account,
2486 request_timeout_secs,
2487 &send_error,
2488 exec_sender,
2489 ts_init,
2490 )
2491 .await;
2492 }
2493
2494 if orders.lock()?.terminal_orders.contains_key(&ib_order_id) {
2497 tracing::debug!(
2498 "Cancel request for order {} was sent after IB reported it terminal",
2499 cmd.client_order_id
2500 );
2501 return Ok(());
2502 }
2503
2504 let result = if target_order.is_pending_cancel() {
2506 Self::track_pending_cancel(cmd.client_order_id, orders).map(|_| ())
2507 } else {
2508 Self::emit_order_pending_cancel(
2509 ib_order_id,
2510 cmd.client_order_id,
2511 venue_order_id,
2512 orders,
2513 exec_sender,
2514 ts_init,
2515 account_id,
2516 )
2517 };
2518
2519 if let Err(e) = result {
2520 tracing::error!(
2521 "Cancel request for order {} was sent, but OrderPendingCancel emission failed: {e}",
2522 cmd.client_order_id
2523 );
2524 }
2525
2526 Ok(())
2527 }
2528
2529 #[allow(clippy::too_many_arguments)] async fn resolve_ambiguous_cancel(
2531 target_order: &OrderAny,
2532 client: &Arc<Client>,
2533 order_selector: IbOrderSelector,
2534 account_id: AccountId,
2535 ib_account: Ustr,
2536 request_timeout_secs: u64,
2537 send_error: &str,
2538 exec_sender: &EventSender<ExecutionEvent>,
2539 ts_init: UnixNanos,
2540 ) -> anyhow::Result<()> {
2541 let timeout_dur = Duration::from_secs(request_timeout_secs);
2542
2543 let remains_open = tokio::time::timeout(timeout_dur, async {
2544 let mut subscription = client.all_open_orders().await?.filter_data();
2545 while let Some(result) = subscription.next().await {
2546 let Orders::OrderData(data) = result? else {
2547 continue;
2548 };
2549
2550 if !data.order.account.is_empty() && data.order.account != ib_account {
2551 continue;
2552 }
2553
2554 if order_selector.matches(data.order_id, data.order.perm_id) {
2555 return Ok::<bool, ibapi::Error>(true);
2556 }
2557 }
2558
2559 Ok(false)
2560 })
2561 .await
2562 .context("Timed out querying open orders after ambiguous cancel")??;
2563
2564 if remains_open {
2565 let reason = format!(
2566 "Cancel was not confirmed after transport error; order remains open at IB: {send_error}"
2567 );
2568 return Self::send_order_cancel_rejected(
2569 target_order,
2570 &reason,
2571 exec_sender,
2572 ts_init,
2573 account_id,
2574 );
2575 }
2576
2577 let completed_status = tokio::time::timeout(timeout_dur, async {
2578 let mut subscription = client.completed_orders(false).await?.filter_data();
2579 while let Some(result) = subscription.next().await {
2580 let Orders::OrderData(data) = result? else {
2581 continue;
2582 };
2583
2584 if !data.order.account.is_empty() && data.order.account != ib_account {
2585 continue;
2586 }
2587
2588 let matches_order = order_selector.matches(data.order_id, data.order.perm_id);
2589 if matches_order {
2590 return Ok::<Option<OrderStatusKind>, ibapi::Error>(Some(
2591 data.order_state.status,
2592 ));
2593 }
2594 }
2595
2596 Ok(None)
2597 })
2598 .await
2599 .context("Timed out querying completed orders after ambiguous cancel")??;
2600
2601 if completed_status.as_ref().is_some_and(|status| {
2602 matches!(
2603 status,
2604 OrderStatusKind::Cancelled | OrderStatusKind::ApiCancelled
2605 )
2606 }) {
2607 let event = OrderCanceled::new(
2608 target_order.trader_id(),
2609 target_order.strategy_id(),
2610 target_order.instrument_id(),
2611 target_order.client_order_id(),
2612 UUID4::new(),
2613 ts_init,
2614 ts_init,
2615 false,
2616 Some(order_selector.venue_order_id()),
2617 Some(account_id),
2618 None,
2619 );
2620 exec_sender
2621 .send(ExecutionEvent::Order(OrderEventAny::Canceled(event)))
2622 .map_err(|e| {
2623 anyhow::anyhow!("Failed to send resolved order canceled event: {e}")
2624 })?;
2625
2626 return Ok(());
2627 }
2628
2629 let reason = completed_status.map_or_else(
2630 || {
2631 format!(
2632 "Cancel outcome could not be resolved from IB open or completed orders: {send_error}"
2633 )
2634 },
2635 |status| {
2636 format!(
2637 "Cancel did not complete; IB reports order status {}",
2638 status.as_str()
2639 )
2640 },
2641 );
2642
2643 Self::send_order_cancel_rejected(target_order, &reason, exec_sender, ts_init, account_id)
2644 }
2645
2646 pub(super) async fn resolve_ib_order_id(
2647 client: &Arc<Client>,
2648 order_selector: IbOrderSelector,
2649 ib_account: Ustr,
2650 request_timeout_secs: u64,
2651 ) -> anyhow::Result<i32> {
2652 let target_perm_id = match order_selector {
2653 IbOrderSelector::OrderId(order_id) => return Ok(order_id),
2654 IbOrderSelector::PermId(perm_id) => perm_id,
2655 };
2656
2657 let timeout_dur = Duration::from_secs(request_timeout_secs);
2658 tokio::time::timeout(timeout_dur, async {
2659 let mut subscription = client.all_open_orders().await?.filter_data();
2660 let mut routes = AHashMap::<(i32, i32), AHashSet<i64>>::new();
2661 let mut target_routes = AHashSet::new();
2662
2663 while let Some(result) = subscription.next().await {
2664 let Orders::OrderData(data) = result? else { continue; };
2665 if data.order.account != ib_account || !Self::is_active_open_order(&data.order) { continue; }
2666 let route = (data.order.client_id, data.order_id);
2667 routes.entry(route).or_default().insert(data.order.perm_id);
2668 if data.order.perm_id == target_perm_id { target_routes.insert(route); }
2669 }
2670
2671 anyhow::ensure!(target_routes.len() == 1,
2672 "Cannot resolve PERM-{target_perm_id}: expected one broker route, found {}", target_routes.len());
2673 let route = *target_routes.iter().next().expect("one route was checked");
2674 anyhow::ensure!(route.0 == client.client_id() && route.1 != 0,
2675 "Cannot resolve PERM-{target_perm_id}: order is not bound to this API client");
2676 anyhow::ensure!(routes.get(&route).is_some_and(|ids| ids.len() == 1),
2677 "Cannot resolve PERM-{target_perm_id}: broker route is shared by distinct permanent IDs");
2678 Ok(route.1)
2679 }).await.context("timed out resolving IB permanent order identity")?
2680 }
2681
2682 pub(super) fn is_active_open_order(order: &ibapi::orders::Order) -> bool {
2683 !order.deactivate
2684 }
2685
2686 pub(super) fn is_definitive_order_submit_error(error: &ibapi::Error) -> bool {
2687 matches!(
2688 error,
2689 ibapi::Error::InvalidArgument(_) | ibapi::Error::ServerVersion(_, _, _)
2690 )
2691 }
2692
2693 pub(super) fn classify_order_submit_error(error: &ibapi::Error) -> CommandFailure {
2694 let reason = error.to_string();
2695 if Self::is_definitive_order_submit_error(error) {
2696 CommandFailure::not_sent(reason)
2697 } else if matches!(
2698 error,
2699 ibapi::Error::Notice(notice)
2700 if notice.category() == ibapi::NoticeCategory::OrderRejection
2701 ) {
2702 CommandFailure::venue_rejected(reason)
2703 } else {
2704 CommandFailure::ambiguous(reason)
2705 }
2706 }
2707
2708 async fn handle_cancel_all_orders_async(
2709 client: &Arc<Client>,
2710 orders: &OrderTracker,
2711 exec_sender: &EventSender<ExecutionEvent>,
2712 ts_init: UnixNanos,
2713 account_id: AccountId,
2714 ib_account: Ustr,
2715 request_timeout_secs: u64,
2716 orders_to_cancel: Vec<(ClientOrderId, Option<VenueOrderId>)>,
2717 provider: &Arc<InteractiveBrokersInstrumentProvider>,
2718 order_side: Option<OrderSide>,
2719 ) -> anyhow::Result<()> {
2720 let mut handled = AHashSet::new();
2721 let mut individual = Vec::new();
2722
2723 for (id, venue_id) in orders_to_cancel {
2724 let parent = orders.lock()?.group_for_client(id);
2725 if let Some(parent) = parent {
2726 if handled.insert(parent) {
2727 Self::cancel_incarnation_group(
2728 parent,
2729 client,
2730 orders,
2731 provider,
2732 exec_sender,
2733 ts_init,
2734 account_id,
2735 ib_account,
2736 request_timeout_secs,
2737 order_side,
2738 false,
2739 )
2740 .await?;
2741 }
2742 } else {
2743 individual.push((id, venue_id));
2744 }
2745 }
2746
2747 let orders_to_cancel = individual;
2748
2749 let order_selectors: Vec<(ClientOrderId, IbOrderSelector, Option<VenueOrderId>)> = {
2751 let state = orders.lock()?;
2752 orders_to_cancel
2753 .into_iter()
2754 .filter_map(|(client_order_id, venue_order_id)| {
2755 match state.cancel_selector(client_order_id, venue_order_id.as_ref()) {
2756 Ok(Some(order_selector)) => {
2757 Some((client_order_id, order_selector, venue_order_id))
2758 }
2759 Ok(None) => None,
2760 Err(e) => {
2761 tracing::error!(
2762 "Failed to resolve cancel-all order {client_order_id}: {e}"
2763 );
2764 None
2765 }
2766 }
2767 })
2768 .collect()
2769 };
2770
2771 for (client_order_id, order_selector, venue_order_id) in order_selectors {
2773 let ib_order_id = match Self::resolve_ib_order_id(
2774 client,
2775 order_selector,
2776 ib_account,
2777 request_timeout_secs,
2778 )
2779 .await
2780 {
2781 Ok(ib_order_id) => ib_order_id,
2782 Err(e) => {
2783 tracing::error!("Failed resolve cancel-all order {client_order_id}: {e}");
2784 continue;
2785 }
2786 };
2787
2788 let venue_order_id =
2789 venue_order_id.unwrap_or_else(|| VenueOrderId::from(ib_order_id.to_string()));
2790
2791 if let Err(e) = client.cancel_order(ib_order_id, "").await {
2792 tracing::warn!(
2793 "Failed to cancel order {} (IB order ID: {}): {e}",
2794 client_order_id,
2795 ib_order_id
2796 );
2797 } else {
2798 if let Err(e) = Self::emit_order_pending_cancel(
2799 ib_order_id,
2800 client_order_id,
2801 venue_order_id,
2802 orders,
2803 exec_sender,
2804 ts_init,
2805 account_id,
2806 ) {
2807 tracing::error!(
2808 "Failed to emit pending cancel for order {} (IB order ID: {}): {e}",
2809 client_order_id,
2810 ib_order_id
2811 );
2812 }
2813
2814 tracing::debug!(
2815 "Canceled order {} (IB order ID: {})",
2816 client_order_id,
2817 ib_order_id
2818 );
2819 }
2820 }
2821
2822 tracing::debug!("Finished canceling all orders");
2823
2824 Ok(())
2825 }
2826
2827 pub(super) fn emit_order_pending_cancel(
2828 _order_id: i32,
2829 client_order_id: ClientOrderId,
2830 venue_order_id: VenueOrderId,
2831 orders: &OrderTracker,
2832 exec_sender: &EventSender<ExecutionEvent>,
2833 ts_init: UnixNanos,
2834 account_id: AccountId,
2835 ) -> anyhow::Result<()> {
2836 let Some((trader_id, strategy_id, instrument_id)) =
2837 Self::track_pending_cancel(client_order_id, orders)?
2838 else {
2839 return Ok(());
2840 };
2841
2842 let event = OrderPendingCancel::new(
2843 trader_id,
2844 strategy_id,
2845 instrument_id,
2846 client_order_id,
2847 Some(account_id),
2848 UUID4::new(),
2849 ts_init,
2850 ts_init,
2851 false,
2852 Some(venue_order_id),
2853 );
2854
2855 exec_sender
2856 .send(ExecutionEvent::Order(OrderEventAny::PendingCancel(event)))
2857 .map_err(|e| anyhow::anyhow!("Failed to send order pending cancel event: {e}"))?;
2858
2859 Ok(())
2860 }
2861
2862 pub(super) fn track_pending_cancel(
2864 client_order_id: ClientOrderId,
2865 orders: &OrderTracker,
2866 ) -> anyhow::Result<Option<(TraderId, StrategyId, InstrumentId)>> {
2867 let mut state = orders.lock()?;
2868 let order = state
2869 .order_by_client_mut(client_order_id)
2870 .context("Tracked state not found for pending cancel order")?;
2871
2872 if order.pending_cancel {
2873 return Ok(None);
2874 }
2875
2876 order.pending_cancel = true;
2877 Ok(Some((
2878 order.trader_id,
2879 order.strategy_id,
2880 order.instrument_id,
2881 )))
2882 }
2883}