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