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