Skip to main content

nautilus_interactive_brokers/execution/
core.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Core execution client implementation for Interactive Brokers.
17
18#[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/// Interactive Brokers execution client.
136///
137/// This client provides order execution functionality using the `rust-ibapi` library.
138/// It manages order submission, modification, cancellation, and execution reporting.
139#[cfg_attr(
140    feature = "python",
141    pyo3::pyclass(module = "nautilus_trader.adapters.interactive_brokers", unsendable)
142)]
143pub struct InteractiveBrokersExecutionClient {
144    /// Core execution client functionality.
145    pub(super) core: ExecutionClientCore,
146    /// Configuration for the client.
147    pub(super) config: InteractiveBrokersExecutionClientConfig,
148    /// Raw IB account code sent to TWS, derived once from the configured `account_id`.
149    pub(super) ib_account: Ustr,
150    /// Instrument provider.
151    pub(super) instrument_provider: Arc<InteractiveBrokersInstrumentProvider>,
152    /// Connection state.
153    pub(super) is_connected: Arc<AtomicBool>,
154    /// IB API client (shared per host/port/client_id when both data and execution connect).
155    pub(super) ib_client: Option<SharedClientHandle>,
156    /// Tasks serving execution commands.
157    pub(super) pending_tasks: TaskGroup,
158    /// Tasks serving the active connection session.
159    pub(super) session_tasks: TaskGroup,
160    /// Order ID counter.
161    pub(super) next_order_id: Arc<Mutex<i32>>,
162    /// Serializes order submissions so TWS receives monotonically increasing order IDs.
163    pub(super) order_submit_lock: Arc<AsyncMutex<()>>,
164    /// Per-order state and indexes.
165    pub(super) orders: OrderTracker,
166    /// Commission cache by execution ID (to merge with fill reports).
167    pub(super) commission_cache: Arc<Mutex<CommissionCache>>,
168    /// Execution cache by execution ID while awaiting commission reports.
169    pub(super) pending_execution_cache: Arc<Mutex<PendingExecutionCache>>,
170    /// Position tracker for detecting external position changes (e.g., option exercises).
171    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    /// Fills held for untracked orders while their details resolve; a key marks a resolution
195    /// in flight.
196    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        // A duplicate member shares its raw order ID with another physical order, so
242        // only its permanent ID identifies it
243        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    /// Quantity, price, and trigger price of the last `OrderUpdated` sent from an openOrder.
328    pub(super) last_update: Option<(Quantity, Option<Price>, Option<Price>)>,
329}
330
331/// Spread fills waiting for their leg fills, so a spread fill is the last event of its group.
332///
333/// IB reports the combo execution before the leg executions, and leg fills carry the account
334/// changes, so a spread fill only goes out once the legs cover its quantity.
335#[derive(Clone, Debug, Default)]
336pub(super) struct HeldSpreadFills {
337    pub(super) fills: std::collections::VecDeque<(tokio::time::Instant, OrderFilled)>,
338    /// Leg quantity sent but not yet matched to a spread fill.
339    pub(super) leg_quantities: AHashMap<InstrumentId, f64>,
340}
341
342/// Requested modify values in IB units, kept until an openOrder reflects them or the
343/// modify is rejected, so unrelated openOrder refreshes cannot clear the pending flag.
344#[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    /// Requested trailing stop trigger; `None` leaves the venue-moved trigger unchecked.
350    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    /// Creates a new [`InteractiveBrokersExecutionClient`].
428    ///
429    /// # Errors
430    ///
431    /// Returns an error if client creation fails.
432    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                &currency,
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    // IB refuses every order ID below the client's next valid ID (error 103). When that ID lies
648    // above the client's partition no ID in it can be used, so this fails instead of mapping
649    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    // IB refuses an order ID at or below one the same client already used (error 103). A next
682    // valid ID below the client's partition says nothing about this client's IDs, so the start
683    // also clears the highest ID this client has in orders IB lists
684    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    /// Gets the next valid order ID from IB.
698    ///
699    /// # Errors
700    ///
701    /// Returns an error if getting the next order ID fails.
702    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    // IB lists completed orders for a limited period only. A completed order's API order ID is on
821    // the order itself because its top-level order ID is unset. A snapshot that fails, drops
822    // records, or does not reach IB's end marker by the deadline is an error, since a missing
823    // order could hold the highest ID
824    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// Implementation of ExecutionClient trait
913#[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    // IB uses a broker venue for the client while routing exchange-MIC instruments;
932    // contract transformation remains the authority for actual venue support.
933    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        // Start is handled by connect() for live clients
974        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        // Initialize provider and load instruments from cache/config if configured
1086        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        // Get initial next order ID (uses self.ib_client internally)
1110        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        // Start order update subscription (uses self.ib_client internally)
1145        log::debug!("Starting IB order update stream");
1146        self.start_order_updates().await?;
1147
1148        // Subscribe to account summary and keep it open so the account state follows the broker
1149        // Wait for the initial account summary to load before proceeding
1150        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        // Initialize position tracking with existing positions
1167        // This avoids processing duplicates from execDetails
1168        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        // Subscribe to PnL updates
1190        let client_for_pnl = Arc::clone(&client); // Clone Arc
1191
1192        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        // Subscribe to position updates for option exercise tracking if enabled
1205        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        // IB `reqExecutions` and completed orders are session-scoped, so without a
1406        // configured lookback the actual venue bound is unknown and the report set
1407        // cannot be claimed complete.
1408        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                            &currency,
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                    // The order is absent from open orders, so any unfilled remainder
1710                    // was cancelled at the venue. Quantize to the instrument's size
1711                    // precision so float dust from the f64 cumulative quantity cannot
1712                    // register as a phantom remainder after a full fill.
1713                    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        // Cancel each order in the batch
2008        for cancel_cmd in cmd.cancels {
2009            self.cancel_order(cancel_cmd)?;
2010        }
2011
2012        Ok(())
2013    }
2014
2015    // Reconciliation creates orders a previous session left working at IB; without tracking,
2016    // their IB status updates are dropped as untracked.
2017    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            // A filled order awaiting its deferred fills is still open in the cache but no
2067            // longer listed by IB; cancels resolve `PERM-` IDs on demand either way
2068            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    // Captures the thread-local exec event sender here so tokio worker threads, which have no
2089    // sender or message bus of their own, can still deliver account states.
2090    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    // Returns an error while disconnected, so the report hooks fall back to the inline path
2111    // that reports the same error.
2112    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    // Selects open orders for the instrument and account from the cache, the tracker, and
2161    // incarnation groups, keeping only the requested side when one is set
2162    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)] // Event construction preserves the query context.
2234    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        // Command actor IDs identify the requester and are not ownership evidence.
2388        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    /// Handles a cancel order asynchronously.
2401    ///
2402    /// # Errors
2403    ///
2404    /// Returns an error if broker order resolution or identity caching fails.
2405    #[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        // IB's terminal status can evict an order while its execution details still wait for
2495        // the commission report, so a cancel sent in that gap has no pending state to record
2496        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        // A strategy cancel already published PENDING_CANCEL, so only the tracking records it
2505        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)] // Resolution needs the original cancel context.
2530    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        // Get all IB order selectors first, then drop the guard before awaiting
2750        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        // Now cancel each order (guard is dropped, so we can await)
2772        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    // Marks a tracked order as pending cancel, returning its identity when it was not already.
2863    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}