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