1#[path = "core_streams.rs"]
19mod streams;
20
21use std::{
22 collections::HashMap,
23 fmt::Debug,
24 sync::{
25 Arc,
26 atomic::{AtomicBool, Ordering},
27 },
28};
29
30use ahash::AHashMap;
31use anyhow::Context;
32use ibapi::{
33 contracts::{Contract, Currency as IBCurrency, Exchange as IBExchange, SecurityType, Symbol},
34 market_data::{IgnoreSize, historical::ToDuration},
35};
36use nautilus_common::{
37 clients::DataClient,
38 live::{get_runtime, runner::get_data_event_sender},
39 messages::{
40 DataEvent, DataResponse,
41 data::{
42 BarsResponse, InstrumentResponse, InstrumentsResponse, QuotesResponse, RequestBars,
43 RequestInstrument, RequestInstruments, RequestQuotes, RequestTrades, SubscribeBars,
44 SubscribeBookDeltas, SubscribeIndexPrices, SubscribeOptionGreeks, SubscribeQuotes,
45 SubscribeTrades, TradesResponse, UnsubscribeBars, UnsubscribeBookDeltas,
46 UnsubscribeIndexPrices, UnsubscribeOptionGreeks, UnsubscribeQuotes, UnsubscribeTrades,
47 },
48 },
49};
50use nautilus_core::{
51 UnixNanos,
52 params::Params,
53 time::{AtomicTime, get_atomic_clock_realtime},
54};
55use nautilus_model::{
56 enums::BookType,
57 identifiers::{ClientId, InstrumentId, Venue},
58 instruments::{Instrument, any::InstrumentAny},
59};
60use tokio::task::JoinHandle;
61use tokio_util::sync::CancellationToken;
62
63use self::streams::{
64 handle_historical_bars_subscription, handle_index_price_subscription,
65 handle_market_depth_subscription, handle_option_greeks_subscription, handle_quote_subscription,
66 handle_realtime_bars_subscription, handle_tick_by_tick_quote_subscription,
67 handle_trade_subscription,
68};
69use super::{
70 cache::{OptionGreeksCache, QuoteCache},
71 convert::{
72 apply_bar_price_magnifier, apply_price_magnifier, bar_type_to_ib_bar_size,
73 calculate_duration, calculate_duration_segments, chrono_to_ib_datetime,
74 ib_bar_to_nautilus_bar, price_type_to_ib_what_to_show,
75 },
76};
77use crate::{
78 common::{consts::IB_VENUE, shared_client::SharedClientHandle},
79 config::InteractiveBrokersDataClientConfig,
80 providers::instruments::InteractiveBrokersInstrumentProvider,
81};
82
83#[cfg_attr(
89 feature = "python",
90 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.interactive_brokers")
91)]
92pub struct InteractiveBrokersDataClient {
93 client_id: ClientId,
95 config: InteractiveBrokersDataClientConfig,
97 instrument_provider: Arc<InteractiveBrokersInstrumentProvider>,
99 is_connected: AtomicBool,
101 cancellation_token: CancellationToken,
103 tasks: Vec<JoinHandle<()>>,
105 data_sender: tokio::sync::mpsc::UnboundedSender<DataEvent>,
107 subscriptions: Arc<tokio::sync::Mutex<AHashMap<InstrumentId, SubscriptionInfo>>>,
109 option_greeks_subscriptions: Arc<tokio::sync::Mutex<AHashMap<InstrumentId, CancellationToken>>>,
111 quote_cache: Arc<tokio::sync::Mutex<QuoteCache>>,
113 option_greeks_cache: Arc<tokio::sync::Mutex<OptionGreeksCache>>,
115 clock: &'static AtomicTime,
117 ib_client: Option<SharedClientHandle>,
119 last_bars: Arc<tokio::sync::Mutex<AHashMap<String, ibapi::market_data::realtime::Bar>>>,
121 bar_timeout_tasks: Arc<tokio::sync::Mutex<AHashMap<String, tokio::task::JoinHandle<()>>>>,
123}
124
125#[derive(Debug)]
127#[allow(dead_code)]
128struct SubscriptionInfo {
129 instrument_id: InstrumentId,
131 subscription_type: SubscriptionType,
133 cancellation_token: CancellationToken,
135}
136
137#[derive(Debug, Clone)]
139enum SubscriptionType {
140 Quotes,
142 IndexPrices,
144 Trades,
146 Bars,
148 BookDeltas,
150}
151
152fn parse_start_ns(params: Option<&nautilus_core::Params>) -> Option<UnixNanos> {
153 params
154 .and_then(|params| params.get_u64("start_ns"))
155 .or_else(|| {
156 params
157 .and_then(|params| params.get_str("start_ns"))
158 .and_then(|value| value.parse::<u64>().ok())
159 })
160 .map(UnixNanos::from)
161}
162
163fn parse_bool_param_value(value: &str) -> bool {
164 matches!(value, "true" | "True" | "1")
165}
166
167fn params_to_string_filters(params: Option<&Params>) -> Option<HashMap<String, String>> {
168 let filters: HashMap<String, String> = params?
169 .iter()
170 .filter_map(|(key, value)| value.as_str().map(|value| (key.clone(), value.to_string())))
171 .collect();
172 (!filters.is_empty()).then_some(filters)
173}
174
175fn datetime_to_unix_nanos(dt: chrono::DateTime<chrono::Utc>) -> UnixNanos {
176 UnixNanos::from(
177 dt.timestamp_nanos_opt()
178 .unwrap_or_else(|| dt.timestamp() * 1_000_000_000) as u64,
179 )
180}
181
182fn request_trading_hours(use_regular_trading_hours: bool) -> ibapi::market_data::TradingHours {
183 if use_regular_trading_hours {
184 ibapi::market_data::TradingHours::Regular
185 } else {
186 ibapi::market_data::TradingHours::Extended
187 }
188}
189
190fn retreat_historical_tick_end_datetime(
191 min_ts_nanos: u64,
192) -> Option<chrono::DateTime<chrono::Utc>> {
193 let new_end_nanos = min_ts_nanos.saturating_sub(1_000_000);
194 let seconds = (new_end_nanos / 1_000_000_000) as i64;
195 let nanos = (new_end_nanos % 1_000_000_000) as u32;
196 chrono::DateTime::from_timestamp(seconds, nanos)
197}
198
199fn should_continue_historical_tick_pagination(
200 current_start_date: Option<chrono::DateTime<chrono::Utc>>,
201 current_end_date: Option<chrono::DateTime<chrono::Utc>>,
202 current_len: usize,
203 limit: Option<usize>,
204) -> bool {
205 limit.is_none_or(|limit| current_len < limit)
206 && current_start_date
207 .zip(current_end_date)
208 .is_none_or(|(start, end)| end > start)
209}
210
211fn retreat_end_to_earliest_tick<T>(
212 batch: &[T],
213 ts_event: impl Fn(&T) -> UnixNanos,
214) -> Option<chrono::DateTime<chrono::Utc>> {
215 batch
216 .iter()
217 .min_by_key(|tick| ts_event(tick))
218 .and_then(|tick| retreat_historical_tick_end_datetime(ts_event(tick).as_u64()))
219}
220
221fn retain_historical_ticks_in_range<T>(
222 ticks: &mut Vec<T>,
223 start_nanos: Option<UnixNanos>,
224 end_nanos: Option<UnixNanos>,
225 ts_event: impl Fn(&T) -> UnixNanos,
226) {
227 ticks.retain(|tick| {
228 let ts_event = ts_event(tick);
229 start_nanos.is_none_or(|start| ts_event >= start)
230 && end_nanos.is_none_or(|end| ts_event <= end)
231 });
232}
233
234fn extend_historical_tick_batch<T>(
235 all_ticks: &mut Vec<T>,
236 batch_ticks: Vec<T>,
237 current_start_date: Option<chrono::DateTime<chrono::Utc>>,
238 current_end_date: &mut Option<chrono::DateTime<chrono::Utc>>,
239 start_nanos: Option<UnixNanos>,
240 end_nanos: Option<UnixNanos>,
241 limit: Option<usize>,
242 ts_event: impl Fn(&T) -> UnixNanos,
243) -> bool {
244 if batch_ticks.is_empty() {
245 return false;
246 }
247
248 if let Some(new_end) = retreat_end_to_earliest_tick(&batch_ticks, &ts_event) {
249 *current_end_date = Some(new_end);
250 } else {
251 return false;
252 }
253
254 all_ticks.extend(batch_ticks);
255
256 if current_start_date
257 .as_ref()
258 .zip(current_end_date.as_ref())
259 .is_some_and(|(start, end)| end <= start)
260 {
261 retain_historical_ticks_in_range(all_ticks, start_nanos, end_nanos, &ts_event);
262 return false;
263 }
264
265 limit.is_none_or(|limit| all_ticks.len() < limit)
266}
267
268impl InteractiveBrokersDataClient {
269 pub fn new(
281 client_id: ClientId,
282 config: InteractiveBrokersDataClientConfig,
283 instrument_provider: Arc<InteractiveBrokersInstrumentProvider>,
284 ) -> anyhow::Result<Self> {
285 let clock = get_atomic_clock_realtime();
286 let data_sender = get_data_event_sender();
287
288 Ok(Self {
289 client_id,
290 config,
291 instrument_provider,
292 is_connected: AtomicBool::new(false),
293 cancellation_token: CancellationToken::new(),
294 tasks: Vec::new(),
295 data_sender,
296 subscriptions: Arc::new(tokio::sync::Mutex::new(AHashMap::new())),
297 option_greeks_subscriptions: Arc::new(tokio::sync::Mutex::new(AHashMap::new())),
298 quote_cache: Arc::new(tokio::sync::Mutex::new(QuoteCache::new())),
299 option_greeks_cache: Arc::new(tokio::sync::Mutex::new(OptionGreeksCache::new())),
300 clock,
301 ib_client: None,
302 last_bars: Arc::new(tokio::sync::Mutex::new(AHashMap::new())),
303 bar_timeout_tasks: Arc::new(tokio::sync::Mutex::new(AHashMap::new())),
304 })
305 }
306
307 fn venue_id(&self) -> Venue {
308 *IB_VENUE
309 }
310
311 fn cancel_active_subscriptions(&self) -> anyhow::Result<()> {
312 {
313 let mut subscriptions = self
314 .subscriptions
315 .try_lock()
316 .context("Failed to lock IB subscriptions for cancellation")?;
317 for subscription in subscriptions.values() {
318 subscription.cancellation_token.cancel();
319 }
320 subscriptions.clear();
321 }
322 {
323 let mut subscriptions = self
324 .option_greeks_subscriptions
325 .try_lock()
326 .context("Failed to lock IB option greeks subscriptions for cancellation")?;
327 for cancellation_token in subscriptions.values() {
328 cancellation_token.cancel();
329 }
330 subscriptions.clear();
331 }
332
333 Ok(())
334 }
335
336 #[allow(dead_code)] pub(crate) fn get_ib_client(&self) -> Option<&Arc<ibapi::Client>> {
340 self.ib_client.as_ref().map(|h| h.as_arc())
341 }
342
343 #[allow(dead_code)] pub(crate) fn instrument_provider(&self) -> Arc<InteractiveBrokersInstrumentProvider> {
346 Arc::clone(&self.instrument_provider)
347 }
348
349 pub async fn batch_load_instruments(
363 &self,
364 instrument_ids: Vec<InstrumentId>,
365 ) -> anyhow::Result<Vec<InstrumentId>> {
366 log::debug!(
367 "Batch loading {} IB instruments through data client",
368 instrument_ids.len()
369 );
370 let client = self
371 .ib_client
372 .as_ref()
373 .context("IB client not connected. Call connect() first")?;
374
375 let loaded = self
376 .instrument_provider
377 .batch_load(client, instrument_ids, None)
378 .await?;
379 log::debug!("Batch loaded {} IB instruments", loaded.len());
380 Ok(loaded)
381 }
382
383 pub async fn fetch_option_chain_by_range(
401 &self,
402 underlying_symbol: &str,
403 exchange: Option<&str>,
404 currency: Option<&str>,
405 expiry_min: Option<&str>,
406 expiry_max: Option<&str>,
407 ) -> anyhow::Result<usize> {
408 log::debug!(
409 "Fetching IB option chain by range (symbol={}, exchange={:?}, currency={:?}, expiry_min={:?}, expiry_max={:?})",
410 underlying_symbol,
411 exchange,
412 currency,
413 expiry_min,
414 expiry_max
415 );
416 let client = self
417 .ib_client
418 .as_ref()
419 .context("IB client not connected. Call connect() first")?;
420
421 let underlying = Contract {
422 contract_id: 0,
423 symbol: Symbol::from(underlying_symbol.to_string()),
424 security_type: SecurityType::Stock,
425 last_trade_date_or_contract_month: String::new(),
426 strike: f64::MAX,
427 right: None,
428 multiplier: String::new(),
429 exchange: IBExchange::from(exchange.unwrap_or("SMART")),
430 currency: IBCurrency::from(currency.unwrap_or("USD")),
431 local_symbol: String::new(),
432 primary_exchange: IBExchange::from(""),
433 trading_class: String::new(),
434 include_expired: false,
435 security_id_type: None,
436 security_id: String::new(),
437 combo_legs_description: String::new(),
438 combo_legs: Vec::new(),
439 delta_neutral_contract: None,
440 issuer_id: String::new(),
441 description: String::new(),
442 last_trade_date: None,
443 };
444
445 let count = self
446 .instrument_provider
447 .fetch_option_chain_by_range(client, &underlying, expiry_min, expiry_max, None)
448 .await?;
449 log::debug!(
450 "Fetched {} IB option instruments for {}",
451 count,
452 underlying_symbol
453 );
454 Ok(count)
455 }
456
457 pub async fn fetch_futures_chain(
473 &self,
474 symbol: &str,
475 exchange: Option<&str>,
476 currency: Option<&str>,
477 min_expiry_days: Option<u32>,
478 max_expiry_days: Option<u32>,
479 ) -> anyhow::Result<usize> {
480 log::debug!(
481 "Fetching IB futures chain (symbol={}, exchange={:?}, currency={:?}, min_days={:?}, max_days={:?})",
482 symbol,
483 exchange,
484 currency,
485 min_expiry_days,
486 max_expiry_days
487 );
488 let client = self
489 .ib_client
490 .as_ref()
491 .context("IB client not connected. Call connect() first")?;
492
493 let count = self
494 .instrument_provider
495 .fetch_futures_chain(
496 client,
497 symbol,
498 exchange.unwrap_or(""),
499 currency.unwrap_or("USD"),
500 None,
501 false,
502 min_expiry_days,
503 max_expiry_days,
504 )
505 .await?;
506 log::debug!("Fetched {} IB futures instruments for {}", count, symbol);
507 Ok(count)
508 }
509
510 pub async fn fetch_bag_contract(
524 &self,
525 bag_contract: &ibapi::contracts::Contract,
526 ) -> anyhow::Result<usize> {
527 log::debug!(
528 "Fetching IB BAG contract details (contract_id={}, exchange={}, symbol={})",
529 bag_contract.contract_id,
530 bag_contract.exchange.as_str(),
531 bag_contract.symbol.as_str()
532 );
533 let client = self
534 .ib_client
535 .as_ref()
536 .context("IB client not connected. Call connect() first")?;
537
538 let count = self
539 .instrument_provider
540 .fetch_bag_contract(client, bag_contract)
541 .await?;
542 log::debug!("Fetched {} BAG instruments", count);
543 Ok(count)
544 }
545}
546
547impl Debug for InteractiveBrokersDataClient {
548 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
549 f.debug_struct(stringify!(InteractiveBrokersDataClient))
550 .field("client_id", &self.client_id)
551 .field("config", &self.config)
552 .field("is_connected", &self.is_connected.load(Ordering::Relaxed))
553 .field("has_ib_client", &self.ib_client.is_some())
554 .finish_non_exhaustive()
555 }
556}
557
558#[async_trait::async_trait(?Send)]
559impl DataClient for InteractiveBrokersDataClient {
560 fn client_id(&self) -> ClientId {
561 self.client_id
562 }
563
564 fn venue(&self) -> Option<Venue> {
565 Some(self.venue_id())
566 }
567
568 fn start(&mut self) -> anyhow::Result<()> {
569 tracing::info!(
570 client_id = %self.client_id,
571 "Starting Interactive Brokers data client"
572 );
573 Ok(())
574 }
575
576 fn stop(&mut self) -> anyhow::Result<()> {
577 tracing::info!(
578 "Stopping Interactive Brokers data client {id}",
579 id = self.client_id
580 );
581 self.cancellation_token.cancel();
582 self.cancel_active_subscriptions()?;
583 self.is_connected.store(false, Ordering::Relaxed);
584
585 for task in &self.tasks {
586 task.abort();
587 }
588 self.tasks.clear();
589 self.clear_bar_tracking_state();
590 self.cancellation_token = CancellationToken::new();
591
592 Ok(())
593 }
594
595 fn reset(&mut self) -> anyhow::Result<()> {
596 tracing::debug!(
597 "Resetting Interactive Brokers data client {id}",
598 id = self.client_id
599 );
600 self.is_connected.store(false, Ordering::Relaxed);
601 self.cancel_active_subscriptions()?;
602 self.cancellation_token = CancellationToken::new();
603 self.tasks.clear();
604 self.clear_bar_tracking_state();
605
606 {
607 let mut cache = self
608 .quote_cache
609 .try_lock()
610 .context("Failed to lock IB quote cache for reset")?;
611 cache.clear();
612 }
613 {
614 let mut cache = self
615 .option_greeks_cache
616 .try_lock()
617 .context("Failed to lock IB option greeks cache for reset")?;
618 cache.clear();
619 }
620
621 Ok(())
622 }
623
624 fn dispose(&mut self) -> anyhow::Result<()> {
625 self.stop()
626 }
627
628 async fn connect(&mut self) -> anyhow::Result<()> {
629 tracing::debug!("Connecting Interactive Brokers data client...");
630
631 let handle = crate::common::shared_client::get_or_connect(
632 &self.config.host,
633 self.config.port,
634 self.config.client_id,
635 self.config.connection_timeout,
636 )
637 .await
638 .context("Failed to connect to IB Gateway/TWS")?;
639
640 let client = handle.as_arc();
641
642 tracing::info!(
643 "Connected to IB Gateway/TWS at {}:{} (client_id: {})",
644 self.config.host,
645 self.config.port,
646 self.config.client_id
647 );
648
649 if self.config.market_data_type != crate::config::MarketDataType::Realtime {
651 let ib_data_type: ibapi::market_data::MarketDataType =
652 self.config.market_data_type.into();
653 client
654 .switch_market_data_type(ib_data_type)
655 .await
656 .context("Failed to switch market data type")?;
657 tracing::info!("Set market data type to {:?}", self.config.market_data_type);
658 }
659
660 self.ib_client = Some(handle);
661 self.is_connected.store(true, Ordering::Relaxed);
662
663 tracing::debug!("Initializing IB data instrument provider");
665
666 if let Err(e) = self
667 .instrument_provider
668 .initialize_with_client(self.ib_client.as_ref().unwrap().as_arc().as_ref())
669 .await
670 {
671 if !self.config.instrument_provider.load_ids.is_empty()
672 || !self.config.instrument_provider.load_contracts.is_empty()
673 {
674 return Err(e).context("Failed to load configured IB instruments on startup");
675 }
676
677 tracing::warn!("Failed to load instruments on startup: {}", e);
678 }
679
680 let instrument_count = self.instrument_provider.count();
681 if instrument_count > 0 {
682 tracing::debug!(
683 "Data client connected with {} instruments in provider cache",
684 instrument_count
685 );
686
687 for instrument in self.instrument_provider.get_all() {
688 if let Err(e) = self.data_sender.send(DataEvent::Instrument(instrument)) {
689 tracing::warn!("Failed to publish startup-loaded instrument: {e}");
690 break;
691 }
692 }
693 }
694
695 tracing::info!("Connected Interactive Brokers data client");
696 Ok(())
697 }
698
699 async fn disconnect(&mut self) -> anyhow::Result<()> {
700 tracing::debug!("Disconnecting Interactive Brokers data client...");
701
702 self.stop()?;
703 self.ib_client = None;
704 self.is_connected.store(false, Ordering::Relaxed);
705 tracing::info!("Disconnected Interactive Brokers data client");
706 Ok(())
707 }
708
709 fn is_connected(&self) -> bool {
710 self.is_connected.load(Ordering::Relaxed)
711 }
712
713 fn is_disconnected(&self) -> bool {
714 !self.is_connected()
715 }
716
717 fn subscribe_quotes(&mut self, cmd: SubscribeQuotes) -> anyhow::Result<()> {
719 tracing::debug!("Subscribing to quotes for {}", cmd.instrument_id);
720
721 let client = self
722 .ib_client
723 .as_ref()
724 .context("IB client not connected. Call connect() first")?;
725
726 let instrument = self
728 .instrument_provider
729 .find(&cmd.instrument_id)
730 .context(format!(
731 "Instrument {} not found in provider",
732 cmd.instrument_id
733 ))?;
734
735 let price_precision = instrument.price_precision();
736 let size_precision = instrument.size_precision();
737
738 let contract = self
740 .instrument_provider
741 .resolve_contract_for_instrument(cmd.instrument_id)
742 .context("Failed to convert instrument_id to IB contract")?;
743
744 let is_bag = matches!(
747 contract.security_type,
748 ibapi::contracts::SecurityType::Spread
749 ) || !contract.combo_legs.is_empty();
750 let batch_quotes = cmd
751 .params
752 .as_ref()
753 .and_then(|params| params.get_str("batch_quotes"))
754 .map_or(self.config.batch_quotes, parse_bool_param_value);
755
756 let use_market_data = is_bag || batch_quotes;
757
758 let instrument_id = cmd.instrument_id;
759 let data_sender = self.data_sender.clone();
760 let quote_cache = Arc::clone(&self.quote_cache);
761 let clock = self.clock;
762
763 let price_magnifier = self.instrument_provider.get_price_magnifier(&instrument_id) as f64;
765
766 let subscription_token = self.cancellation_token.child_token();
767
768 let client_clone = client.as_arc().clone();
770 let subscription_token_clone = subscription_token.clone();
771 let ignore_size_updates = self.config.ignore_quote_tick_size_updates;
772
773 let task = get_runtime().spawn(async move {
774 if use_market_data {
775 tracing::debug!(
777 "Using market_data subscription for {} (BAG: {}, batch_quotes: {})",
778 instrument_id,
779 is_bag,
780 batch_quotes
781 );
782
783 if let Err(e) = handle_quote_subscription(
784 client_clone,
785 contract,
786 instrument_id,
787 price_precision,
788 size_precision,
789 data_sender,
790 quote_cache,
791 clock,
792 subscription_token_clone,
793 ignore_size_updates,
794 )
795 .await
796 {
797 tracing::error!("Quote subscription error for {}: {:?}", instrument_id, e);
798 }
799 } else {
800 tracing::debug!(
803 "Attempting tick_by_tick_bid_ask subscription for {}",
804 instrument_id
805 );
806
807 match handle_tick_by_tick_quote_subscription(
808 client_clone.clone(),
809 contract.clone(),
810 instrument_id,
811 price_precision,
812 size_precision,
813 data_sender.clone(),
814 clock,
815 subscription_token_clone.clone(),
816 price_magnifier,
817 )
818 .await
819 {
820 Ok(()) => {
821 }
823 Err(e) => {
824 tracing::warn!(
825 "tick_by_tick_bid_ask failed for {} (may be BAG contract), falling back to market_data: {:?}",
826 instrument_id,
827 e
828 );
829 if let Err(fallback_err) = handle_quote_subscription(
831 client_clone,
832 contract,
833 instrument_id,
834 price_precision,
835 size_precision,
836 data_sender,
837 quote_cache,
838 clock,
839 subscription_token_clone,
840 ignore_size_updates,
841 )
842 .await
843 {
844 tracing::error!(
845 "Quote subscription fallback also failed for {}: {:?}",
846 instrument_id,
847 fallback_err
848 );
849 } else {
850 tracing::debug!(
851 "Successfully subscribed to {} using market_data fallback",
852 instrument_id
853 );
854 }
855 }
856 }
857 }
858 });
859
860 self.tasks.push(task);
861
862 let mut subscriptions = self
864 .subscriptions
865 .try_lock()
866 .context("Failed to lock IB subscriptions")?;
867 subscriptions.insert(
868 cmd.instrument_id,
869 SubscriptionInfo {
870 instrument_id: cmd.instrument_id,
871 subscription_type: SubscriptionType::Quotes,
872 cancellation_token: subscription_token,
873 },
874 );
875
876 tracing::debug!(
877 "Quote subscription started for {} (method: {})",
878 cmd.instrument_id,
879 if use_market_data {
880 "market_data"
881 } else {
882 "tick_by_tick_bid_ask"
883 }
884 );
885 Ok(())
886 }
887
888 fn subscribe_index_prices(&mut self, cmd: SubscribeIndexPrices) -> anyhow::Result<()> {
889 tracing::debug!("Subscribing to index prices for {}", cmd.instrument_id);
890
891 let client = self
892 .ib_client
893 .as_ref()
894 .context("IB client not connected. Call connect() first")?;
895
896 let instrument = self
897 .instrument_provider
898 .find(&cmd.instrument_id)
899 .context(format!(
900 "Instrument {} not found in provider",
901 cmd.instrument_id
902 ))?;
903
904 let contract = self
905 .instrument_provider
906 .resolve_contract_for_instrument(cmd.instrument_id)
907 .context("Failed to convert instrument_id to IB contract")?;
908
909 if !matches!(contract.security_type, SecurityType::Index) {
910 tracing::warn!(
911 "Index price subscription not supported for security type {:?} on {}",
912 contract.security_type,
913 cmd.instrument_id
914 );
915 return Ok(());
916 }
917
918 let price_precision = instrument.price_precision();
919 let price_magnifier = self
920 .instrument_provider
921 .get_price_magnifier(&cmd.instrument_id);
922 let instrument_id = cmd.instrument_id;
923 let data_sender = self.data_sender.clone();
924 let clock = self.clock;
925
926 let subscription_token = self.cancellation_token.child_token();
927
928 let client_clone = client.as_arc().clone();
929 let subscription_token_clone = subscription_token.clone();
930
931 let task = get_runtime().spawn(async move {
932 if let Err(e) = handle_index_price_subscription(
933 client_clone,
934 contract,
935 instrument_id,
936 price_precision,
937 price_magnifier,
938 data_sender,
939 clock,
940 subscription_token_clone,
941 )
942 .await
943 {
944 tracing::error!(
945 "Index price subscription error for {}: {:?}",
946 instrument_id,
947 e
948 );
949 }
950 });
951
952 self.tasks.push(task);
953
954 let mut subscriptions = self
955 .subscriptions
956 .try_lock()
957 .context("Failed to lock IB subscriptions")?;
958 subscriptions.insert(
959 cmd.instrument_id,
960 SubscriptionInfo {
961 instrument_id: cmd.instrument_id,
962 subscription_type: SubscriptionType::IndexPrices,
963 cancellation_token: subscription_token,
964 },
965 );
966
967 tracing::debug!("Index price subscription started for {}", cmd.instrument_id);
968 Ok(())
969 }
970
971 fn subscribe_option_greeks(&mut self, cmd: SubscribeOptionGreeks) -> anyhow::Result<()> {
972 tracing::debug!("Subscribing to option greeks for {}", cmd.instrument_id);
973
974 let client = self
975 .ib_client
976 .as_ref()
977 .context("IB client not connected. Call connect() first")?;
978
979 let instrument = self
980 .instrument_provider
981 .find(&cmd.instrument_id)
982 .context(format!(
983 "Instrument {} not found in provider",
984 cmd.instrument_id
985 ))?;
986
987 if !matches!(
988 instrument,
989 InstrumentAny::OptionContract(_)
990 | InstrumentAny::FuturesContract(_)
991 | InstrumentAny::CryptoOption(_)
992 ) && !matches!(
993 self.instrument_provider
994 .resolve_contract_for_instrument(cmd.instrument_id)?
995 .security_type,
996 SecurityType::Option | SecurityType::FuturesOption
997 ) {
998 tracing::warn!(
999 "Option greeks subscription is only supported for option instruments: {}",
1000 cmd.instrument_id
1001 );
1002 return Ok(());
1003 }
1004
1005 let contract = self
1006 .instrument_provider
1007 .resolve_contract_for_instrument(cmd.instrument_id)
1008 .context("Failed to convert instrument_id to IB contract")?;
1009
1010 let instrument_id = cmd.instrument_id;
1011 let data_sender = self.data_sender.clone();
1012 let option_greeks_cache = Arc::clone(&self.option_greeks_cache);
1013 let clock = self.clock;
1014 let subscription_token = self.cancellation_token.child_token();
1015 let subscription_token_clone = subscription_token.clone();
1016 let client_clone = client.as_arc().clone();
1017
1018 let task = get_runtime().spawn(async move {
1019 if let Err(e) = handle_option_greeks_subscription(
1020 client_clone,
1021 contract,
1022 instrument_id,
1023 data_sender,
1024 option_greeks_cache,
1025 clock,
1026 subscription_token_clone,
1027 )
1028 .await
1029 {
1030 tracing::error!(
1031 "Option greeks subscription error for {}: {:?}",
1032 instrument_id,
1033 e
1034 );
1035 }
1036 });
1037
1038 self.tasks.push(task);
1039
1040 let mut subscriptions = self
1041 .option_greeks_subscriptions
1042 .try_lock()
1043 .context("Failed to lock IB option greeks subscriptions")?;
1044 if let Some(existing) = subscriptions.insert(cmd.instrument_id, subscription_token) {
1045 existing.cancel();
1046 }
1047
1048 tracing::debug!(
1049 "Option greeks subscription started for {}",
1050 cmd.instrument_id
1051 );
1052 Ok(())
1053 }
1054
1055 fn unsubscribe_quotes(&mut self, cmd: &UnsubscribeQuotes) -> anyhow::Result<()> {
1056 tracing::debug!("Unsubscribing from quotes for {}", cmd.instrument_id);
1057
1058 let mut subscriptions = self
1059 .subscriptions
1060 .try_lock()
1061 .context("Failed to lock IB subscriptions")?;
1062 if let Some(sub_info) = subscriptions.remove(&cmd.instrument_id) {
1063 sub_info.cancellation_token.cancel();
1064 tracing::debug!("Unsubscribed from quotes for {}", cmd.instrument_id);
1065 } else {
1066 tracing::warn!(
1067 "No active quote subscription found for {}",
1068 cmd.instrument_id
1069 );
1070 }
1071
1072 {
1074 }
1077
1078 Ok(())
1079 }
1080
1081 fn unsubscribe_index_prices(&mut self, cmd: &UnsubscribeIndexPrices) -> anyhow::Result<()> {
1082 tracing::debug!("Unsubscribing from index prices for {}", cmd.instrument_id);
1083
1084 let mut subscriptions = self
1085 .subscriptions
1086 .try_lock()
1087 .context("Failed to lock IB subscriptions")?;
1088 if let Some(sub_info) = subscriptions.remove(&cmd.instrument_id) {
1089 sub_info.cancellation_token.cancel();
1090 tracing::debug!("Unsubscribed from index prices for {}", cmd.instrument_id);
1091 } else {
1092 tracing::warn!(
1093 "No active index price subscription found for {}",
1094 cmd.instrument_id
1095 );
1096 }
1097
1098 Ok(())
1099 }
1100
1101 fn unsubscribe_option_greeks(&mut self, cmd: &UnsubscribeOptionGreeks) -> anyhow::Result<()> {
1102 tracing::debug!("Unsubscribing from option greeks for {}", cmd.instrument_id);
1103
1104 let mut subscriptions = self
1105 .option_greeks_subscriptions
1106 .try_lock()
1107 .context("Failed to lock IB option greeks subscriptions")?;
1108 if let Some(subscription_token) = subscriptions.remove(&cmd.instrument_id) {
1109 subscription_token.cancel();
1110 tracing::debug!("Unsubscribed from option greeks for {}", cmd.instrument_id);
1111 } else {
1112 tracing::warn!(
1113 "No active option greeks subscription found for {}",
1114 cmd.instrument_id
1115 );
1116 }
1117
1118 Ok(())
1119 }
1120
1121 fn subscribe_trades(&mut self, cmd: SubscribeTrades) -> anyhow::Result<()> {
1122 tracing::debug!("Subscribing to trades for {}", cmd.instrument_id);
1123
1124 let client = self
1125 .ib_client
1126 .as_ref()
1127 .context("IB client not connected. Call connect() first")?;
1128
1129 let instrument = self
1131 .instrument_provider
1132 .find(&cmd.instrument_id)
1133 .context(format!(
1134 "Instrument {} not found in provider",
1135 cmd.instrument_id
1136 ))?;
1137
1138 if matches!(instrument, InstrumentAny::CurrencyPair(_)) {
1140 tracing::error!(
1141 "Interactive Brokers does not support trades for CurrencyPair instruments: {}",
1142 cmd.instrument_id
1143 );
1144 return Ok(());
1145 }
1146
1147 let price_precision = instrument.price_precision();
1148 let size_precision = instrument.size_precision();
1149
1150 let contract = self
1152 .instrument_provider
1153 .resolve_contract_for_instrument(cmd.instrument_id)
1154 .context("Failed to convert instrument_id to IB contract")?;
1155
1156 let instrument_id = cmd.instrument_id;
1157 let data_sender = self.data_sender.clone();
1158 let clock = self.clock;
1159
1160 let subscription_token = self.cancellation_token.child_token();
1162
1163 let client_clone = client.as_arc().clone();
1165 let subscription_token_clone = subscription_token.clone();
1166
1167 let task = get_runtime().spawn(async move {
1168 if let Err(e) = handle_trade_subscription(
1169 client_clone,
1170 contract,
1171 instrument_id,
1172 price_precision,
1173 size_precision,
1174 data_sender,
1175 clock,
1176 subscription_token_clone,
1177 )
1178 .await
1179 {
1180 tracing::error!("Trade subscription error for {}: {:?}", instrument_id, e);
1181 }
1182 });
1183
1184 self.tasks.push(task);
1185
1186 let mut subscriptions = self
1188 .subscriptions
1189 .try_lock()
1190 .context("Failed to lock IB subscriptions")?;
1191 subscriptions.insert(
1192 cmd.instrument_id,
1193 SubscriptionInfo {
1194 instrument_id: cmd.instrument_id,
1195 subscription_type: SubscriptionType::Trades,
1196 cancellation_token: subscription_token,
1197 },
1198 );
1199
1200 tracing::debug!("Trade subscription started for {}", cmd.instrument_id);
1201 Ok(())
1202 }
1203
1204 fn unsubscribe_trades(&mut self, cmd: &UnsubscribeTrades) -> anyhow::Result<()> {
1205 tracing::debug!("Unsubscribing from trades for {}", cmd.instrument_id);
1206
1207 let mut subscriptions = self
1208 .subscriptions
1209 .try_lock()
1210 .context("Failed to lock IB subscriptions")?;
1211 if let Some(sub_info) = subscriptions.remove(&cmd.instrument_id) {
1212 sub_info.cancellation_token.cancel();
1213 tracing::debug!("Unsubscribed from trades for {}", cmd.instrument_id);
1214 } else {
1215 tracing::warn!(
1216 "No active trade subscription found for {}",
1217 cmd.instrument_id
1218 );
1219 }
1220
1221 Ok(())
1222 }
1223
1224 fn subscribe_bars(&mut self, cmd: SubscribeBars) -> anyhow::Result<()> {
1225 tracing::debug!("Subscribing to bars for {}", cmd.bar_type);
1226
1227 let client = self
1228 .ib_client
1229 .as_ref()
1230 .context("IB client not connected. Call connect() first")?;
1231
1232 let instrument_id = cmd.bar_type.instrument_id();
1234 let instrument = self
1235 .instrument_provider
1236 .find(&instrument_id)
1237 .context(format!("Instrument {instrument_id} not found in provider"))?;
1238
1239 let price_precision = instrument.price_precision();
1240 let size_precision = instrument.size_precision();
1241
1242 let contract = self
1244 .instrument_provider
1245 .resolve_contract_for_instrument(instrument_id)
1246 .context("Failed to convert instrument_id to IB contract")?;
1247
1248 let bar_type = cmd.bar_type;
1249 let bar_type_str = bar_type.to_string();
1250 let data_sender = self.data_sender.clone();
1251 let clock = self.clock;
1252 let last_bars = Arc::clone(&self.last_bars);
1253 let bar_timeout_tasks = Arc::clone(&self.bar_timeout_tasks);
1254 let handle_revised_bars = self.config.handle_revised_bars;
1255 let use_rth = self.config.use_regular_trading_hours;
1256 let start_ns = parse_start_ns(cmd.params.as_ref());
1257
1258 let subscription_token = self.cancellation_token.child_token();
1260
1261 let client_clone = client.as_arc().clone();
1263 let subscription_token_clone = subscription_token.clone();
1264
1265 let task = get_runtime().spawn(async move {
1266 let result = if bar_type.spec().timedelta().num_seconds() == 5 {
1267 handle_realtime_bars_subscription(
1268 client_clone,
1269 contract,
1270 bar_type,
1271 bar_type_str,
1272 instrument_id,
1273 price_precision,
1274 size_precision,
1275 data_sender,
1276 clock,
1277 last_bars,
1278 bar_timeout_tasks,
1279 handle_revised_bars,
1280 use_rth,
1281 subscription_token_clone,
1282 )
1283 .await
1284 } else {
1285 handle_historical_bars_subscription(
1286 client_clone,
1287 contract,
1288 bar_type,
1289 price_type_to_ib_what_to_show(bar_type.spec().price_type),
1290 price_precision,
1291 size_precision,
1292 use_rth,
1293 start_ns,
1294 data_sender,
1295 handle_revised_bars,
1296 clock,
1297 subscription_token_clone,
1298 )
1299 .await
1300 };
1301
1302 if let Err(e) = result {
1303 tracing::error!("Bars subscription error for {}: {:?}", bar_type, e);
1304 }
1305 });
1306
1307 self.tasks.push(task);
1308
1309 let mut subscriptions = self
1311 .subscriptions
1312 .try_lock()
1313 .context("Failed to lock IB subscriptions")?;
1314 subscriptions.insert(
1315 instrument_id,
1316 SubscriptionInfo {
1317 instrument_id,
1318 subscription_type: SubscriptionType::Bars,
1319 cancellation_token: subscription_token,
1320 },
1321 );
1322
1323 tracing::debug!("Real-time bars subscription started for {}", bar_type);
1324 Ok(())
1325 }
1326
1327 fn unsubscribe_bars(&mut self, cmd: &UnsubscribeBars) -> anyhow::Result<()> {
1328 tracing::debug!("Unsubscribing from bars for {}", cmd.bar_type);
1329
1330 let instrument_id = cmd.bar_type.instrument_id();
1331 let mut subscriptions = self
1332 .subscriptions
1333 .try_lock()
1334 .context("Failed to lock IB subscriptions")?;
1335 if let Some(sub_info) = subscriptions.remove(&instrument_id) {
1336 sub_info.cancellation_token.cancel();
1337 tracing::debug!("Unsubscribed from bars for {}", cmd.bar_type);
1338 } else {
1339 tracing::warn!("No active bar subscription found for {}", cmd.bar_type);
1340 }
1341
1342 Ok(())
1343 }
1344
1345 fn subscribe_book_deltas(&mut self, cmd: SubscribeBookDeltas) -> anyhow::Result<()> {
1346 tracing::debug!("Subscribing to book deltas for {}", cmd.instrument_id);
1347
1348 if cmd.book_type == BookType::L3_MBO {
1350 tracing::error!(
1351 "Cannot subscribe to order book deltas: L3_MBO data is not published by Interactive Brokers. Valid book types are L1_MBP, L2_MBP"
1352 );
1353 return Ok(());
1354 }
1355
1356 let client = self
1357 .ib_client
1358 .as_ref()
1359 .context("IB client not connected. Call connect() first")?;
1360
1361 let instrument = self
1363 .instrument_provider
1364 .find(&cmd.instrument_id)
1365 .context(format!(
1366 "Instrument {} not found in provider",
1367 cmd.instrument_id
1368 ))?;
1369
1370 let price_precision = instrument.price_precision();
1371 let size_precision = instrument.size_precision();
1372
1373 let contract = self
1375 .instrument_provider
1376 .resolve_contract_for_instrument(cmd.instrument_id)
1377 .context("Failed to convert instrument_id to IB contract")?;
1378
1379 let instrument_id = cmd.instrument_id;
1380 let data_sender = self.data_sender.clone();
1381 let clock = self.clock;
1382
1383 let subscription_token = self.cancellation_token.child_token();
1385
1386 let depth_rows = cmd.depth.map_or(20, |d| d.get() as i32);
1388
1389 let is_smart_depth = cmd
1391 .params
1392 .as_ref()
1393 .and_then(|params| params.get_str("is_smart_depth"))
1394 .is_none_or(parse_bool_param_value);
1395
1396 let client_clone = client.as_arc().clone();
1398 let subscription_token_clone = subscription_token.clone();
1399
1400 let task = get_runtime().spawn(async move {
1401 if let Err(e) = handle_market_depth_subscription(
1402 client_clone,
1403 contract,
1404 instrument_id,
1405 price_precision,
1406 size_precision,
1407 depth_rows,
1408 is_smart_depth,
1409 data_sender,
1410 clock,
1411 subscription_token_clone,
1412 )
1413 .await
1414 {
1415 tracing::error!(
1416 "Market depth subscription error for {}: {:?}",
1417 instrument_id,
1418 e
1419 );
1420 }
1421 });
1422
1423 self.tasks.push(task);
1424
1425 let mut subscriptions = self
1427 .subscriptions
1428 .try_lock()
1429 .context("Failed to lock IB subscriptions")?;
1430 subscriptions.insert(
1431 cmd.instrument_id,
1432 SubscriptionInfo {
1433 instrument_id: cmd.instrument_id,
1434 subscription_type: SubscriptionType::BookDeltas,
1435 cancellation_token: subscription_token,
1436 },
1437 );
1438
1439 tracing::debug!(
1440 "Market depth subscription started for {}",
1441 cmd.instrument_id
1442 );
1443 Ok(())
1444 }
1445
1446 fn unsubscribe_book_deltas(&mut self, cmd: &UnsubscribeBookDeltas) -> anyhow::Result<()> {
1447 tracing::debug!("Unsubscribing from book deltas for {}", cmd.instrument_id);
1448
1449 let mut subscriptions = self
1450 .subscriptions
1451 .try_lock()
1452 .context("Failed to lock IB subscriptions")?;
1453 if let Some(sub_info) = subscriptions.remove(&cmd.instrument_id) {
1454 sub_info.cancellation_token.cancel();
1455 tracing::debug!("Unsubscribed from book deltas for {}", cmd.instrument_id);
1456 } else {
1457 tracing::warn!(
1458 "No active book delta subscription found for {}",
1459 cmd.instrument_id
1460 );
1461 }
1462
1463 Ok(())
1464 }
1465
1466 fn request_instrument(&self, cmd: RequestInstrument) -> anyhow::Result<()> {
1468 tracing::debug!("Requesting instrument: {}", cmd.instrument_id);
1469 if cmd.start.is_some() {
1470 tracing::warn!(
1471 "Requesting instrument {} with specified `start` which has no effect",
1472 cmd.instrument_id
1473 );
1474 }
1475
1476 if cmd.end.is_some() {
1477 tracing::warn!(
1478 "Requesting instrument {} with specified `end` which has no effect",
1479 cmd.instrument_id
1480 );
1481 }
1482
1483 let force_update = cmd
1485 .params
1486 .as_ref()
1487 .and_then(|params| params.get_str("force_instrument_update"))
1488 .is_some_and(parse_bool_param_value);
1489
1490 let instrument =
1492 if force_update || self.instrument_provider.find(&cmd.instrument_id).is_none() {
1493 let client = self
1495 .ib_client
1496 .as_ref()
1497 .context("IB client not connected. Call connect() first")?;
1498 let instrument_provider = Arc::clone(&self.instrument_provider);
1499 let instrument_id = cmd.instrument_id;
1500 let data_sender = self.data_sender.clone();
1501 let clock = self.clock;
1502 let request_id = cmd.request_id;
1503 let client_id = cmd.client_id.unwrap_or(self.client_id);
1504 let params = cmd.params.clone();
1505 let start_nanos = cmd.start.map(datetime_to_unix_nanos);
1506 let end_nanos = cmd.end.map(datetime_to_unix_nanos);
1507
1508 let client_clone = client.as_arc().clone();
1509
1510 get_runtime().spawn(async move {
1511 let filters = params_to_string_filters(params.as_ref());
1512 if let Err(e) = instrument_provider
1513 .fetch_contract_details(&client_clone, instrument_id, force_update, filters)
1514 .await
1515 {
1516 tracing::error!(
1517 "Failed to fetch contract details for {}: {:?}",
1518 instrument_id,
1519 e
1520 );
1521 return;
1522 }
1523
1524 if let Some(instrument) = instrument_provider.find(&instrument_id) {
1525 let response = DataResponse::Instrument(Box::new(InstrumentResponse::new(
1526 request_id,
1527 client_id,
1528 instrument_id,
1529 instrument,
1530 start_nanos,
1531 end_nanos,
1532 clock.get_time_ns(),
1533 params,
1534 )));
1535
1536 if let Err(e) = data_sender.send(DataEvent::Response(response)) {
1537 tracing::error!("Failed to send instrument response: {e}");
1538 }
1539 }
1540 });
1541
1542 return Ok(());
1544 } else {
1545 self.instrument_provider
1547 .find(&cmd.instrument_id)
1548 .context(format!(
1549 "Instrument {} not found in provider",
1550 cmd.instrument_id
1551 ))?
1552 };
1553
1554 let start_nanos = cmd.start.map(datetime_to_unix_nanos);
1555 let end_nanos = cmd.end.map(datetime_to_unix_nanos);
1556
1557 let response = DataResponse::Instrument(Box::new(InstrumentResponse::new(
1558 cmd.request_id,
1559 cmd.client_id.unwrap_or(self.client_id),
1560 cmd.instrument_id,
1561 instrument,
1562 start_nanos,
1563 end_nanos,
1564 self.clock.get_time_ns(),
1565 cmd.params,
1566 )));
1567
1568 if let Err(e) = self.data_sender.send(DataEvent::Response(response)) {
1569 tracing::error!("Failed to send instrument response: {e}");
1570 }
1571
1572 Ok(())
1573 }
1574
1575 fn request_instruments(&self, cmd: RequestInstruments) -> anyhow::Result<()> {
1576 tracing::debug!("Requesting all instruments for venue: {:?}", cmd.venue);
1577
1578 let client = self
1579 .ib_client
1580 .as_ref()
1581 .context("IB client not connected. Call connect() first")?;
1582
1583 let force_update = cmd
1585 .params
1586 .as_ref()
1587 .and_then(|params| params.get_str("force_instrument_update"))
1588 .is_some_and(parse_bool_param_value);
1589
1590 let mut contract_specs_to_load: Vec<serde_json::Value> = Vec::new();
1592
1593 if let Some(params) = &cmd.params
1594 && let Some(ib_contracts_value) = params.get("ib_contracts")
1595 {
1596 match ib_contracts_value {
1597 serde_json::Value::Array(contract_specs) => {
1598 tracing::debug!(
1599 "Parsed {} structured contract specs from ib_contracts",
1600 contract_specs.len()
1601 );
1602 contract_specs_to_load = contract_specs.clone();
1603 }
1604 serde_json::Value::String(ib_contracts_json_str) => {
1605 match serde_json::from_str::<serde_json::Value>(ib_contracts_json_str) {
1606 Ok(serde_json::Value::Array(contract_specs)) => {
1607 tracing::debug!(
1608 "Parsed {} contract specs from ib_contracts JSON",
1609 contract_specs.len()
1610 );
1611 log::debug!("Parsed ib_contracts payload: {}", ib_contracts_json_str);
1612 contract_specs_to_load = contract_specs;
1613 }
1614 Ok(value) => {
1615 tracing::warn!(
1616 "Expected ib_contracts JSON array, received {}. Continuing without contracts",
1617 value
1618 );
1619 }
1620 Err(e) => {
1621 tracing::warn!(
1622 "Failed to parse ib_contracts JSON: {}. Continuing without contracts",
1623 e
1624 );
1625 }
1626 }
1627 }
1628 value => {
1629 tracing::warn!(
1630 "Expected ib_contracts array or JSON string, received {}. Continuing without contracts",
1631 value
1632 );
1633 }
1634 }
1635 }
1636
1637 let instrument_provider = Arc::clone(&self.instrument_provider);
1639 let client_clone = client.as_arc().clone();
1640 let data_sender = self.data_sender.clone();
1641 let clock = self.clock;
1642 let request_id = cmd.request_id;
1643 let client_id = cmd.client_id.unwrap_or(self.client_id);
1644 let venue = cmd.venue.unwrap_or(*IB_VENUE);
1645 let params = cmd.params.clone();
1646 let start_nanos = cmd.start.map(datetime_to_unix_nanos);
1647 let end_nanos = cmd.end.map(datetime_to_unix_nanos);
1648
1649 if !contract_specs_to_load.is_empty() || force_update {
1651 let contract_specs_to_load_clone = contract_specs_to_load;
1652 let return_loaded_only = !contract_specs_to_load_clone.is_empty();
1653
1654 get_runtime().spawn(async move {
1655 let mut loaded_instrument_ids = Vec::new();
1656
1657 if !contract_specs_to_load_clone.is_empty() {
1659 for contract_spec in contract_specs_to_load_clone {
1660 let contract =
1661 match crate::common::contracts::parse_contract_from_json(&contract_spec)
1662 {
1663 Ok(contract) => contract,
1664 Err(e) => {
1665 tracing::warn!(
1666 "Failed to parse IB contract spec {:?}: {}",
1667 contract_spec,
1668 e
1669 );
1670 continue;
1671 }
1672 };
1673
1674 log::debug!(
1675 "Loading instrument from IB contract spec (sec_type={:?}, symbol={}, local_symbol={}, exchange={}, expiry={})",
1676 contract.security_type,
1677 contract.symbol.as_str(),
1678 contract.local_symbol.as_str(),
1679 contract.exchange.as_str(),
1680 contract.last_trade_date_or_contract_month.as_str()
1681 );
1682
1683 match instrument_provider
1684 .load_contract_spec(&client_clone, &contract, Some(&contract_spec))
1685 .await
1686 {
1687 Ok(mut instrument_ids) => {
1688 loaded_instrument_ids.append(&mut instrument_ids);
1689 }
1690 Err(e) => {
1691 tracing::warn!(
1692 "Failed to load IB contract spec {:?}: {}",
1693 contract_spec,
1694 e
1695 );
1696 }
1697 }
1698 }
1699 }
1700
1701 if force_update && !return_loaded_only {
1703 let all_instrument_ids: Vec<InstrumentId> = instrument_provider
1704 .get_all()
1705 .into_iter()
1706 .map(|inst| inst.id())
1707 .collect();
1708
1709 if !all_instrument_ids.is_empty()
1710 && let Ok(mut reloaded_ids) = instrument_provider
1711 .batch_load(&client_clone, all_instrument_ids, None)
1712 .await
1713 {
1714 loaded_instrument_ids.append(&mut reloaded_ids);
1715 }
1716 }
1717
1718 let instruments = if return_loaded_only {
1719 instrument_provider.find_all(&loaded_instrument_ids)
1720 } else {
1721 Vec::new()
1722 };
1723 let instruments_count = instruments.len();
1724
1725 let response = DataResponse::Instruments(InstrumentsResponse::new(
1726 request_id,
1727 client_id,
1728 venue,
1729 instruments,
1730 start_nanos,
1731 end_nanos,
1732 clock.get_time_ns(),
1733 params,
1734 ));
1735
1736 if let Err(e) = data_sender.send(DataEvent::Response(response)) {
1737 tracing::error!("Failed to send instruments response: {e}");
1738 } else {
1739 tracing::debug!(
1740 "Successfully sent {} instruments response (loaded {} new instruments)",
1741 instruments_count,
1742 loaded_instrument_ids.len()
1743 );
1744 }
1745 });
1746 } else {
1747 let response = DataResponse::Instruments(InstrumentsResponse::new(
1748 cmd.request_id,
1749 cmd.client_id.unwrap_or(self.client_id),
1750 venue,
1751 Vec::new(),
1752 start_nanos,
1753 end_nanos,
1754 self.clock.get_time_ns(),
1755 cmd.params,
1756 ));
1757
1758 if let Err(e) = self.data_sender.send(DataEvent::Response(response)) {
1759 tracing::error!("Failed to send instruments response: {e}");
1760 } else {
1761 tracing::debug!("Successfully sent empty instruments response");
1762 }
1763 }
1764
1765 Ok(())
1766 }
1767
1768 fn request_quotes(&self, cmd: RequestQuotes) -> anyhow::Result<()> {
1769 tracing::debug!("Requesting quotes for {}", cmd.instrument_id);
1770
1771 let client = self
1772 .ib_client
1773 .as_ref()
1774 .context("IB client not connected. Call connect() first")?;
1775
1776 let instrument = self
1778 .instrument_provider
1779 .find(&cmd.instrument_id)
1780 .context(format!(
1781 "Instrument {} not found in provider",
1782 cmd.instrument_id
1783 ))?;
1784
1785 let price_precision = instrument.price_precision();
1786 let size_precision = instrument.size_precision();
1787
1788 let contract = self
1790 .instrument_provider
1791 .resolve_contract_for_instrument(cmd.instrument_id)
1792 .context("Failed to convert instrument_id to IB contract")?;
1793
1794 let number_of_ticks = cmd.limit.map_or(1000, |l| l.get() as i32).min(1000);
1795
1796 let instrument_id = cmd.instrument_id;
1797 let data_sender = self.data_sender.clone();
1798 let clock = self.clock;
1799 let request_id = cmd.request_id;
1800 let client_id = cmd.client_id.unwrap_or(self.client_id);
1801 let params = cmd.params.clone();
1802 let start_nanos = cmd.start.map(datetime_to_unix_nanos);
1803 let end_nanos = cmd.end.map(datetime_to_unix_nanos);
1804
1805 let client_clone = client.as_arc().clone();
1807 let limit = cmd.limit.map(|l| l.get());
1808 let start_nanos_clone = start_nanos;
1809 let end_nanos_clone = end_nanos;
1810 let cmd_start = cmd.start;
1811 let cmd_end = cmd.end;
1812 let trading_hours = request_trading_hours(self.config.use_regular_trading_hours);
1813 let price_magnifier = self.instrument_provider.get_price_magnifier(&instrument_id);
1814
1815 get_runtime().spawn(async move {
1816 let mut all_quotes = Vec::new();
1817 let mut current_end_date = cmd_end;
1819 if current_end_date.is_none() {
1820 current_end_date = Some(chrono::Utc::now());
1821 }
1822 let current_start_date = cmd_start;
1823
1824 loop {
1825 if !should_continue_historical_tick_pagination(
1826 current_start_date,
1827 current_end_date,
1828 all_quotes.len(),
1829 limit,
1830 ) {
1831 break;
1832 }
1833
1834 let current_end_ib = current_end_date.as_ref().map(chrono_to_ib_datetime);
1835
1836 let mut builder = client_clone
1838 .historical_ticks(&contract, number_of_ticks)
1839 .trading_hours(trading_hours);
1840
1841 if let Some(start) = current_start_date.as_ref().map(chrono_to_ib_datetime) {
1842 builder = builder.starting(start);
1843 }
1844
1845 if let Some(end) = current_end_ib {
1846 builder = builder.ending(end);
1847 }
1848
1849 match builder.bid_ask(IgnoreSize::No).await {
1850 Ok(mut subscription) => {
1851 let mut batch_quotes = Vec::new();
1852
1853 while let Some(tick) = subscription.next().await {
1854 let ts_event =
1855 super::convert::ib_timestamp_to_unix_nanos(&tick.timestamp);
1856 let ts_init = clock.get_time_ns();
1857
1858 match super::parse::parse_quote_tick(
1859 instrument_id,
1860 Some(apply_price_magnifier(tick.price_bid, price_magnifier)),
1861 Some(apply_price_magnifier(tick.price_ask, price_magnifier)),
1862 Some(tick.size_bid as f64),
1863 Some(tick.size_ask as f64),
1864 price_precision,
1865 size_precision,
1866 ts_event,
1867 ts_init,
1868 ) {
1869 Ok(quote_tick) => batch_quotes.push(quote_tick),
1870 Err(e) => {
1871 tracing::warn!("Failed to parse quote tick: {:?}", e);
1872 }
1873 }
1874 }
1875
1876 if !extend_historical_tick_batch(
1877 &mut all_quotes,
1878 batch_quotes,
1879 current_start_date,
1880 &mut current_end_date,
1881 start_nanos_clone,
1882 end_nanos_clone,
1883 limit,
1884 |quote| quote.ts_event,
1885 ) {
1886 break;
1887 }
1888 }
1889 Err(e) => {
1890 tracing::error!(
1891 "Historical quotes request failed for {}: {:?}",
1892 instrument_id,
1893 e
1894 );
1895 break;
1896 }
1897 }
1898 }
1899
1900 retain_historical_ticks_in_range(
1901 &mut all_quotes,
1902 start_nanos_clone,
1903 end_nanos_clone,
1904 |quote| quote.ts_event,
1905 );
1906
1907 all_quotes.sort_by_key(|q| q.ts_event);
1908 if let Some(limit) = limit
1909 && all_quotes.len() > limit
1910 {
1911 all_quotes = all_quotes.split_off(all_quotes.len() - limit);
1912 }
1913
1914 let quotes_count = all_quotes.len();
1915 let response = DataResponse::Quotes(QuotesResponse::new(
1916 request_id,
1917 client_id,
1918 instrument_id,
1919 all_quotes,
1920 start_nanos_clone,
1921 end_nanos_clone,
1922 clock.get_time_ns(),
1923 params,
1924 ));
1925
1926 if let Err(e) = data_sender.send(DataEvent::Response(response)) {
1927 tracing::error!("Failed to send quotes response: {e}");
1928 } else {
1929 tracing::debug!(
1930 "Successfully sent {} quotes for {}",
1931 quotes_count,
1932 instrument_id
1933 );
1934 }
1935 });
1936
1937 Ok(())
1938 }
1939
1940 fn request_trades(&self, cmd: RequestTrades) -> anyhow::Result<()> {
1941 tracing::debug!("Requesting trades for {}", cmd.instrument_id);
1942
1943 let client = self
1944 .ib_client
1945 .as_ref()
1946 .context("IB client not connected. Call connect() first")?;
1947
1948 let instrument = self
1950 .instrument_provider
1951 .find(&cmd.instrument_id)
1952 .context(format!(
1953 "Instrument {} not found in provider",
1954 cmd.instrument_id
1955 ))?;
1956
1957 if matches!(instrument, InstrumentAny::CurrencyPair(_)) {
1959 tracing::error!(
1960 "Interactive Brokers does not support trades for CurrencyPair instruments: {}",
1961 cmd.instrument_id
1962 );
1963 return Ok(());
1964 }
1965
1966 let price_precision = instrument.price_precision();
1967 let size_precision = instrument.size_precision();
1968
1969 let contract = self
1971 .instrument_provider
1972 .resolve_contract_for_instrument(cmd.instrument_id)
1973 .context("Failed to convert instrument_id to IB contract")?;
1974
1975 let number_of_ticks = cmd.limit.map_or(1000, |l| l.get() as i32).min(1000);
1976
1977 let instrument_id = cmd.instrument_id;
1978 let data_sender = self.data_sender.clone();
1979 let clock = self.clock;
1980 let request_id = cmd.request_id;
1981 let client_id = cmd.client_id.unwrap_or(self.client_id);
1982 let params = cmd.params.clone();
1983 let start_nanos = cmd.start.map(datetime_to_unix_nanos);
1984 let end_nanos = cmd.end.map(datetime_to_unix_nanos);
1985
1986 let client_clone = client.as_arc().clone();
1988 let limit = cmd.limit.map(|l| l.get());
1989 let start_nanos_clone = start_nanos;
1990 let end_nanos_clone = end_nanos;
1991 let cmd_start = cmd.start;
1992 let cmd_end = cmd.end;
1993 let trading_hours = request_trading_hours(self.config.use_regular_trading_hours);
1994 let price_magnifier = self.instrument_provider.get_price_magnifier(&instrument_id);
1995
1996 get_runtime().spawn(async move {
1997 let mut all_trades = Vec::new();
1998 let mut current_end_date = cmd_end;
2000 if current_end_date.is_none() {
2001 current_end_date = Some(chrono::Utc::now());
2002 }
2003 let current_start_date = cmd_start;
2004
2005 loop {
2006 if !should_continue_historical_tick_pagination(
2007 current_start_date,
2008 current_end_date,
2009 all_trades.len(),
2010 limit,
2011 ) {
2012 break;
2013 }
2014
2015 let current_end_ib = current_end_date.as_ref().map(chrono_to_ib_datetime);
2016
2017 let mut builder = client_clone
2019 .historical_ticks(&contract, number_of_ticks)
2020 .trading_hours(trading_hours);
2021
2022 if let Some(start) = current_start_date.as_ref().map(chrono_to_ib_datetime) {
2023 builder = builder.starting(start);
2024 }
2025
2026 if let Some(end) = current_end_ib {
2027 builder = builder.ending(end);
2028 }
2029
2030 match builder.trade().await {
2031 Ok(mut subscription) => {
2032 let mut batch_trades = Vec::new();
2033
2034 while let Some(tick) = subscription.next().await {
2035 let ts_event =
2036 super::convert::ib_timestamp_to_unix_nanos(&tick.timestamp);
2037 let ts_init = clock.get_time_ns();
2038
2039 let trade_id = None;
2041
2042 match super::parse::parse_trade_tick(
2043 instrument_id,
2044 apply_price_magnifier(tick.price, price_magnifier),
2045 tick.size as f64,
2046 price_precision,
2047 size_precision,
2048 ts_event,
2049 ts_init,
2050 trade_id,
2051 ) {
2052 Ok(trade_tick) => batch_trades.push(trade_tick),
2053 Err(e) => {
2054 tracing::warn!("Failed to parse trade tick: {:?}", e);
2055 }
2056 }
2057 }
2058
2059 if !extend_historical_tick_batch(
2060 &mut all_trades,
2061 batch_trades,
2062 current_start_date,
2063 &mut current_end_date,
2064 start_nanos_clone,
2065 end_nanos_clone,
2066 limit,
2067 |trade| trade.ts_event,
2068 ) {
2069 break;
2070 }
2071 }
2072 Err(e) => {
2073 tracing::error!(
2074 "Historical trades request failed for {}: {:?}",
2075 instrument_id,
2076 e
2077 );
2078 break;
2079 }
2080 }
2081 }
2082
2083 retain_historical_ticks_in_range(
2084 &mut all_trades,
2085 start_nanos_clone,
2086 end_nanos_clone,
2087 |trade| trade.ts_event,
2088 );
2089
2090 all_trades.sort_by_key(|t| t.ts_event);
2091 if let Some(limit) = limit
2092 && all_trades.len() > limit
2093 {
2094 all_trades = all_trades.split_off(all_trades.len() - limit);
2095 }
2096
2097 let trades_count = all_trades.len();
2098 let response = DataResponse::Trades(TradesResponse::new(
2099 request_id,
2100 client_id,
2101 instrument_id,
2102 all_trades,
2103 start_nanos_clone,
2104 end_nanos_clone,
2105 clock.get_time_ns(),
2106 params,
2107 ));
2108
2109 if let Err(e) = data_sender.send(DataEvent::Response(response)) {
2110 tracing::error!("Failed to send trades response: {e}");
2111 } else {
2112 tracing::debug!(
2113 "Successfully sent {} trades for {}",
2114 trades_count,
2115 instrument_id
2116 );
2117 }
2118 });
2119
2120 Ok(())
2121 }
2122
2123 fn request_bars(&self, cmd: RequestBars) -> anyhow::Result<()> {
2124 tracing::debug!("Requesting bars for {}", cmd.bar_type);
2125
2126 if !cmd.bar_type.spec().is_time_aggregated() {
2128 tracing::error!(
2129 "Cannot request {} bars: only time bars are aggregated by Interactive Brokers",
2130 cmd.bar_type
2131 );
2132 return Ok(());
2133 }
2134
2135 let client = self
2136 .ib_client
2137 .as_ref()
2138 .context("IB client not connected. Call connect() first")?;
2139
2140 let instrument_id = cmd.bar_type.instrument_id();
2142 let instrument = self
2143 .instrument_provider
2144 .find(&instrument_id)
2145 .context(format!("Instrument {instrument_id} not found in provider"))?;
2146
2147 let price_precision = instrument.price_precision();
2148 let size_precision = instrument.size_precision();
2149
2150 let contract = self
2152 .instrument_provider
2153 .resolve_contract_for_instrument(instrument_id)
2154 .context("Failed to convert instrument_id to IB contract")?;
2155
2156 let ib_bar_size = bar_type_to_ib_bar_size(&cmd.bar_type)
2158 .context("Failed to convert bar type to IB bar size")?;
2159 let ib_what_to_show = price_type_to_ib_what_to_show(cmd.bar_type.spec().price_type);
2160
2161 let segments = if let (Some(start), Some(end)) = (cmd.start, cmd.end) {
2163 calculate_duration_segments(start, end)
2164 } else {
2165 let end_date = cmd.end.unwrap_or_else(chrono::Utc::now);
2166 let duration = calculate_duration(cmd.start, cmd.end).unwrap_or_else(|_| 1i32.days());
2167 vec![(end_date, duration)]
2168 };
2169
2170 let bar_type = cmd.bar_type;
2171 let data_sender = self.data_sender.clone();
2172 let clock = self.clock;
2173 let request_id = cmd.request_id;
2174 let client_id = cmd.client_id.unwrap_or(self.client_id);
2175 let params = cmd.params.clone();
2176 let start_nanos = cmd.start.map(datetime_to_unix_nanos);
2177 let end_nanos = cmd.end.map(datetime_to_unix_nanos);
2178 let limit = cmd.limit.map(|limit| limit.get());
2179 let price_magnifier = self.instrument_provider.get_price_magnifier(&instrument_id);
2180
2181 let client_clone = client.as_arc().clone();
2183 let trading_hours = request_trading_hours(self.config.use_regular_trading_hours);
2184
2185 get_runtime().spawn(async move {
2186 let mut all_bars = Vec::new();
2187
2188 for (seg_end, seg_duration) in segments {
2189 let end_ib = chrono_to_ib_datetime(&seg_end);
2190
2191 match client_clone
2192 .historical_data(&contract, ib_bar_size)
2193 .ending(end_ib)
2194 .duration(seg_duration)
2195 .what_to_show(ib_what_to_show)
2196 .trading_hours(trading_hours)
2197 .fetch()
2198 .await
2199 {
2200 Ok(historical_data) => {
2201 for ib_bar in &historical_data.bars {
2203 let ib_bar = apply_bar_price_magnifier(ib_bar, price_magnifier);
2204 match ib_bar_to_nautilus_bar(
2205 &ib_bar,
2206 bar_type,
2207 price_precision,
2208 size_precision,
2209 ) {
2210 Ok(bar) => all_bars.push(bar),
2211 Err(e) => {
2212 tracing::warn!(
2213 "Failed to convert IB bar to Nautilus bar: {:?}",
2214 e
2215 );
2216 }
2217 }
2218 }
2219 }
2220 Err(e) => {
2221 tracing::error!(
2222 "Historical data request failed for {} segment: {:?}",
2223 bar_type,
2224 e
2225 );
2226 }
2229 }
2230 }
2231
2232 if all_bars.is_empty() {
2234 tracing::warn!("No bar data received for {}", bar_type);
2235 }
2236
2237 all_bars.sort_by_key(|b| b.ts_event);
2239 all_bars.dedup();
2240
2241 if let Some(limit) = limit
2242 && all_bars.len() > limit
2243 {
2244 all_bars = all_bars.split_off(all_bars.len() - limit);
2245 }
2246 let bars_count = all_bars.len();
2247
2248 let response = DataResponse::Bars(BarsResponse::new(
2249 request_id,
2250 client_id,
2251 bar_type,
2252 all_bars,
2253 start_nanos,
2254 end_nanos,
2255 clock.get_time_ns(),
2256 params,
2257 ));
2258
2259 if let Err(e) = data_sender.send(DataEvent::Response(response)) {
2260 tracing::error!("Failed to send bars response: {e}");
2261 } else {
2262 tracing::debug!(
2263 "Successfully sent {} bars for {} (segmented)",
2264 bars_count,
2265 bar_type
2266 );
2267 }
2268 });
2269
2270 Ok(())
2271 }
2272}
2273
2274impl InteractiveBrokersDataClient {
2275 fn clear_bar_tracking_state(&self) {
2276 if let Ok(mut tasks) = self.bar_timeout_tasks.try_lock() {
2277 for task in tasks.values() {
2278 task.abort();
2279 }
2280 tasks.clear();
2281 } else {
2282 tracing::warn!("Failed to lock IB bar timeout tasks for cleanup");
2283 }
2284
2285 if let Ok(mut last_bars) = self.last_bars.try_lock() {
2286 last_bars.clear();
2287 } else {
2288 tracing::warn!("Failed to lock IB last bars for cleanup");
2289 }
2290 }
2291}
2292
2293impl Drop for InteractiveBrokersDataClient {
2294 fn drop(&mut self) {
2295 let _ = self.stop();
2296 }
2297}
2298
2299#[cfg(test)]
2300mod tests {
2301 use rstest::rstest;
2302
2303 use super::*;
2304 use crate::common::consts::IB_CLIENT_ID;
2305
2306 #[rstest]
2307 #[case(true, ibapi::market_data::TradingHours::Regular)]
2308 #[case(false, ibapi::market_data::TradingHours::Extended)]
2309 fn test_request_trading_hours_uses_config(
2310 #[case] use_regular_trading_hours: bool,
2311 #[case] expected: ibapi::market_data::TradingHours,
2312 ) {
2313 assert_eq!(request_trading_hours(use_regular_trading_hours), expected);
2314 }
2315
2316 #[rstest]
2317 fn test_retreat_historical_tick_end_datetime_subtracts_one_millisecond() {
2318 let result = retreat_historical_tick_end_datetime(1_234_567_890).unwrap();
2319
2320 assert_eq!(result.timestamp_nanos_opt().unwrap() as u64, 1_233_567_890);
2321 }
2322
2323 #[rstest]
2324 fn test_retreat_historical_tick_end_datetime_saturates_at_zero() {
2325 let result = retreat_historical_tick_end_datetime(0).unwrap();
2326
2327 assert_eq!(result.timestamp_nanos_opt().unwrap(), 0);
2328 }
2329
2330 #[rstest]
2331 #[case(None, Some(chrono::DateTime::from_timestamp(2, 0).unwrap()), 0, Some(10), true)]
2332 #[case(Some(chrono::DateTime::from_timestamp(1, 0).unwrap()), Some(chrono::DateTime::from_timestamp(2, 0).unwrap()), 0, Some(10), true)]
2333 #[case(Some(chrono::DateTime::from_timestamp(2, 0).unwrap()), Some(chrono::DateTime::from_timestamp(1, 0).unwrap()), 0, Some(10), false)]
2334 #[case(Some(chrono::DateTime::from_timestamp(1, 0).unwrap()), Some(chrono::DateTime::from_timestamp(2, 0).unwrap()), 10, Some(10), false)]
2335 #[case(Some(chrono::DateTime::from_timestamp(1, 0).unwrap()), Some(chrono::DateTime::from_timestamp(2, 0).unwrap()), 10, None, true)]
2336 fn test_should_continue_historical_tick_pagination(
2337 #[case] start: Option<chrono::DateTime<chrono::Utc>>,
2338 #[case] end: Option<chrono::DateTime<chrono::Utc>>,
2339 #[case] current_len: usize,
2340 #[case] limit: Option<usize>,
2341 #[case] expected: bool,
2342 ) {
2343 assert_eq!(
2344 should_continue_historical_tick_pagination(start, end, current_len, limit),
2345 expected
2346 );
2347 }
2348
2349 #[rstest]
2350 #[case("true", true)]
2351 #[case("True", true)]
2352 #[case("1", true)]
2353 #[case("false", false)]
2354 #[case("False", false)]
2355 #[case("0", false)]
2356 fn test_parse_bool_param_value(#[case] value: &str, #[case] expected: bool) {
2357 assert_eq!(parse_bool_param_value(value), expected);
2358 }
2359
2360 #[rstest]
2361 fn test_datetime_to_unix_nanos() {
2362 let dt = chrono::DateTime::from_timestamp(1, 2).unwrap();
2363
2364 assert_eq!(datetime_to_unix_nanos(dt), UnixNanos::from(1_000_000_002));
2365 }
2366
2367 #[rstest]
2368 fn test_stop_refreshes_cancellation_token_for_restart() {
2369 let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel();
2370 nautilus_common::live::runner::replace_data_event_sender(sender);
2371
2372 let config = InteractiveBrokersDataClientConfig::default();
2373 let provider = Arc::new(InteractiveBrokersInstrumentProvider::new(
2374 config.instrument_provider.clone(),
2375 ));
2376 let mut client =
2377 InteractiveBrokersDataClient::new(*IB_CLIENT_ID, config, provider).unwrap();
2378
2379 client.stop().unwrap();
2380
2381 assert!(!client.cancellation_token.is_cancelled());
2382 assert!(!client.cancellation_token.child_token().is_cancelled());
2383 }
2384
2385 #[rstest]
2386 fn test_stop_clears_bar_tracking_state() {
2387 let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel();
2388 nautilus_common::live::runner::replace_data_event_sender(sender);
2389
2390 let config = InteractiveBrokersDataClientConfig::default();
2391 let provider = Arc::new(InteractiveBrokersInstrumentProvider::new(
2392 config.instrument_provider.clone(),
2393 ));
2394 let mut client =
2395 InteractiveBrokersDataClient::new(*IB_CLIENT_ID, config, provider).unwrap();
2396 let bar_type = "AAPL.NASDAQ-1-MINUTE-LAST-EXTERNAL".to_string();
2397
2398 let task = get_runtime().spawn(async {
2399 std::future::pending::<()>().await;
2400 });
2401
2402 get_runtime().block_on(async {
2403 client
2404 .bar_timeout_tasks
2405 .lock()
2406 .await
2407 .insert(bar_type.clone(), task);
2408 client.last_bars.lock().await.insert(
2409 bar_type.clone(),
2410 ibapi::market_data::realtime::Bar {
2411 date: time::OffsetDateTime::from_unix_timestamp(1).unwrap(),
2412 open: 1.0,
2413 high: 1.0,
2414 low: 1.0,
2415 close: 1.0,
2416 volume: 1.0,
2417 wap: 1.0,
2418 count: 1,
2419 },
2420 );
2421 });
2422
2423 client.stop().unwrap();
2424
2425 get_runtime().block_on(async {
2426 assert!(client.bar_timeout_tasks.lock().await.is_empty());
2427 assert!(client.last_bars.lock().await.is_empty());
2428 });
2429 }
2430}