1use std::{
19 collections::HashMap,
20 fmt::Debug,
21 sync::{
22 Arc, Mutex,
23 atomic::{AtomicBool, Ordering},
24 },
25 time::Duration,
26};
27
28use ahash::AHashMap;
29use anyhow::Context;
30use ibapi::{
31 contracts::{Contract, Currency as IBCurrency, Exchange as IBExchange, SecurityType, Symbol},
32 market_data::IgnoreSize,
33 prelude::{StreamExt, SubscriptionItemStreamExt},
34};
35use nautilus_common::{
36 clients::{DataClient, execute_bar_subscriptions, execute_instrument_subscriptions},
37 live::{runner::get_data_event_sender, sender::EventSender},
38 messages::{
39 DataEvent, DataResponse,
40 data::{
41 BarsResponse, InstrumentResponse, InstrumentsResponse, QuotesResponse, RequestBars,
42 RequestInstrument, RequestInstruments, RequestQuotes, RequestTrades, SubscribeBars,
43 SubscribeBookDeltas, SubscribeIndexPrices, SubscribeOptionGreeks, SubscribeQuotes,
44 SubscribeTrades, TradesResponse, UnsubscribeBars, UnsubscribeBookDeltas,
45 UnsubscribeIndexPrices, UnsubscribeOptionGreeks, UnsubscribeQuotes, UnsubscribeTrades,
46 },
47 },
48};
49use nautilus_core::{
50 UnixNanos,
51 params::Params,
52 time::{AtomicTime, get_atomic_clock_realtime},
53};
54use nautilus_live::task::{TaskGroup, TaskJoinOutcome, TaskSlot, finish_task};
55use nautilus_model::{
56 data::BarType,
57 enums::BookType,
58 identifiers::{ClientId, InstrumentId, Venue},
59 instruments::{Instrument, any::InstrumentAny},
60};
61use tokio_util::sync::CancellationToken;
62
63use super::{
64 cache::{OptionGreeksCache, QuoteCache},
65 convert::{
66 IbPriceConverter, bar_request_segments, bar_type_to_ib_bar_size,
67 calculate_duration_segments, extend_historical_tick_batch, ib_bar_to_nautilus_bar,
68 jiff_to_ib_datetime, price_type_to_ib_realtime_what_to_show_for_security,
69 price_type_to_ib_what_to_show_for_security, retain_historical_ticks_in_range,
70 should_continue_historical_tick_pagination,
71 },
72 parse::log_tick_parse_error,
73 streams::{
74 DataFarmConnectionState, StreamConfig, handle_historical_bars_subscription,
75 handle_index_price_subscription, handle_market_depth_subscription,
76 handle_option_greeks_subscription, handle_quote_subscription,
77 handle_realtime_bars_subscription, handle_tick_by_tick_quote_subscription,
78 handle_trade_subscription, monitor_data_farm_notices,
79 },
80};
81use crate::{
82 common::{
83 consts::IB_VENUE,
84 contracts::parse_contract_from_json,
85 shared_client::SharedClientHandle,
86 spreads::{is_spread_instrument_id, parse_spread_instrument_id_to_legs},
87 symbology::is_crypto_contract,
88 },
89 config::{InteractiveBrokersDataClientConfig, MarketDataType},
90 data_types::register_ib_custom_data,
91 providers::instruments::InteractiveBrokersInstrumentProvider,
92};
93
94#[cfg_attr(
100 feature = "python",
101 pyo3::pyclass(module = "nautilus_trader.adapters.interactive_brokers")
102)]
103pub struct InteractiveBrokersDataClient {
104 client_id: ClientId,
105 config: InteractiveBrokersDataClientConfig,
106 instrument_provider: Arc<InteractiveBrokersInstrumentProvider>,
107 is_connected: Arc<AtomicBool>,
108 cancellation_token: CancellationToken,
109 session_tasks: TaskGroup,
110 command_tasks: TaskGroup,
111 data_sender: EventSender<DataEvent>,
112 subscriptions: Arc<Mutex<AHashMap<SubscriptionKey, SubscriptionInfo>>>,
113 option_greeks_subscriptions: Arc<Mutex<AHashMap<InstrumentId, CancellationToken>>>,
114 quote_cache: Arc<tokio::sync::Mutex<QuoteCache>>,
115 option_greeks_cache: Arc<tokio::sync::Mutex<OptionGreeksCache>>,
116 clock: &'static AtomicTime,
117 ib_client: Option<SharedClientHandle>,
118 last_bars: Arc<tokio::sync::Mutex<AHashMap<String, ibapi::market_data::realtime::Bar>>>,
119 bar_timeout_tasks: Arc<tokio::sync::Mutex<AHashMap<String, TaskSlot<()>>>>,
120 data_farm_state: Arc<DataFarmConnectionState>,
121}
122
123#[derive(Debug)]
125struct SubscriptionInfo {
126 cancellation_token: CancellationToken,
128 id: u64,
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
134enum SubscriptionType {
135 Quotes,
137 IndexPrices,
139 Trades,
141 Bars(BarType),
143 BookDeltas,
145}
146
147type SubscriptionKey = (InstrumentId, SubscriptionType);
148
149static SUBSCRIPTION_ID_SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
150
151fn next_subscription_id() -> u64 {
152 SUBSCRIPTION_ID_SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
153}
154
155struct SubscriptionGuard {
159 subscriptions: Arc<Mutex<AHashMap<SubscriptionKey, SubscriptionInfo>>>,
160 key: SubscriptionKey,
161 id: u64,
162}
163
164impl SubscriptionGuard {
165 fn release(self) {
166 }
169}
170
171impl Drop for SubscriptionGuard {
172 fn drop(&mut self) {
173 if let Ok(mut subscriptions) = self.subscriptions.lock()
174 && subscriptions
175 .get(&self.key)
176 .is_some_and(|info| info.id == self.id)
177 {
178 subscriptions.remove(&self.key);
179 }
180 }
181}
182
183fn lock<T>(mutex: &Mutex<T>) -> anyhow::Result<std::sync::MutexGuard<'_, T>> {
184 mutex
185 .lock()
186 .map_err(|_| anyhow::anyhow!("Failed to lock IB data client state"))
187}
188
189fn parse_start_ns(params: Option<&nautilus_core::Params>) -> Option<UnixNanos> {
190 params
191 .and_then(|params| params.get_u64("start_ns"))
192 .or_else(|| {
193 params
194 .and_then(|params| params.get_str("start_ns"))
195 .and_then(|value| value.parse::<u64>().ok())
196 })
197 .map(UnixNanos::from)
198}
199
200fn parse_bool_param_value(value: &str) -> bool {
201 matches!(value, "true" | "True" | "1")
202}
203
204fn params_to_string_filters(params: Option<&Params>) -> Option<HashMap<String, String>> {
205 let filters: HashMap<String, String> = params?
206 .iter()
207 .filter_map(|(key, value)| value.as_str().map(|value| (key.clone(), value.to_string())))
208 .collect();
209 (!filters.is_empty()).then_some(filters)
210}
211
212fn datetime_to_unix_nanos(dt: jiff::Timestamp) -> UnixNanos {
213 UnixNanos::from(u64::try_from(dt.as_nanosecond()).unwrap_or_default())
214}
215
216fn request_trading_hours(use_regular_trading_hours: bool) -> ibapi::market_data::TradingHours {
217 if use_regular_trading_hours {
218 ibapi::market_data::TradingHours::Regular
219 } else {
220 ibapi::market_data::TradingHours::Extended
221 }
222}
223
224const HISTORICAL_TICK_DEFAULT_LIMIT: usize = 10_000;
225
226impl InteractiveBrokersDataClient {
227 pub fn new(
233 client_id: ClientId,
234 config: InteractiveBrokersDataClientConfig,
235 instrument_provider: Arc<InteractiveBrokersInstrumentProvider>,
236 ) -> anyhow::Result<Self> {
237 let clock = get_atomic_clock_realtime();
238 config.validate()?;
239 register_ib_custom_data();
240 let data_sender = get_data_event_sender();
241
242 let session_tasks = TaskGroup::new();
243 let command_tasks = TaskGroup::new();
244
245 Ok(Self {
246 client_id,
247 config,
248 instrument_provider,
249 is_connected: Arc::new(AtomicBool::new(false)),
250 cancellation_token: session_tasks.cancellation_token(),
251 session_tasks,
252 command_tasks,
253 data_sender,
254 subscriptions: Arc::new(Mutex::new(AHashMap::new())),
255 option_greeks_subscriptions: Arc::new(Mutex::new(AHashMap::new())),
256 quote_cache: Arc::new(tokio::sync::Mutex::new(QuoteCache::new())),
257 option_greeks_cache: Arc::new(tokio::sync::Mutex::new(OptionGreeksCache::new())),
258 clock,
259 ib_client: None,
260 last_bars: Arc::new(tokio::sync::Mutex::new(AHashMap::new())),
261 bar_timeout_tasks: Arc::new(tokio::sync::Mutex::new(AHashMap::new())),
262 data_farm_state: Arc::new(DataFarmConnectionState::default()),
263 })
264 }
265
266 fn cancel_active_subscriptions(&self) -> anyhow::Result<()> {
267 {
268 let mut subscriptions = lock(&self.subscriptions)?;
269 for subscription in subscriptions.values() {
270 subscription.cancellation_token.cancel();
271 }
272
273 subscriptions.clear();
274 }
275
276 {
277 let mut subscriptions = lock(&self.option_greeks_subscriptions)?;
278 for cancellation_token in subscriptions.values() {
279 cancellation_token.cancel();
280 }
281
282 subscriptions.clear();
283 }
284
285 Ok(())
286 }
287
288 fn spawn_command<F>(&self, future: F)
289 where
290 F: std::future::Future<Output = ()> + Send + 'static,
291 {
292 if let Err(e) = self.command_tasks.spawn(future) {
293 tracing::warn!("Skipping IB data command after shutdown began: {e}");
294 }
295 }
296
297 async fn finish_tasks(&self) -> anyhow::Result<()> {
298 let (session_result, command_result) = tokio::join!(
299 self.session_tasks
300 .finish_shutdown(Duration::from_secs(1), Duration::from_secs(2)),
301 self.command_tasks
302 .finish_shutdown(Duration::from_secs(1), Duration::from_secs(2)),
303 );
304 self.finish_bar_timeout_tasks().await;
305 session_result.context("failed to finish IB data session tasks")?;
306 command_result.context("failed to finish IB data command tasks")?;
307 Ok(())
308 }
309
310 async fn prepare_task_groups(&mut self) -> anyhow::Result<()> {
311 if !self.session_tasks.is_open() || !self.command_tasks.is_open() {
312 self.session_tasks.begin_shutdown();
313 self.command_tasks.begin_shutdown();
314 self.finish_tasks().await?;
315 self.session_tasks
316 .start_generation()
317 .context("failed to start IB data session task generation")?;
318 self.command_tasks
319 .start_generation()
320 .context("failed to start IB data command task generation")?;
321 self.cancellation_token = self.session_tasks.cancellation_token();
322 }
323
324 Ok(())
325 }
326
327 async fn finish_bar_timeout_tasks(&self) {
328 let mut tasks = self.bar_timeout_tasks.lock().await;
329 let bar_types = tasks.keys().cloned().collect::<Vec<_>>();
330 for bar_type in bar_types {
331 let handle = tasks
332 .get_mut(&bar_type)
333 .expect("bar timeout task key collected from map");
334
335 match finish_task(handle, Duration::ZERO, Duration::from_secs(1)).await {
336 None => {}
337 Some(TaskJoinOutcome::Completed(()) | TaskJoinOutcome::Aborted) => {}
338 Some(TaskJoinOutcome::Failed(e)) => {
339 tracing::warn!("IB bar timeout task join failed: {e}");
340 }
341 Some(TaskJoinOutcome::Incomplete) => {
342 tracing::warn!("IB bar timeout task did not stop within one second");
343 }
344 }
345
346 if handle.is_none() {
347 tasks.remove(&bar_type);
348 }
349 }
350 }
351
352 #[allow(dead_code)] pub(crate) fn get_ib_client(&self) -> Option<&Arc<ibapi::Client>> {
356 self.ib_client.as_ref().map(|h| h.as_arc())
357 }
358
359 #[allow(dead_code)] pub(crate) fn instrument_provider(&self) -> Arc<InteractiveBrokersInstrumentProvider> {
362 Arc::clone(&self.instrument_provider)
363 }
364
365 pub async fn batch_load_instruments(
375 &self,
376 instrument_ids: Vec<InstrumentId>,
377 ) -> anyhow::Result<Vec<InstrumentId>> {
378 log::debug!(
379 "Batch loading {} IB instruments through data client",
380 instrument_ids.len()
381 );
382 let client = self
383 .ib_client
384 .as_ref()
385 .context("IB client not connected. Call connect() first")?;
386
387 let loaded = self
388 .instrument_provider
389 .batch_load(client, instrument_ids, None)
390 .await?;
391 log::debug!("Batch loaded {} IB instruments", loaded.len());
392 Ok(loaded)
393 }
394
395 pub async fn fetch_option_chain_by_range(
405 &self,
406 underlying_symbol: &str,
407 exchange: Option<&str>,
408 currency: Option<&str>,
409 expiry_min: Option<&str>,
410 expiry_max: Option<&str>,
411 ) -> anyhow::Result<usize> {
412 log::debug!(
413 "Fetching IB option chain by range (symbol={underlying_symbol}, exchange={exchange:?}, currency={currency:?}, expiry_min={expiry_min:?}, expiry_max={expiry_max:?})"
414 );
415 let client = self
416 .ib_client
417 .as_ref()
418 .context("IB client not connected. Call connect() first")?;
419
420 let underlying = Contract {
421 contract_id: 0,
422 symbol: Symbol::from(underlying_symbol.to_string()),
423 security_type: SecurityType::Stock,
424 last_trade_date_or_contract_month: String::new(),
425 strike: f64::MAX,
426 right: None,
427 multiplier: String::new(),
428 exchange: IBExchange::from(exchange.unwrap_or("SMART")),
429 currency: IBCurrency::from(currency.unwrap_or("USD")),
430 local_symbol: String::new(),
431 primary_exchange: IBExchange::from(""),
432 trading_class: String::new(),
433 include_expired: false,
434 security_id_type: None,
435 security_id: String::new(),
436 combo_legs_description: String::new(),
437 combo_legs: Vec::new(),
438 delta_neutral_contract: None,
439 issuer_id: String::new(),
440 description: String::new(),
441 last_trade_date: None,
442 };
443
444 let count = self
445 .instrument_provider
446 .fetch_option_chain_by_range(client, &underlying, expiry_min, expiry_max, None)
447 .await?;
448 log::debug!("Fetched {count} IB option instruments for {underlying_symbol}");
449 Ok(count)
450 }
451
452 pub async fn fetch_futures_chain(
462 &self,
463 symbol: &str,
464 exchange: Option<&str>,
465 currency: Option<&str>,
466 min_expiry_days: Option<u32>,
467 max_expiry_days: Option<u32>,
468 ) -> anyhow::Result<usize> {
469 log::debug!(
470 "Fetching IB futures chain (symbol={symbol}, exchange={exchange:?}, currency={currency:?}, min_days={min_expiry_days:?}, max_days={max_expiry_days:?})"
471 );
472 let client = self
473 .ib_client
474 .as_ref()
475 .context("IB client not connected. Call connect() first")?;
476
477 let count = self
478 .instrument_provider
479 .fetch_futures_chain(
480 client,
481 symbol,
482 exchange.unwrap_or(""),
483 currency.unwrap_or("USD"),
484 None,
485 false,
486 min_expiry_days,
487 max_expiry_days,
488 )
489 .await?;
490 log::debug!("Fetched {count} IB futures instruments for {symbol}");
491 Ok(count)
492 }
493
494 pub async fn fetch_bag_contract(
504 &self,
505 bag_contract: &ibapi::contracts::Contract,
506 ) -> anyhow::Result<usize> {
507 log::debug!(
508 "Fetching IB BAG contract details (contract_id={}, exchange={}, symbol={})",
509 bag_contract.contract_id,
510 bag_contract.exchange.as_str(),
511 bag_contract.symbol.as_str()
512 );
513 let client = self
514 .ib_client
515 .as_ref()
516 .context("IB client not connected. Call connect() first")?;
517
518 let count = self
519 .instrument_provider
520 .fetch_bag_contract(client, bag_contract)
521 .await?;
522 log::debug!("Fetched {count} BAG instruments");
523 Ok(count)
524 }
525}
526
527impl Debug for InteractiveBrokersDataClient {
528 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
529 f.debug_struct(stringify!(InteractiveBrokersDataClient))
530 .field("client_id", &self.client_id)
531 .field("config", &self.config)
532 .field("is_connected", &self.is_connected.load(Ordering::Relaxed))
533 .field("has_ib_client", &self.ib_client.is_some())
534 .finish_non_exhaustive()
535 }
536}
537
538#[async_trait::async_trait(?Send)]
539impl DataClient for InteractiveBrokersDataClient {
540 fn client_id(&self) -> ClientId {
541 self.client_id
542 }
543
544 fn venue(&self) -> Option<Venue> {
545 None
551 }
552
553 fn start(&mut self) -> anyhow::Result<()> {
554 tracing::info!(
555 client_id = %self.client_id,
556 "Starting Interactive Brokers data client"
557 );
558 Ok(())
559 }
560
561 fn stop(&mut self) -> anyhow::Result<()> {
562 tracing::info!(
563 "Stopping Interactive Brokers data client {id}",
564 id = self.client_id
565 );
566 self.session_tasks.begin_shutdown();
567 self.command_tasks.begin_shutdown();
568 self.cancel_active_subscriptions()?;
569 self.is_connected.store(false, Ordering::Relaxed);
570
571 self.clear_bar_tracking_state();
572
573 Ok(())
574 }
575
576 fn reset(&mut self) -> anyhow::Result<()> {
577 tracing::debug!(
578 "Resetting Interactive Brokers data client {id}",
579 id = self.client_id
580 );
581 self.cancel_active_subscriptions()?;
582 self.session_tasks.begin_shutdown();
583 self.command_tasks.begin_shutdown();
584 self.is_connected.store(false, Ordering::Relaxed);
585 self.clear_bar_tracking_state();
586 self.quote_cache = Arc::new(tokio::sync::Mutex::new(QuoteCache::new()));
587 self.option_greeks_cache = Arc::new(tokio::sync::Mutex::new(OptionGreeksCache::new()));
588 self.cancellation_token = CancellationToken::new();
589
590 Ok(())
591 }
592
593 fn dispose(&mut self) -> anyhow::Result<()> {
594 self.stop()
595 }
596
597 async fn connect(&mut self) -> anyhow::Result<()> {
598 tracing::debug!("Connecting Interactive Brokers data client...");
599
600 self.prepare_task_groups().await?;
601
602 let handle = crate::common::shared_client::get_or_connect(
603 &self.config.host,
604 self.config.port,
605 self.config.client_id,
606 self.config.connection_timeout,
607 self.config.max_reconnect_attempts,
608 )
609 .await
610 .context("Failed to connect to IB Gateway/TWS")?;
611
612 let client = Arc::clone(handle.as_arc());
613
614 tracing::info!(
615 "Connected to IB Gateway/TWS at {}:{} (client_id: {})",
616 self.config.host,
617 self.config.port,
618 self.config.client_id
619 );
620
621 let ib_data_type = (self.config.market_data_type != MarketDataType::Realtime)
623 .then(|| self.config.market_data_type.into());
624
625 if let Some(ib_data_type) = ib_data_type {
626 client
627 .switch_market_data_type(ib_data_type)
628 .await
629 .context("Failed to switch market data type")?;
630 tracing::info!("Set market data type to {:?}", self.config.market_data_type);
631 }
632
633 tracing::debug!("Initializing IB data instrument provider");
635
636 if let Err(e) = self
637 .instrument_provider
638 .initialize_with_client(client.as_ref())
639 .await
640 {
641 if !self.config.instrument_provider.load_ids.is_empty()
642 || !self.config.instrument_provider.load_contracts.is_empty()
643 {
644 return Err(e).context("Failed to load configured IB instruments on startup");
645 }
646
647 tracing::warn!("Failed to load instruments on startup: {}", e);
648 }
649
650 self.ib_client = Some(handle);
651
652 let data_farm_state = Arc::clone(&self.data_farm_state);
653 let is_connected = Arc::clone(&self.is_connected);
654 let cancellation_token = self.cancellation_token.child_token();
655
656 if let Err(e) = self.session_tasks.spawn(async move {
657 if let Err(e) = monitor_data_farm_notices(
658 client,
659 data_farm_state,
660 ib_data_type,
661 cancellation_token,
662 is_connected,
663 )
664 .await
665 {
666 tracing::warn!("IB data farm notice monitor stopped: {e:?}");
667 }
668 }) {
669 self.session_tasks.begin_shutdown();
670 self.command_tasks.begin_shutdown();
671 self.ib_client = None;
672
673 if let Err(teardown_error) = self.finish_tasks().await {
674 return Err(anyhow::Error::new(e)
675 .context(format!("IB data startup teardown failed: {teardown_error}")));
676 }
677
678 return Err(anyhow::Error::new(e).context("failed to register IB data farm monitor"));
679 }
680
681 self.is_connected.store(true, Ordering::Relaxed);
682
683 let instrument_count = self.instrument_provider.count();
684 if instrument_count > 0 {
685 tracing::debug!(
686 "Data client connected with {} instruments in provider cache",
687 instrument_count
688 );
689
690 for instrument in self.instrument_provider.get_all() {
691 if let Err(e) = self.data_sender.send(DataEvent::Data(instrument.into())) {
692 tracing::warn!("Failed to publish startup-loaded instrument: {e}");
693 break;
694 }
695 }
696 }
697
698 tracing::info!("Connected Interactive Brokers data client");
699 Ok(())
700 }
701
702 async fn disconnect(&mut self) -> anyhow::Result<()> {
703 tracing::debug!("Disconnecting Interactive Brokers data client...");
704
705 self.session_tasks.begin_shutdown();
706 self.command_tasks.begin_shutdown();
707 self.cancel_active_subscriptions()?;
708 self.ib_client = None;
709 let tasks_result = self.finish_tasks().await;
710 self.is_connected.store(false, Ordering::Relaxed);
711 tracing::info!("Disconnected Interactive Brokers data client");
712 tasks_result
713 }
714
715 fn is_connected(&self) -> bool {
716 self.is_connected.load(Ordering::Relaxed)
717 }
718
719 fn is_disconnected(&self) -> bool {
720 !self.is_connected()
721 }
722
723 fn subscribe_quotes(&mut self, cmd: SubscribeQuotes) -> anyhow::Result<()> {
725 execute_instrument_subscriptions(&cmd.instrument_ids, |instrument_id| {
726 self.subscribe_quotes_member(instrument_id, &cmd)
727 })
728 }
729
730 fn subscribe_index_prices(&mut self, cmd: SubscribeIndexPrices) -> anyhow::Result<()> {
731 execute_instrument_subscriptions(&cmd.instrument_ids, |instrument_id| {
732 tracing::debug!("Subscribing to index prices for {}", instrument_id);
733
734 if self.has_subscription(instrument_id, SubscriptionType::IndexPrices)? {
735 tracing::debug!(
736 "Index price subscription already active for {}",
737 instrument_id
738 );
739 return Ok(());
740 }
741
742 let client = self
743 .ib_client
744 .as_ref()
745 .context("IB client not connected. Call connect() first")?;
746
747 let instrument = self
748 .instrument_provider
749 .find(&instrument_id)
750 .context(format!(
751 "Instrument {} not found in provider",
752 instrument_id
753 ))?;
754
755 let contract = self
756 .instrument_provider
757 .resolve_contract_for_instrument(instrument_id)
758 .context("Failed to convert instrument_id to IB contract")?;
759
760 if !matches!(contract.security_type, SecurityType::Index) {
761 tracing::warn!(
762 "Index price subscription not supported for security type {:?} on {}",
763 contract.security_type,
764 instrument_id
765 );
766 return Ok(());
767 }
768
769 let prices = IbPriceConverter::for_instrument(
770 &instrument,
771 self.instrument_provider.get_price_magnifier(&instrument_id),
772 );
773 let data_sender = self.data_sender.clone();
774 let clock = self.clock;
775
776 let subscription_token = self.cancellation_token.child_token();
777
778 let subscription_id = next_subscription_id();
779 lock(&self.subscriptions)?.insert(
780 (instrument_id, SubscriptionType::IndexPrices),
781 SubscriptionInfo {
782 cancellation_token: subscription_token.clone(),
783 id: subscription_id,
784 },
785 );
786
787 let guard = SubscriptionGuard {
788 subscriptions: Arc::clone(&self.subscriptions),
789 key: (instrument_id, SubscriptionType::IndexPrices),
790 id: subscription_id,
791 };
792
793 let client_clone = client.as_arc().clone();
794 let data_farm_state = Arc::clone(&self.data_farm_state);
795 let stream_config = StreamConfig::new(self.client_id, &self.config);
796
797 let task = async move {
798 if let Err(e) = handle_index_price_subscription(
799 client_clone,
800 contract,
801 instrument_id,
802 prices,
803 data_sender,
804 clock,
805 subscription_token,
806 data_farm_state,
807 stream_config,
808 )
809 .await
810 {
811 tracing::error!(
812 "Index price subscription error for {}: {:?}",
813 instrument_id,
814 e
815 );
816 }
817
818 guard.release();
819 };
820
821 self.session_tasks
822 .spawn(task)
823 .context("failed to register IB data subscription task")?;
824
825 tracing::debug!("Index price subscription started for {}", instrument_id);
826 Ok(())
827 })
828 }
829
830 fn subscribe_option_greeks(&mut self, cmd: SubscribeOptionGreeks) -> anyhow::Result<()> {
831 execute_instrument_subscriptions(&cmd.instrument_ids, |instrument_id| {
832 tracing::debug!("Subscribing to option greeks for {}", instrument_id);
833
834 if lock(&self.option_greeks_subscriptions)?.contains_key(&instrument_id) {
835 tracing::debug!(
836 "Option greeks subscription already active for {}",
837 instrument_id
838 );
839 return Ok(());
840 }
841
842 let client = self
843 .ib_client
844 .as_ref()
845 .context("IB client not connected. Call connect() first")?;
846
847 let instrument = self
848 .instrument_provider
849 .find(&instrument_id)
850 .context(format!(
851 "Instrument {} not found in provider",
852 instrument_id
853 ))?;
854
855 if !matches!(
856 instrument,
857 InstrumentAny::OptionContract(_)
858 | InstrumentAny::FuturesContract(_)
859 | InstrumentAny::CryptoOption(_)
860 ) && !matches!(
861 self.instrument_provider
862 .resolve_contract_for_instrument(instrument_id)?
863 .security_type,
864 SecurityType::Option | SecurityType::FuturesOption
865 ) {
866 tracing::warn!(
867 "Option greeks subscription is only supported for option instruments: {}",
868 instrument_id
869 );
870 return Ok(());
871 }
872
873 let contract = self
874 .instrument_provider
875 .resolve_contract_for_instrument(instrument_id)
876 .context("Failed to convert instrument_id to IB contract")?;
877
878 let data_sender = self.data_sender.clone();
879 let option_greeks_cache = Arc::clone(&self.option_greeks_cache);
880 let clock = self.clock;
881 let subscription_token = self.cancellation_token.child_token();
882 let subscription_token_clone = subscription_token.clone();
883 let client_clone = client.as_arc().clone();
884 let data_farm_state = Arc::clone(&self.data_farm_state);
885 let stream_config = StreamConfig::new(self.client_id, &self.config);
886
887 let task = async move {
888 if let Err(e) = handle_option_greeks_subscription(
889 client_clone,
890 contract,
891 instrument_id,
892 data_sender,
893 option_greeks_cache,
894 clock,
895 subscription_token_clone,
896 data_farm_state,
897 stream_config,
898 )
899 .await
900 {
901 tracing::error!(
902 "Option greeks subscription error for {}: {:?}",
903 instrument_id,
904 e
905 );
906 }
907 };
908
909 self.session_tasks
910 .spawn(task)
911 .context("failed to register IB data subscription task")?;
912
913 let mut subscriptions = lock(&self.option_greeks_subscriptions)?;
914 subscriptions.insert(instrument_id, subscription_token);
915
916 tracing::debug!("Option greeks subscription started for {}", instrument_id);
917 Ok(())
918 })
919 }
920
921 fn unsubscribe_quotes(&mut self, cmd: &UnsubscribeQuotes) -> anyhow::Result<()> {
922 execute_instrument_subscriptions(&cmd.instrument_ids, |instrument_id| {
923 tracing::debug!("Unsubscribing from quotes for {instrument_id}");
924
925 let mut subscriptions = lock(&self.subscriptions)?;
926 if let Some(sub_info) = subscriptions.remove(&(instrument_id, SubscriptionType::Quotes))
927 {
928 sub_info.cancellation_token.cancel();
929 tracing::debug!("Unsubscribed from quotes for {instrument_id}");
930 } else {
931 tracing::warn!("No active quote subscription found for {instrument_id}");
932 }
933
934 {
936 }
939
940 Ok(())
941 })
942 }
943
944 fn unsubscribe_index_prices(&mut self, cmd: &UnsubscribeIndexPrices) -> anyhow::Result<()> {
945 execute_instrument_subscriptions(&cmd.instrument_ids, |instrument_id| {
946 tracing::debug!("Unsubscribing from index prices for {}", instrument_id);
947
948 let mut subscriptions = lock(&self.subscriptions)?;
949
950 if let Some(sub_info) =
951 subscriptions.remove(&(instrument_id, SubscriptionType::IndexPrices))
952 {
953 sub_info.cancellation_token.cancel();
954 tracing::debug!("Unsubscribed from index prices for {}", instrument_id);
955 } else {
956 tracing::warn!(
957 "No active index price subscription found for {}",
958 instrument_id
959 );
960 }
961
962 Ok(())
963 })
964 }
965
966 fn unsubscribe_option_greeks(&mut self, cmd: &UnsubscribeOptionGreeks) -> anyhow::Result<()> {
967 execute_instrument_subscriptions(&cmd.instrument_ids, |instrument_id| {
968 tracing::debug!("Unsubscribing from option greeks for {}", instrument_id);
969
970 let mut subscriptions = lock(&self.option_greeks_subscriptions)?;
971 if let Some(subscription_token) = subscriptions.remove(&instrument_id) {
972 subscription_token.cancel();
973 tracing::debug!("Unsubscribed from option greeks for {}", instrument_id);
974 } else {
975 tracing::warn!(
976 "No active option greeks subscription found for {}",
977 instrument_id
978 );
979 }
980
981 Ok(())
982 })
983 }
984
985 fn subscribe_trades(&mut self, cmd: SubscribeTrades) -> anyhow::Result<()> {
986 execute_instrument_subscriptions(&cmd.instrument_ids, |instrument_id| {
987 tracing::debug!("Subscribing to trades for {}", instrument_id);
988
989 if self.has_subscription(instrument_id, SubscriptionType::Trades)? {
990 tracing::debug!("Trade subscription already active for {}", instrument_id);
991 return Ok(());
992 }
993
994 let client = self
995 .ib_client
996 .as_ref()
997 .context("IB client not connected. Call connect() first")?;
998
999 let instrument = self
1001 .instrument_provider
1002 .find(&instrument_id)
1003 .context(format!(
1004 "Instrument {} not found in provider",
1005 instrument_id
1006 ))?;
1007
1008 if matches!(instrument, InstrumentAny::CurrencyPair(_)) {
1010 tracing::error!(
1011 "Interactive Brokers does not support trades for CurrencyPair instruments: {}",
1012 instrument_id
1013 );
1014 return Ok(());
1015 }
1016
1017 let prices = IbPriceConverter::for_instrument(
1018 &instrument,
1019 self.instrument_provider.get_price_magnifier(&instrument_id),
1020 );
1021 let size_precision = instrument.size_precision();
1022
1023 let contract = self
1025 .instrument_provider
1026 .resolve_contract_for_instrument(instrument_id)
1027 .context("Failed to convert instrument_id to IB contract")?;
1028
1029 let data_sender = self.data_sender.clone();
1030 let clock = self.clock;
1031
1032 let subscription_token = self.cancellation_token.child_token();
1034
1035 let subscription_id = next_subscription_id();
1036 lock(&self.subscriptions)?.insert(
1037 (instrument_id, SubscriptionType::Trades),
1038 SubscriptionInfo {
1039 cancellation_token: subscription_token.clone(),
1040 id: subscription_id,
1041 },
1042 );
1043
1044 let guard = SubscriptionGuard {
1045 subscriptions: Arc::clone(&self.subscriptions),
1046 key: (instrument_id, SubscriptionType::Trades),
1047 id: subscription_id,
1048 };
1049
1050 let client_clone = client.as_arc().clone();
1052 let data_farm_state = Arc::clone(&self.data_farm_state);
1053 let stream_config = StreamConfig::new(self.client_id, &self.config);
1054
1055 let task = async move {
1056 if let Err(e) = handle_trade_subscription(
1057 client_clone,
1058 contract,
1059 instrument_id,
1060 prices,
1061 size_precision,
1062 data_sender,
1063 clock,
1064 subscription_token,
1065 data_farm_state,
1066 stream_config,
1067 )
1068 .await
1069 {
1070 tracing::error!("Trade subscription error for {}: {:?}", instrument_id, e);
1071 }
1072
1073 guard.release();
1074 };
1075
1076 self.session_tasks
1077 .spawn(task)
1078 .context("failed to register IB data subscription task")?;
1079
1080 tracing::debug!("Trade subscription started for {}", instrument_id);
1081 Ok(())
1082 })
1083 }
1084
1085 fn unsubscribe_trades(&mut self, cmd: &UnsubscribeTrades) -> anyhow::Result<()> {
1086 execute_instrument_subscriptions(&cmd.instrument_ids, |instrument_id| {
1087 tracing::debug!("Unsubscribing from trades for {}", instrument_id);
1088
1089 let mut subscriptions = lock(&self.subscriptions)?;
1090 if let Some(sub_info) = subscriptions.remove(&(instrument_id, SubscriptionType::Trades))
1091 {
1092 sub_info.cancellation_token.cancel();
1093 tracing::debug!("Unsubscribed from trades for {}", instrument_id);
1094 } else {
1095 tracing::warn!("No active trade subscription found for {}", instrument_id);
1096 }
1097
1098 Ok(())
1099 })
1100 }
1101
1102 fn subscribe_bars(&mut self, cmd: SubscribeBars) -> anyhow::Result<()> {
1103 execute_bar_subscriptions(&cmd.bar_types, |bar_type| {
1104 let cmd = cmd.with_bar_types(bar_type);
1105 tracing::debug!("Subscribing to bars for {}", bar_type);
1106
1107 let client = self
1108 .ib_client
1109 .as_ref()
1110 .context("IB client not connected. Call connect() first")?;
1111
1112 let instrument_id = bar_type.instrument_id();
1114 if self.has_subscription(instrument_id, SubscriptionType::Bars(bar_type))? {
1115 tracing::debug!("Bar subscription already active for {}", bar_type);
1116 return Ok(());
1117 }
1118
1119 let instrument = self
1120 .instrument_provider
1121 .find(&instrument_id)
1122 .context(format!("Instrument {instrument_id} not found in provider"))?;
1123
1124 let prices = IbPriceConverter::for_instrument(
1125 &instrument,
1126 self.instrument_provider.get_price_magnifier(&instrument_id),
1127 );
1128 let size_precision = instrument.size_precision();
1129
1130 let contract = self
1132 .instrument_provider
1133 .resolve_contract_for_instrument(instrument_id)
1134 .context("Failed to convert instrument_id to IB contract")?;
1135
1136 let bar_type_str = bar_type.to_string();
1137 let data_sender = self.data_sender.clone();
1138 let clock = self.clock;
1139 let last_bars = Arc::clone(&self.last_bars);
1140 let bar_timeout_tasks = Arc::clone(&self.bar_timeout_tasks);
1141 let handle_revised_bars = self.config.handle_revised_bars;
1142 let use_rth = self.config.use_regular_trading_hours;
1143 let start_ns = parse_start_ns(cmd.params.as_ref());
1144 let is_crypto = is_crypto_contract(&contract);
1150
1151 let subscription_token = self.cancellation_token.child_token();
1153
1154 let subscription_id = next_subscription_id();
1155 lock(&self.subscriptions)?.insert(
1156 (instrument_id, SubscriptionType::Bars(bar_type)),
1157 SubscriptionInfo {
1158 cancellation_token: subscription_token.clone(),
1159 id: subscription_id,
1160 },
1161 );
1162
1163 let guard = SubscriptionGuard {
1164 subscriptions: Arc::clone(&self.subscriptions),
1165 key: (instrument_id, SubscriptionType::Bars(bar_type)),
1166 id: subscription_id,
1167 };
1168
1169 let client_clone = client.as_arc().clone();
1171 let data_farm_state = Arc::clone(&self.data_farm_state);
1172 let stream_config = StreamConfig::new(self.client_id, &self.config);
1173
1174 let task = async move {
1175 let result = if bar_type.spec().timedelta().as_secs() == 5 {
1176 handle_realtime_bars_subscription(
1177 client_clone,
1178 contract,
1179 bar_type,
1180 bar_type_str,
1181 instrument_id,
1182 price_type_to_ib_realtime_what_to_show_for_security(
1183 bar_type.spec().price_type,
1184 is_crypto,
1185 ),
1186 prices,
1187 size_precision,
1188 data_sender,
1189 clock,
1190 last_bars,
1191 bar_timeout_tasks,
1192 handle_revised_bars,
1193 use_rth,
1194 subscription_token,
1195 Arc::clone(&data_farm_state),
1196 stream_config,
1197 )
1198 .await
1199 } else {
1200 handle_historical_bars_subscription(
1201 client_clone,
1202 contract,
1203 bar_type,
1204 price_type_to_ib_what_to_show_for_security(
1205 bar_type.spec().price_type,
1206 is_crypto,
1207 ),
1208 prices,
1209 size_precision,
1210 use_rth,
1211 start_ns,
1212 data_sender,
1213 handle_revised_bars,
1214 clock,
1215 subscription_token,
1216 Arc::clone(&data_farm_state),
1217 stream_config,
1218 )
1219 .await
1220 };
1221
1222 if let Err(e) = result {
1223 tracing::error!("Bars subscription error for {}: {:?}", bar_type, e);
1224 }
1225
1226 guard.release();
1227 };
1228
1229 self.session_tasks
1230 .spawn(task)
1231 .context("failed to register IB data subscription task")?;
1232
1233 tracing::debug!("Real-time bars subscription started for {}", bar_type);
1234 Ok(())
1235 })
1236 }
1237
1238 fn unsubscribe_bars(&mut self, cmd: &UnsubscribeBars) -> anyhow::Result<()> {
1239 execute_bar_subscriptions(&cmd.bar_types, |bar_type| {
1240 tracing::debug!("Unsubscribing from bars for {}", bar_type);
1241
1242 let instrument_id = bar_type.instrument_id();
1243 let mut subscriptions = lock(&self.subscriptions)?;
1244
1245 if let Some(sub_info) =
1246 subscriptions.remove(&(instrument_id, SubscriptionType::Bars(bar_type)))
1247 {
1248 sub_info.cancellation_token.cancel();
1249 tracing::debug!("Unsubscribed from bars for {}", bar_type);
1250 } else {
1251 tracing::warn!("No active bar subscription found for {}", bar_type);
1252 }
1253
1254 Ok(())
1255 })
1256 }
1257
1258 fn subscribe_book_deltas(&mut self, cmd: SubscribeBookDeltas) -> anyhow::Result<()> {
1259 execute_instrument_subscriptions(&cmd.instrument_ids, |instrument_id| {
1260 let cmd = cmd.with_instrument_ids(instrument_id);
1261 tracing::debug!("Subscribing to book deltas for {}", instrument_id);
1262
1263 if self.has_subscription(instrument_id, SubscriptionType::BookDeltas)? {
1264 tracing::debug!(
1265 "Book delta subscription already active for {}",
1266 instrument_id
1267 );
1268 return Ok(());
1269 }
1270
1271 if cmd.book_type == BookType::L3_MBO {
1273 tracing::error!(
1274 "Cannot subscribe to order book deltas: L3_MBO data is not published by Interactive Brokers. Valid book types are L1_MBP, L2_MBP"
1275 );
1276 return Ok(());
1277 }
1278
1279 let client = self
1280 .ib_client
1281 .as_ref()
1282 .context("IB client not connected. Call connect() first")?;
1283
1284 let instrument = self
1286 .instrument_provider
1287 .find(&instrument_id)
1288 .context(format!(
1289 "Instrument {} not found in provider",
1290 instrument_id
1291 ))?;
1292
1293 let prices = IbPriceConverter::new(instrument.price_precision())
1295 .with_price_magnifier(self.instrument_provider.get_price_magnifier(&instrument_id));
1296 let size_precision = instrument.size_precision();
1297
1298 let contract = self
1300 .instrument_provider
1301 .resolve_contract_for_instrument(instrument_id)
1302 .context("Failed to convert instrument_id to IB contract")?;
1303
1304 let data_sender = self.data_sender.clone();
1305 let clock = self.clock;
1306
1307 let subscription_token = self.cancellation_token.child_token();
1309
1310 let depth_rows = cmd.depth.map_or(20, |d| d.get() as i32);
1312
1313 let is_smart_depth = cmd
1315 .params
1316 .as_ref()
1317 .and_then(|params| params.get_str("is_smart_depth"))
1318 .is_none_or(parse_bool_param_value);
1319
1320 let subscription_id = next_subscription_id();
1321 lock(&self.subscriptions)?.insert(
1322 (instrument_id, SubscriptionType::BookDeltas),
1323 SubscriptionInfo {
1324 cancellation_token: subscription_token.clone(),
1325 id: subscription_id,
1326 },
1327 );
1328
1329 let guard = SubscriptionGuard {
1330 subscriptions: Arc::clone(&self.subscriptions),
1331 key: (instrument_id, SubscriptionType::BookDeltas),
1332 id: subscription_id,
1333 };
1334
1335 let client_clone = client.as_arc().clone();
1337 let data_farm_state = Arc::clone(&self.data_farm_state);
1338 let stream_config = StreamConfig::new(self.client_id, &self.config);
1339
1340 let task = async move {
1341 if let Err(e) = handle_market_depth_subscription(
1342 client_clone,
1343 contract,
1344 instrument_id,
1345 prices,
1346 size_precision,
1347 depth_rows,
1348 is_smart_depth,
1349 data_sender,
1350 clock,
1351 subscription_token,
1352 data_farm_state,
1353 stream_config,
1354 )
1355 .await
1356 {
1357 tracing::error!(
1358 "Market depth subscription error for {}: {:?}",
1359 instrument_id,
1360 e
1361 );
1362 }
1363
1364 guard.release();
1365 };
1366
1367 self.session_tasks
1368 .spawn(task)
1369 .context("failed to register IB data subscription task")?;
1370
1371 tracing::debug!("Market depth subscription started for {}", instrument_id);
1372 Ok(())
1373 })
1374 }
1375
1376 fn unsubscribe_book_deltas(&mut self, cmd: &UnsubscribeBookDeltas) -> anyhow::Result<()> {
1377 execute_instrument_subscriptions(&cmd.instrument_ids, |instrument_id| {
1378 tracing::debug!("Unsubscribing from book deltas for {}", instrument_id);
1379
1380 let mut subscriptions = lock(&self.subscriptions)?;
1381
1382 if let Some(sub_info) =
1383 subscriptions.remove(&(instrument_id, SubscriptionType::BookDeltas))
1384 {
1385 sub_info.cancellation_token.cancel();
1386 tracing::debug!("Unsubscribed from book deltas for {}", instrument_id);
1387 } else {
1388 tracing::warn!(
1389 "No active book delta subscription found for {}",
1390 instrument_id
1391 );
1392 }
1393
1394 Ok(())
1395 })
1396 }
1397
1398 fn request_instrument(&self, cmd: RequestInstrument) -> anyhow::Result<()> {
1400 let instrument_id = cmd.single_instrument_id()?;
1401
1402 tracing::debug!("Requesting instrument: {instrument_id}");
1403
1404 if cmd.start.is_some() {
1405 tracing::warn!(
1406 "Requesting instrument {instrument_id} with specified `start` which has no effect"
1407 );
1408 }
1409
1410 if cmd.end.is_some() {
1411 tracing::warn!(
1412 "Requesting instrument {instrument_id} with specified `end` which has no effect"
1413 );
1414 }
1415
1416 let force_update = cmd
1418 .params
1419 .as_ref()
1420 .and_then(|params| params.get_str("force_instrument_update"))
1421 .is_some_and(parse_bool_param_value);
1422
1423 let instrument = if force_update || self.instrument_provider.find(&instrument_id).is_none()
1425 {
1426 let client = self
1428 .ib_client
1429 .as_ref()
1430 .context("IB client not connected. Call connect() first")?;
1431 let instrument_provider = Arc::clone(&self.instrument_provider);
1432 let data_sender = self.data_sender.clone();
1433 let clock = self.clock;
1434 let request_id = cmd.request_id;
1435 let client_id = cmd.client_id.unwrap_or(self.client_id);
1436 let params = cmd.params.clone();
1437 let start_nanos = cmd.start.map(datetime_to_unix_nanos);
1438 let end_nanos = cmd.end.map(datetime_to_unix_nanos);
1439
1440 let client_clone = client.as_arc().clone();
1441
1442 self.spawn_command(async move {
1443 let filters = params_to_string_filters(params.as_ref());
1444
1445 if let Err(e) = load_requested_instrument(
1446 &instrument_provider,
1447 &client_clone,
1448 instrument_id,
1449 force_update,
1450 filters,
1451 &data_sender,
1452 )
1453 .await
1454 {
1455 tracing::error!(
1456 "Failed to fetch contract details for {}: {:?}",
1457 instrument_id,
1458 e
1459 );
1460 return;
1461 }
1462
1463 if let Some(instrument) = instrument_provider.find(&instrument_id) {
1464 let response = DataResponse::Instrument(Box::new(InstrumentResponse::new(
1465 request_id,
1466 client_id,
1467 instrument_id,
1468 instrument,
1469 start_nanos,
1470 end_nanos,
1471 clock.get_time_ns(),
1472 params,
1473 )));
1474
1475 if let Err(e) = data_sender.send(DataEvent::Response(response)) {
1476 tracing::error!("Failed to send instrument response: {e}");
1477 }
1478 }
1479 });
1480
1481 return Ok(());
1483 } else {
1484 self.instrument_provider
1486 .find(&instrument_id)
1487 .context(format!("Instrument {instrument_id} not found in provider"))?
1488 };
1489
1490 let start_nanos = cmd.start.map(datetime_to_unix_nanos);
1491 let end_nanos = cmd.end.map(datetime_to_unix_nanos);
1492
1493 let response = DataResponse::Instrument(Box::new(InstrumentResponse::new(
1494 cmd.request_id,
1495 cmd.client_id.unwrap_or(self.client_id),
1496 instrument_id,
1497 instrument,
1498 start_nanos,
1499 end_nanos,
1500 self.clock.get_time_ns(),
1501 cmd.params,
1502 )));
1503
1504 if let Err(e) = self.data_sender.send(DataEvent::Response(response)) {
1505 tracing::error!("Failed to send instrument response: {e}");
1506 }
1507
1508 Ok(())
1509 }
1510
1511 fn request_instruments(&self, cmd: RequestInstruments) -> anyhow::Result<()> {
1512 tracing::debug!("Requesting all instruments for venue: {:?}", cmd.venue);
1513
1514 let client = self
1515 .ib_client
1516 .as_ref()
1517 .context("IB client not connected. Call connect() first")?;
1518
1519 let force_update = cmd
1521 .params
1522 .as_ref()
1523 .and_then(|params| params.get_str("force_instrument_update"))
1524 .is_some_and(parse_bool_param_value);
1525
1526 let mut contract_specs_to_load: Vec<serde_json::Value> = Vec::new();
1528
1529 if let Some(params) = &cmd.params
1530 && let Some(ib_contracts_value) = params.get("ib_contracts")
1531 {
1532 match ib_contracts_value {
1533 serde_json::Value::Array(contract_specs) => {
1534 tracing::debug!(
1535 "Parsed {} structured contract specs from ib_contracts",
1536 contract_specs.len()
1537 );
1538 contract_specs_to_load = contract_specs.clone();
1539 }
1540 serde_json::Value::String(ib_contracts_json_str) => {
1541 match serde_json::from_str::<serde_json::Value>(ib_contracts_json_str) {
1542 Ok(serde_json::Value::Array(contract_specs)) => {
1543 tracing::debug!(
1544 "Parsed {} contract specs from ib_contracts JSON",
1545 contract_specs.len()
1546 );
1547 log::debug!("Parsed ib_contracts payload: {ib_contracts_json_str}");
1548 contract_specs_to_load = contract_specs;
1549 }
1550 Ok(value) => {
1551 tracing::warn!(
1552 "Expected ib_contracts JSON array, received {}. Continuing without contracts",
1553 value
1554 );
1555 }
1556 Err(e) => {
1557 tracing::warn!(
1558 "Failed to parse ib_contracts JSON: {}. Continuing without contracts",
1559 e
1560 );
1561 }
1562 }
1563 }
1564 value => {
1565 tracing::warn!(
1566 "Expected ib_contracts array or JSON string, received {}. Continuing without contracts",
1567 value
1568 );
1569 }
1570 }
1571 }
1572
1573 let instrument_provider = Arc::clone(&self.instrument_provider);
1575 let client_clone = client.as_arc().clone();
1576 let data_sender = self.data_sender.clone();
1577 let clock = self.clock;
1578 let request_id = cmd.request_id;
1579 let client_id = cmd.client_id.unwrap_or(self.client_id);
1580 let venue = cmd.venue.unwrap_or(*IB_VENUE);
1581 let params = cmd.params.clone();
1582 let start_nanos = cmd.start.map(datetime_to_unix_nanos);
1583 let end_nanos = cmd.end.map(datetime_to_unix_nanos);
1584
1585 if !contract_specs_to_load.is_empty() || force_update {
1587 let contract_specs_to_load_clone = contract_specs_to_load;
1588 let return_loaded_only = !contract_specs_to_load_clone.is_empty();
1589
1590 self.spawn_command(async move {
1591 let mut loaded_instrument_ids = Vec::new();
1592
1593 if !contract_specs_to_load_clone.is_empty() {
1595 for contract_spec in contract_specs_to_load_clone {
1596 let contract =
1597 match parse_contract_from_json(&contract_spec)
1598 {
1599 Ok(contract) => contract,
1600 Err(e) => {
1601 tracing::warn!(
1602 "Failed to parse IB contract spec {:?}: {}",
1603 contract_spec,
1604 e
1605 );
1606 continue;
1607 }
1608 };
1609
1610 log::debug!(
1611 "Loading instrument from IB contract spec (sec_type={:?}, symbol={}, local_symbol={}, exchange={}, expiry={})",
1612 contract.security_type,
1613 contract.symbol.as_str(),
1614 contract.local_symbol.as_str(),
1615 contract.exchange.as_str(),
1616 contract.last_trade_date_or_contract_month.as_str()
1617 );
1618
1619 match instrument_provider
1620 .load_contract_spec(&client_clone, &contract, Some(&contract_spec))
1621 .await
1622 {
1623 Ok(mut instrument_ids) => {
1624 loaded_instrument_ids.append(&mut instrument_ids);
1625 }
1626 Err(e) => {
1627 tracing::warn!(
1628 "Failed to load IB contract spec {:?}: {}",
1629 contract_spec,
1630 e
1631 );
1632 }
1633 }
1634 }
1635 }
1636
1637 if force_update && !return_loaded_only {
1639 let all_instrument_ids: Vec<InstrumentId> = instrument_provider
1640 .get_all()
1641 .into_iter()
1642 .map(|inst| inst.id())
1643 .collect();
1644
1645 if !all_instrument_ids.is_empty()
1646 && let Ok(mut reloaded_ids) = instrument_provider
1647 .batch_load(&client_clone, all_instrument_ids, None)
1648 .await
1649 {
1650 loaded_instrument_ids.append(&mut reloaded_ids);
1651 }
1652 }
1653
1654 let instruments = if return_loaded_only {
1655 instrument_provider.find_all(&loaded_instrument_ids)
1656 } else {
1657 Vec::new()
1658 };
1659
1660 let instruments_count = instruments.len();
1661
1662 let response = DataResponse::Instruments(InstrumentsResponse::new(
1663 request_id,
1664 client_id,
1665 venue,
1666 instruments,
1667 start_nanos,
1668 end_nanos,
1669 clock.get_time_ns(),
1670 params,
1671 ));
1672
1673 if let Err(e) = data_sender.send(DataEvent::Response(response)) {
1674 tracing::error!("Failed to send instruments response: {e}");
1675 } else {
1676 tracing::debug!(
1677 "Successfully sent {} instruments response (loaded {} new instruments)",
1678 instruments_count,
1679 loaded_instrument_ids.len()
1680 );
1681 }
1682 });
1683 } else {
1684 let response = DataResponse::Instruments(InstrumentsResponse::new(
1685 cmd.request_id,
1686 cmd.client_id.unwrap_or(self.client_id),
1687 venue,
1688 Vec::new(),
1689 start_nanos,
1690 end_nanos,
1691 self.clock.get_time_ns(),
1692 cmd.params,
1693 ));
1694
1695 if let Err(e) = self.data_sender.send(DataEvent::Response(response)) {
1696 tracing::error!("Failed to send instruments response: {e}");
1697 } else {
1698 tracing::debug!("Successfully sent empty instruments response");
1699 }
1700 }
1701
1702 Ok(())
1703 }
1704
1705 fn request_quotes(&self, cmd: RequestQuotes) -> anyhow::Result<()> {
1706 let instrument_id = cmd.single_instrument_id()?;
1707
1708 tracing::debug!("Requesting quotes for {instrument_id}");
1709
1710 let client = self
1711 .ib_client
1712 .as_ref()
1713 .context("IB client not connected. Call connect() first")?;
1714
1715 let instrument = self
1717 .instrument_provider
1718 .find(&instrument_id)
1719 .context(format!("Instrument {instrument_id} not found in provider"))?;
1720
1721 let prices = IbPriceConverter::for_instrument(
1722 &instrument,
1723 self.instrument_provider.get_price_magnifier(&instrument_id),
1724 );
1725 let size_precision = instrument.size_precision();
1726
1727 let contract = self
1729 .instrument_provider
1730 .resolve_contract_for_instrument(instrument_id)
1731 .context("Failed to convert instrument_id to IB contract")?;
1732
1733 let limit = cmd
1734 .limit
1735 .map(|limit| limit.get())
1736 .or_else(|| cmd.start.is_none().then_some(HISTORICAL_TICK_DEFAULT_LIMIT));
1737 let number_of_ticks = limit.unwrap_or(1000).min(1000) as i32;
1738
1739 let data_sender = self.data_sender.clone();
1740 let clock = self.clock;
1741 let request_id = cmd.request_id;
1742 let client_id = cmd.client_id.unwrap_or(self.client_id);
1743 let params = cmd.params.clone();
1744 let start_nanos = cmd.start.map(datetime_to_unix_nanos);
1745 let end_nanos = cmd.end.map(datetime_to_unix_nanos);
1746
1747 let client_clone = client.as_arc().clone();
1749 let start_nanos_clone = start_nanos;
1750 let end_nanos_clone = end_nanos;
1751 let cmd_start = cmd.start;
1752 let cmd_end = cmd.end;
1753 let trading_hours = request_trading_hours(self.config.use_regular_trading_hours);
1754
1755 self.spawn_command(async move {
1756 let mut all_quotes = Vec::new();
1757 let mut current_end_date = cmd_end;
1759 if current_end_date.is_none() {
1760 current_end_date = Some(jiff::Timestamp::now());
1761 }
1762
1763 let current_start_date = cmd_start;
1764
1765 loop {
1766 if !should_continue_historical_tick_pagination(
1767 current_start_date,
1768 current_end_date,
1769 all_quotes.len(),
1770 limit,
1771 ) {
1772 break;
1773 }
1774
1775 let current_end_ib = current_end_date.as_ref().map(jiff_to_ib_datetime);
1776
1777 let mut builder = client_clone
1779 .historical_ticks(&contract, number_of_ticks)
1780 .trading_hours(trading_hours);
1781
1782 if let Some(end) = current_end_ib {
1785 builder = builder.ending(end);
1786 }
1787
1788 match builder.bid_ask(IgnoreSize::No).await {
1789 Ok(subscription) => {
1790 let mut subscription = subscription.filter_data();
1791 let mut batch_quotes = Vec::new();
1792
1793 while let Some(tick_result) = subscription.next().await {
1794 let tick = match tick_result {
1795 Ok(tick) => tick,
1796 Err(e) => {
1797 tracing::warn!("Historical quote ticks stream error: {e:?}");
1798 continue;
1799 }
1800 };
1801
1802 let ts_event =
1803 super::convert::ib_timestamp_to_unix_nanos(&tick.timestamp);
1804 let ts_init = clock.get_time_ns();
1805
1806 match super::parse::parse_quote_tick(
1807 instrument_id,
1808 Some(tick.price_bid),
1809 Some(tick.price_ask),
1810 tick.size_bid,
1811 tick.size_ask,
1812 prices,
1813 size_precision,
1814 ts_event,
1815 ts_init,
1816 ) {
1817 Ok(quote_tick) => batch_quotes.push(quote_tick),
1818 Err(e) => {
1819 tracing::warn!("Failed to parse quote tick: {:?}", e);
1820 }
1821 }
1822 }
1823
1824 if !extend_historical_tick_batch(
1825 &mut all_quotes,
1826 batch_quotes,
1827 current_start_date,
1828 &mut current_end_date,
1829 start_nanos_clone,
1830 end_nanos_clone,
1831 limit,
1832 |quote| quote.ts_event,
1833 ) {
1834 break;
1835 }
1836 }
1837 Err(e) => {
1838 tracing::error!(
1839 "Historical quotes request failed for {}: {:?}",
1840 instrument_id,
1841 e
1842 );
1843 break;
1844 }
1845 }
1846 }
1847
1848 retain_historical_ticks_in_range(
1849 &mut all_quotes,
1850 start_nanos_clone,
1851 end_nanos_clone,
1852 |quote| quote.ts_event,
1853 );
1854
1855 all_quotes.sort_by_key(|q| q.ts_event);
1856 if let Some(limit) = limit
1857 && all_quotes.len() > limit
1858 {
1859 all_quotes = all_quotes.split_off(all_quotes.len() - limit);
1860 }
1861
1862 let quotes_count = all_quotes.len();
1863 let response = DataResponse::Quotes(QuotesResponse::new(
1864 request_id,
1865 client_id,
1866 instrument_id,
1867 all_quotes,
1868 start_nanos_clone,
1869 end_nanos_clone,
1870 clock.get_time_ns(),
1871 params,
1872 ));
1873
1874 if let Err(e) = data_sender.send(DataEvent::Response(response)) {
1875 tracing::error!("Failed to send quotes response: {e}");
1876 } else {
1877 tracing::debug!(
1878 "Successfully sent {} quotes for {}",
1879 quotes_count,
1880 instrument_id
1881 );
1882 }
1883 });
1884
1885 Ok(())
1886 }
1887
1888 fn request_trades(&self, cmd: RequestTrades) -> anyhow::Result<()> {
1889 let instrument_id = cmd.single_instrument_id()?;
1890
1891 tracing::debug!("Requesting trades for {instrument_id}");
1892
1893 let client = self
1894 .ib_client
1895 .as_ref()
1896 .context("IB client not connected. Call connect() first")?;
1897
1898 let instrument = self
1900 .instrument_provider
1901 .find(&instrument_id)
1902 .context(format!("Instrument {instrument_id} not found in provider"))?;
1903
1904 if matches!(instrument, InstrumentAny::CurrencyPair(_)) {
1906 tracing::error!(
1907 "Interactive Brokers does not support trades for CurrencyPair instruments: {instrument_id}"
1908 );
1909 return Ok(());
1910 }
1911
1912 let prices = IbPriceConverter::for_instrument(
1913 &instrument,
1914 self.instrument_provider.get_price_magnifier(&instrument_id),
1915 );
1916 let size_precision = instrument.size_precision();
1917
1918 let contract = self
1920 .instrument_provider
1921 .resolve_contract_for_instrument(instrument_id)
1922 .context("Failed to convert instrument_id to IB contract")?;
1923
1924 let limit = cmd
1925 .limit
1926 .map(|limit| limit.get())
1927 .or_else(|| cmd.start.is_none().then_some(HISTORICAL_TICK_DEFAULT_LIMIT));
1928 let number_of_ticks = limit.unwrap_or(1000).min(1000) as i32;
1929
1930 let data_sender = self.data_sender.clone();
1931 let clock = self.clock;
1932 let request_id = cmd.request_id;
1933 let client_id = cmd.client_id.unwrap_or(self.client_id);
1934 let params = cmd.params.clone();
1935 let start_nanos = cmd.start.map(datetime_to_unix_nanos);
1936 let end_nanos = cmd.end.map(datetime_to_unix_nanos);
1937
1938 let client_clone = client.as_arc().clone();
1940 let start_nanos_clone = start_nanos;
1941 let end_nanos_clone = end_nanos;
1942 let cmd_start = cmd.start;
1943 let cmd_end = cmd.end;
1944 let trading_hours = request_trading_hours(self.config.use_regular_trading_hours);
1945
1946 self.spawn_command(async move {
1947 let mut all_trades = Vec::new();
1948 let mut current_end_date = cmd_end;
1950 if current_end_date.is_none() {
1951 current_end_date = Some(jiff::Timestamp::now());
1952 }
1953
1954 let current_start_date = cmd_start;
1955
1956 loop {
1957 if !should_continue_historical_tick_pagination(
1958 current_start_date,
1959 current_end_date,
1960 all_trades.len(),
1961 limit,
1962 ) {
1963 break;
1964 }
1965
1966 let current_end_ib = current_end_date.as_ref().map(jiff_to_ib_datetime);
1967
1968 let mut builder = client_clone
1970 .historical_ticks(&contract, number_of_ticks)
1971 .trading_hours(trading_hours);
1972
1973 if let Some(end) = current_end_ib {
1976 builder = builder.ending(end);
1977 }
1978
1979 match builder.trade().await {
1980 Ok(subscription) => {
1981 let mut subscription = subscription.filter_data();
1982 let mut batch_trades = Vec::new();
1983
1984 while let Some(tick_result) = subscription.next().await {
1985 let tick = match tick_result {
1986 Ok(tick) => tick,
1987 Err(e) => {
1988 tracing::warn!("Historical trade ticks stream error: {e:?}");
1989 continue;
1990 }
1991 };
1992
1993 let ts_event =
1994 super::convert::ib_timestamp_to_unix_nanos(&tick.timestamp);
1995 let ts_init = clock.get_time_ns();
1996
1997 let trade_id = None;
1999
2000 let Some(size) = tick.size else {
2001 tracing::warn!(
2002 "Skipping historical trade tick with no size for {}",
2003 instrument_id
2004 );
2005 continue;
2006 };
2007
2008 match super::parse::parse_trade_tick(
2009 instrument_id,
2010 tick.price,
2011 size,
2012 prices,
2013 size_precision,
2014 ts_event,
2015 ts_init,
2016 trade_id,
2017 ) {
2018 Ok(trade_tick) => batch_trades.push(trade_tick),
2019 Err(e) => {
2020 log_tick_parse_error("trade", instrument_id, &e);
2021 }
2022 }
2023 }
2024
2025 if !extend_historical_tick_batch(
2026 &mut all_trades,
2027 batch_trades,
2028 current_start_date,
2029 &mut current_end_date,
2030 start_nanos_clone,
2031 end_nanos_clone,
2032 limit,
2033 |trade| trade.ts_event,
2034 ) {
2035 break;
2036 }
2037 }
2038 Err(e) => {
2039 tracing::error!(
2040 "Historical trades request failed for {}: {:?}",
2041 instrument_id,
2042 e
2043 );
2044 break;
2045 }
2046 }
2047 }
2048
2049 retain_historical_ticks_in_range(
2050 &mut all_trades,
2051 start_nanos_clone,
2052 end_nanos_clone,
2053 |trade| trade.ts_event,
2054 );
2055
2056 all_trades.sort_by_key(|t| t.ts_event);
2057 if let Some(limit) = limit
2058 && all_trades.len() > limit
2059 {
2060 all_trades = all_trades.split_off(all_trades.len() - limit);
2061 }
2062
2063 let trades_count = all_trades.len();
2064 let response = DataResponse::Trades(TradesResponse::new(
2065 request_id,
2066 client_id,
2067 instrument_id,
2068 all_trades,
2069 start_nanos_clone,
2070 end_nanos_clone,
2071 clock.get_time_ns(),
2072 params,
2073 ));
2074
2075 if let Err(e) = data_sender.send(DataEvent::Response(response)) {
2076 tracing::error!("Failed to send trades response: {e}");
2077 } else {
2078 tracing::debug!(
2079 "Successfully sent {} trades for {}",
2080 trades_count,
2081 instrument_id
2082 );
2083 }
2084 });
2085
2086 Ok(())
2087 }
2088
2089 fn request_bars(&self, cmd: RequestBars) -> anyhow::Result<()> {
2090 let bar_type = cmd.single_bar_type()?;
2091
2092 tracing::debug!("Requesting bars for {bar_type}");
2093
2094 if !bar_type.spec().is_time_aggregated() {
2096 tracing::error!(
2097 "Cannot request {bar_type} bars: only time bars are aggregated by Interactive Brokers"
2098 );
2099 return Ok(());
2100 }
2101
2102 let client = self
2103 .ib_client
2104 .as_ref()
2105 .context("IB client not connected. Call connect() first")?;
2106
2107 let instrument_id = bar_type.instrument_id();
2109 let instrument = self
2110 .instrument_provider
2111 .find(&instrument_id)
2112 .context(format!("Instrument {instrument_id} not found in provider"))?;
2113
2114 let prices = IbPriceConverter::for_instrument(
2115 &instrument,
2116 self.instrument_provider.get_price_magnifier(&instrument_id),
2117 );
2118 let size_precision = instrument.size_precision();
2119
2120 let contract = self
2122 .instrument_provider
2123 .resolve_contract_for_instrument(instrument_id)
2124 .context("Failed to convert instrument_id to IB contract")?;
2125
2126 let ib_bar_size = bar_type_to_ib_bar_size(&bar_type)
2128 .context("Failed to convert bar type to IB bar size")?;
2129 let is_crypto = is_crypto_contract(&contract);
2132 let ib_what_to_show =
2133 price_type_to_ib_what_to_show_for_security(bar_type.spec().price_type, is_crypto);
2134
2135 let is_continuous_future = contract.security_type == SecurityType::ContinuousFuture;
2138 let segments = calculate_duration_segments(cmd.start, cmd.end, None);
2139 let segments = bar_request_segments(segments, is_continuous_future);
2140
2141 let data_sender = self.data_sender.clone();
2142 let clock = self.clock;
2143 let request_id = cmd.request_id;
2144 let client_id = cmd.client_id.unwrap_or(self.client_id);
2145 let params = cmd.params.clone();
2146 let start_nanos = cmd.start.map(datetime_to_unix_nanos);
2147 let end_nanos = cmd.end.map(datetime_to_unix_nanos);
2148 let limit = cmd.limit.map(|limit| limit.get());
2149
2150 let client_clone = client.as_arc().clone();
2152 let trading_hours = request_trading_hours(self.config.use_regular_trading_hours);
2153
2154 self.spawn_command(async move {
2155 let mut all_bars = Vec::new();
2156
2157 for (seg_end, seg_duration) in segments {
2158 let mut request = client_clone
2159 .historical_data(&contract, ib_bar_size)
2160 .duration(seg_duration)
2161 .what_to_show(ib_what_to_show)
2162 .trading_hours(trading_hours);
2163
2164 if let Some(end) = seg_end {
2165 request = request.ending(jiff_to_ib_datetime(&end));
2166 }
2167
2168 match request.fetch().await {
2169 Ok(historical_data) => {
2170 for ib_bar in &historical_data.bars {
2172 match ib_bar_to_nautilus_bar(
2173 ib_bar,
2174 bar_type,
2175 prices,
2176 size_precision,
2177 ) {
2178 Ok(bar) => all_bars.push(bar),
2179 Err(e) => {
2180 tracing::warn!(
2181 "Failed to convert IB bar to Nautilus bar: {:?}",
2182 e
2183 );
2184 }
2185 }
2186 }
2187 }
2188 Err(e) => {
2189 tracing::warn!(
2190 "Historical data request failed for {bar_type} segment ending at {seg_end:?}: {e}, emitting the bars already retrieved"
2191 );
2192 break;
2193 }
2194 }
2195 }
2196
2197 if all_bars.is_empty() {
2199 tracing::warn!("No bar data received for {}", bar_type);
2200 }
2201
2202 all_bars.sort_by_key(|b| b.ts_event);
2204 all_bars.dedup();
2205 if let Some(limit) = limit
2206 && all_bars.len() > limit
2207 {
2208 all_bars = all_bars.split_off(all_bars.len() - limit);
2209 }
2210
2211 let bars_count = all_bars.len();
2212
2213 let response = DataResponse::Bars(BarsResponse::new(
2214 request_id,
2215 client_id,
2216 bar_type,
2217 all_bars,
2218 start_nanos,
2219 end_nanos,
2220 clock.get_time_ns(),
2221 params,
2222 ));
2223
2224 if let Err(e) = data_sender.send(DataEvent::Response(response)) {
2225 tracing::error!("Failed to send bars response: {e}");
2226 } else {
2227 tracing::debug!(
2228 "Successfully sent {} bars for {} (segmented)",
2229 bars_count,
2230 bar_type
2231 );
2232 }
2233 });
2234
2235 Ok(())
2236 }
2237}
2238
2239impl InteractiveBrokersDataClient {
2240 fn has_subscription(
2241 &self,
2242 instrument_id: InstrumentId,
2243 subscription_type: SubscriptionType,
2244 ) -> anyhow::Result<bool> {
2245 let subscriptions = lock(&self.subscriptions)?;
2246 Ok(subscriptions.contains_key(&(instrument_id, subscription_type)))
2247 }
2248
2249 fn clear_bar_tracking_state(&self) {
2250 if let Ok(mut tasks) = self.bar_timeout_tasks.try_lock() {
2251 for task in tasks.values_mut() {
2252 task.abort();
2253 }
2254 } else {
2255 tracing::warn!("Failed to lock IB bar timeout tasks for cleanup");
2256 }
2257
2258 if let Ok(mut last_bars) = self.last_bars.try_lock() {
2259 last_bars.clear();
2260 } else {
2261 tracing::warn!("Failed to lock IB last bars for cleanup");
2262 }
2263 }
2264}
2265
2266impl Drop for InteractiveBrokersDataClient {
2267 fn drop(&mut self) {
2268 let _ = self.stop();
2269 }
2270}
2271
2272impl InteractiveBrokersDataClient {
2273 fn subscribe_quotes_member(
2274 &self,
2275 instrument_id: InstrumentId,
2276 cmd: &SubscribeQuotes,
2277 ) -> anyhow::Result<()> {
2278 tracing::debug!("Subscribing to quotes for {instrument_id}");
2279
2280 if self.has_subscription(instrument_id, SubscriptionType::Quotes)? {
2281 tracing::debug!("Quote subscription already active for {instrument_id}");
2282 return Ok(());
2283 }
2284
2285 let client = self
2286 .ib_client
2287 .as_ref()
2288 .context("IB client not connected. Call connect() first")?;
2289
2290 let instrument = self
2292 .instrument_provider
2293 .find(&instrument_id)
2294 .context(format!("Instrument {instrument_id} not found in provider"))?;
2295
2296 let prices = IbPriceConverter::for_instrument(
2297 &instrument,
2298 self.instrument_provider.get_price_magnifier(&instrument_id),
2299 );
2300 let size_precision = instrument.size_precision();
2301
2302 let contract = self
2304 .instrument_provider
2305 .resolve_contract_for_instrument(instrument_id)
2306 .context("Failed to convert instrument_id to IB contract")?;
2307
2308 let is_bag = matches!(
2311 contract.security_type,
2312 ibapi::contracts::SecurityType::Spread
2313 ) || !contract.combo_legs.is_empty();
2314 let batch_quotes = cmd
2315 .params
2316 .as_ref()
2317 .and_then(|params| params.get_str("batch_quotes"))
2318 .map_or(self.config.batch_quotes, parse_bool_param_value);
2319
2320 let use_market_data = is_bag || batch_quotes;
2321
2322 let data_sender = self.data_sender.clone();
2323 let quote_cache = Arc::clone(&self.quote_cache);
2324 let clock = self.clock;
2325
2326 let subscription_token = self.cancellation_token.child_token();
2327
2328 let subscription_id = next_subscription_id();
2331 lock(&self.subscriptions)?.insert(
2332 (instrument_id, SubscriptionType::Quotes),
2333 SubscriptionInfo {
2334 cancellation_token: subscription_token.clone(),
2335 id: subscription_id,
2336 },
2337 );
2338
2339 let guard = SubscriptionGuard {
2340 subscriptions: Arc::clone(&self.subscriptions),
2341 key: (instrument_id, SubscriptionType::Quotes),
2342 id: subscription_id,
2343 };
2344
2345 let client_clone = client.as_arc().clone();
2347 let ignore_size_updates = self.config.ignore_quote_tick_size_updates;
2348 let data_farm_state = Arc::clone(&self.data_farm_state);
2349 let stream_config = StreamConfig::new(self.client_id, &self.config);
2350
2351 let task = async move {
2352 if use_market_data {
2353 tracing::debug!(
2355 "Using market_data subscription for {} (BAG: {}, batch_quotes: {})",
2356 instrument_id,
2357 is_bag,
2358 batch_quotes
2359 );
2360
2361 if let Err(e) = handle_quote_subscription(
2362 client_clone,
2363 contract,
2364 instrument_id,
2365 prices,
2366 size_precision,
2367 data_sender,
2368 quote_cache,
2369 clock,
2370 subscription_token,
2371 ignore_size_updates,
2372 Arc::clone(&data_farm_state),
2373 stream_config,
2374 )
2375 .await
2376 {
2377 tracing::error!("Quote subscription error for {}: {:?}", instrument_id, e);
2378 }
2379 } else {
2380 tracing::debug!(
2383 "Attempting tick_by_tick_bid_ask subscription for {}",
2384 instrument_id
2385 );
2386
2387 match handle_tick_by_tick_quote_subscription(
2388 client_clone.clone(),
2389 contract.clone(),
2390 instrument_id,
2391 prices,
2392 size_precision,
2393 data_sender.clone(),
2394 clock,
2395 subscription_token.clone(),
2396 Arc::clone(&data_farm_state),
2397 stream_config,
2398 )
2399 .await
2400 {
2401 Ok(()) => {
2402 }
2404 Err(e) => {
2405 tracing::warn!(
2406 "tick_by_tick_bid_ask failed for {} (may be BAG contract), falling back to market_data: {:?}",
2407 instrument_id,
2408 e
2409 );
2410
2411 if let Err(fallback_err) = handle_quote_subscription(
2413 client_clone,
2414 contract,
2415 instrument_id,
2416 prices,
2417 size_precision,
2418 data_sender,
2419 quote_cache,
2420 clock,
2421 subscription_token,
2422 ignore_size_updates,
2423 Arc::clone(&data_farm_state),
2424 stream_config,
2425 )
2426 .await
2427 {
2428 tracing::error!(
2429 "Quote subscription fallback also failed for {}: {:?}",
2430 instrument_id,
2431 fallback_err
2432 );
2433 } else {
2434 tracing::debug!(
2435 "Successfully subscribed to {} using market_data fallback",
2436 instrument_id
2437 );
2438 }
2439 }
2440 }
2441 }
2442
2443 guard.release();
2444 };
2445
2446 self.session_tasks
2447 .spawn(task)
2448 .context("failed to register IB data subscription task")?;
2449
2450 tracing::debug!(
2451 "Quote subscription started for {instrument_id} (method: {})",
2452 if use_market_data {
2453 "market_data"
2454 } else {
2455 "tick_by_tick_bid_ask"
2456 }
2457 );
2458 Ok(())
2459 }
2460}
2461
2462async fn load_requested_instrument(
2466 instrument_provider: &InteractiveBrokersInstrumentProvider,
2467 client: &ibapi::Client,
2468 instrument_id: InstrumentId,
2469 force_update: bool,
2470 filters: Option<HashMap<String, String>>,
2471 data_sender: &EventSender<DataEvent>,
2472) -> anyhow::Result<()> {
2473 if !is_spread_instrument_id(&instrument_id) {
2474 return instrument_provider
2475 .fetch_contract_details(client, instrument_id, force_update, filters)
2476 .await;
2477 }
2478
2479 instrument_provider
2480 .fetch_spread_instrument(client, instrument_id, force_update, filters)
2481 .await?;
2482 let leg_ids = parse_spread_instrument_id_to_legs(&instrument_id)?
2483 .into_iter()
2484 .map(|(leg_id, _)| leg_id)
2485 .collect::<Vec<_>>();
2486
2487 for leg in instrument_provider.find_all(&leg_ids) {
2488 if let Err(e) = data_sender.send(DataEvent::Data(leg.into())) {
2489 tracing::error!("Failed to send spread leg instrument: {e}");
2490 }
2491 }
2492
2493 Ok(())
2494}
2495
2496#[cfg(test)]
2497mod tests {
2498 use nautilus_common::live::get_runtime;
2499 use rstest::rstest;
2500
2501 use super::*;
2502 use crate::common::consts::IB_CLIENT_ID;
2503
2504 #[rstest]
2505 #[case(true, ibapi::market_data::TradingHours::Regular)]
2506 #[case(false, ibapi::market_data::TradingHours::Extended)]
2507 fn test_request_trading_hours_uses_config(
2508 #[case] use_regular_trading_hours: bool,
2509 #[case] expected: ibapi::market_data::TradingHours,
2510 ) {
2511 assert_eq!(request_trading_hours(use_regular_trading_hours), expected);
2512 }
2513
2514 #[rstest]
2515 #[case("true", true)]
2516 #[case("True", true)]
2517 #[case("1", true)]
2518 #[case("false", false)]
2519 #[case("False", false)]
2520 #[case("0", false)]
2521 fn test_parse_bool_param_value(#[case] value: &str, #[case] expected: bool) {
2522 assert_eq!(parse_bool_param_value(value), expected);
2523 }
2524
2525 #[rstest]
2526 fn test_datetime_to_unix_nanos() {
2527 let dt = jiff::Timestamp::new(1, 2).unwrap();
2528
2529 assert_eq!(datetime_to_unix_nanos(dt), UnixNanos::from(1_000_000_002));
2530 }
2531
2532 #[rstest]
2533 fn test_subscription_keys_keep_quote_and_trade_streams_independent() {
2534 let instrument_id = InstrumentId::from("AAPL.NASDAQ");
2535 let quote_token = CancellationToken::new();
2536 let trade_token = CancellationToken::new();
2537 let mut subscriptions = AHashMap::new();
2538 subscriptions.insert(
2539 (instrument_id, SubscriptionType::Quotes),
2540 SubscriptionInfo {
2541 cancellation_token: quote_token.clone(),
2542 id: next_subscription_id(),
2543 },
2544 );
2545
2546 subscriptions.insert(
2547 (instrument_id, SubscriptionType::Trades),
2548 SubscriptionInfo {
2549 cancellation_token: trade_token.clone(),
2550 id: next_subscription_id(),
2551 },
2552 );
2553
2554 let quote = subscriptions
2555 .remove(&(instrument_id, SubscriptionType::Quotes))
2556 .unwrap();
2557 quote.cancellation_token.cancel();
2558
2559 assert!(quote_token.is_cancelled());
2560 assert!(!trade_token.is_cancelled());
2561 assert_eq!(subscriptions.len(), 1);
2562 assert!(subscriptions.contains_key(&(instrument_id, SubscriptionType::Trades)));
2563 }
2564
2565 #[rstest]
2566 fn test_subscription_keys_keep_bar_types_of_one_instrument_independent() {
2567 let minute = BarType::from("AAPL.NASDAQ-1-MINUTE-LAST-EXTERNAL");
2568 let five_minute = BarType::from("AAPL.NASDAQ-5-MINUTE-LAST-EXTERNAL");
2569 let instrument_id = minute.instrument_id();
2570 let minute_token = CancellationToken::new();
2571 let five_minute_token = CancellationToken::new();
2572 let mut subscriptions = AHashMap::new();
2573 subscriptions.insert(
2574 (instrument_id, SubscriptionType::Bars(minute)),
2575 SubscriptionInfo {
2576 cancellation_token: minute_token.clone(),
2577 id: next_subscription_id(),
2578 },
2579 );
2580
2581 subscriptions.insert(
2582 (instrument_id, SubscriptionType::Bars(five_minute)),
2583 SubscriptionInfo {
2584 cancellation_token: five_minute_token.clone(),
2585 id: next_subscription_id(),
2586 },
2587 );
2588
2589 let five = subscriptions
2590 .remove(&(instrument_id, SubscriptionType::Bars(five_minute)))
2591 .unwrap();
2592 five.cancellation_token.cancel();
2593
2594 assert!(five_minute_token.is_cancelled());
2595 assert!(!minute_token.is_cancelled());
2596 assert_eq!(subscriptions.len(), 1);
2597 assert!(subscriptions.contains_key(&(instrument_id, SubscriptionType::Bars(minute))));
2598 }
2599
2600 #[rstest]
2601 fn test_subscription_guard_release_removes_only_matching_registration() {
2602 let subscriptions = Arc::new(Mutex::new(AHashMap::new()));
2603 let instrument_id = InstrumentId::from("AAPL.NASDAQ");
2604 let key = (instrument_id, SubscriptionType::Quotes);
2605
2606 let first_id = next_subscription_id();
2607 subscriptions.lock().unwrap().insert(
2608 key,
2609 SubscriptionInfo {
2610 cancellation_token: CancellationToken::new(),
2611 id: first_id,
2612 },
2613 );
2614
2615 let stale_guard = SubscriptionGuard {
2616 subscriptions: Arc::clone(&subscriptions),
2617 key,
2618 id: first_id,
2619 };
2620
2621 let second_id = next_subscription_id();
2623 subscriptions.lock().unwrap().insert(
2624 key,
2625 SubscriptionInfo {
2626 cancellation_token: CancellationToken::new(),
2627 id: second_id,
2628 },
2629 );
2630
2631 stale_guard.release();
2632 assert!(subscriptions.lock().unwrap().contains_key(&key));
2633
2634 let current_guard = SubscriptionGuard {
2635 subscriptions: Arc::clone(&subscriptions),
2636 key,
2637 id: second_id,
2638 };
2639
2640 current_guard.release();
2641 assert!(!subscriptions.lock().unwrap().contains_key(&key));
2642 }
2643
2644 #[rstest]
2645 fn test_subscription_guard_drop_removes_registration_without_release() {
2646 let subscriptions = Arc::new(Mutex::new(AHashMap::new()));
2647 let instrument_id = InstrumentId::from("AAPL.NASDAQ");
2648 let key = (instrument_id, SubscriptionType::Quotes);
2649
2650 let id = next_subscription_id();
2651 subscriptions.lock().unwrap().insert(
2652 key,
2653 SubscriptionInfo {
2654 cancellation_token: CancellationToken::new(),
2655 id,
2656 },
2657 );
2658
2659 {
2660 let _guard = SubscriptionGuard {
2661 subscriptions: Arc::clone(&subscriptions),
2662 key,
2663 id,
2664 };
2665 }
2667
2668 assert!(!subscriptions.lock().unwrap().contains_key(&key));
2669 }
2670
2671 #[rstest]
2672 fn test_venue_is_none_for_default_routing() {
2673 let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel();
2677 nautilus_common::live::runner::replace_data_event_sender(sender);
2678
2679 let config = InteractiveBrokersDataClientConfig::default();
2680
2681 let provider = Arc::new(
2682 InteractiveBrokersInstrumentProvider::new(config.instrument_provider.clone()).unwrap(),
2683 );
2684 let client = InteractiveBrokersDataClient::new(*IB_CLIENT_ID, config, provider).unwrap();
2685
2686 assert_eq!(client.venue(), None);
2687 }
2688
2689 #[rstest]
2690 fn test_stop_closes_tasks_until_async_generation_preparation() {
2691 let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel();
2692 nautilus_common::live::runner::replace_data_event_sender(sender);
2693
2694 let config = InteractiveBrokersDataClientConfig::default();
2695
2696 let provider = Arc::new(
2697 InteractiveBrokersInstrumentProvider::new(config.instrument_provider.clone()).unwrap(),
2698 );
2699 let mut client =
2700 InteractiveBrokersDataClient::new(*IB_CLIENT_ID, config, provider).unwrap();
2701
2702 client
2703 .session_tasks
2704 .spawn(async { std::future::pending::<()>().await })
2705 .unwrap();
2706
2707 client.stop().unwrap();
2708
2709 assert!(client.cancellation_token.is_cancelled());
2710 assert!(client.cancellation_token.child_token().is_cancelled());
2711 assert!(!client.session_tasks.is_open());
2712 assert!(!client.command_tasks.is_open());
2713
2714 get_runtime()
2715 .block_on(client.prepare_task_groups())
2716 .expect("prepare replacement task generation");
2717
2718 assert!(!client.cancellation_token.is_cancelled());
2719 assert!(!client.cancellation_token.child_token().is_cancelled());
2720 assert!(client.session_tasks.is_open());
2721 assert!(client.command_tasks.is_open());
2722 }
2723
2724 #[rstest]
2725 fn test_stop_retains_bar_timeout_handle_until_async_finish() {
2726 let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel();
2727 nautilus_common::live::runner::replace_data_event_sender(sender);
2728
2729 let config = InteractiveBrokersDataClientConfig::default();
2730
2731 let provider = Arc::new(
2732 InteractiveBrokersInstrumentProvider::new(config.instrument_provider.clone()).unwrap(),
2733 );
2734 let mut client =
2735 InteractiveBrokersDataClient::new(*IB_CLIENT_ID, config, provider).unwrap();
2736 let bar_type = "AAPL.NASDAQ-1-MINUTE-LAST-EXTERNAL".to_string();
2737
2738 let task = get_runtime().spawn(async {
2739 std::future::pending::<()>().await;
2740 });
2741
2742 get_runtime().block_on(async {
2743 client
2744 .bar_timeout_tasks
2745 .lock()
2746 .await
2747 .insert(bar_type.clone(), TaskSlot::from_handle(task));
2748 client.last_bars.lock().await.insert(
2749 bar_type.clone(),
2750 ibapi::market_data::realtime::Bar {
2751 date: time::OffsetDateTime::from_unix_timestamp(1).unwrap(),
2752 open: 1.0,
2753 high: 1.0,
2754 low: 1.0,
2755 close: 1.0,
2756 volume: 1.0,
2757 wap: 1.0,
2758 count: 1,
2759 },
2760 );
2761 });
2762
2763 client.stop().unwrap();
2764
2765 get_runtime().block_on(async {
2766 assert_eq!(client.bar_timeout_tasks.lock().await.len(), 1);
2767 assert!(client.last_bars.lock().await.is_empty());
2768
2769 client
2770 .prepare_task_groups()
2771 .await
2772 .expect("finish stopped task generation");
2773
2774 assert!(client.bar_timeout_tasks.lock().await.is_empty());
2775 });
2776 }
2777}