1#[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#[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: ExecutionClientCore,
128 config: InteractiveBrokersExecClientConfig,
130 instrument_provider: Arc<InteractiveBrokersInstrumentProvider>,
132 is_connected: AtomicBool,
134 ib_client: Option<SharedClientHandle>,
136 pending_tasks: Mutex<Vec<JoinHandle<()>>>,
138 next_order_id: Arc<Mutex<i32>>,
140 order_submit_lock: Arc<AsyncMutex<()>>,
142 order_update_handle: Mutex<Option<JoinHandle<()>>>,
144 order_id_map: Arc<Mutex<AHashMap<ClientOrderId, i32>>>,
146 venue_order_id_map: Arc<Mutex<AHashMap<i32, ClientOrderId>>>,
148 commission_cache: Arc<Mutex<AHashMap<String, (f64, String)>>>,
150 instrument_id_map: Arc<Mutex<AHashMap<i32, InstrumentId>>>,
152 trader_id_map: Arc<Mutex<AHashMap<i32, TraderId>>>,
154 strategy_id_map: Arc<Mutex<AHashMap<i32, StrategyId>>>,
156 spread_fill_tracking: Arc<Mutex<AHashMap<ClientOrderId, ahash::AHashSet<String>>>>,
159 position_tracker: PositionTracker,
161 order_avg_prices: Arc<Mutex<AHashMap<ClientOrderId, Price>>>,
164 pending_combo_fills: Arc<Mutex<AHashMap<ClientOrderId, VecDeque<PendingComboFill>>>>,
166 pending_combo_fill_avgs: Arc<Mutex<AHashMap<ClientOrderId, VecDeque<(Decimal, Price)>>>>,
168 order_fill_progress: Arc<Mutex<AHashMap<ClientOrderId, (Decimal, Decimal)>>>,
170 accepted_orders: Arc<Mutex<ahash::AHashSet<ClientOrderId>>>,
172 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 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 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 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 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#[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 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 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 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 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 log::debug!("Starting IB order update stream");
661 self.start_order_updates().await?;
662
663 let client_for_account = Arc::clone(client);
666 let account_id = self.core.account_id;
667 let _exec_client_core = self.core.clone(); 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 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, 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 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 let client_for_pnl = Arc::clone(client); 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 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 self.abort_pending_tasks();
757
758 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 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 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 if let Some(filter_id) = cmd.instrument_id {
852 if instrument_id != filter_id {
853 continue;
854 }
855 }
856
857 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, perm_id: data.order.perm_id,
868 parent_id: 0, last_fill_price: None, client_id: data.order.client_id,
871 why_held: String::new(), market_cap_price: None, },
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 }
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 let account_code = self.core.account_id.to_string();
1016
1017 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 while let Some(position_result) = subscription.next().await {
1119 match position_result {
1120 Ok(PositionUpdate::Position(position)) => {
1121 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 if let Some(filter_id) = cmd.instrument_id
1154 && instrument_id != filter_id
1155 {
1156 continue;
1157 }
1158
1159 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 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_init, None, None, avg_px_open,
1189 );
1190
1191 reports.push(report);
1192 }
1193 Ok(PositionUpdate::PositionEnd) => {
1194 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 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 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 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 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 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, Some(&cmd.instrument_id), None, None, None, )
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 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, ) {
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 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 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 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";