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