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