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