1use std::{fmt::Debug, str::FromStr, sync::Arc};
19
20use anyhow::Context;
21use ibapi::{
22 client::Client,
23 contracts::{Contract, SecurityType},
24 market_data::{IgnoreSize, TradingHours, historical},
25 prelude::{StreamExt, SubscriptionItemStreamExt},
26};
27use jiff::Timestamp;
28use nautilus_core::UnixNanos;
29use nautilus_model::{
30 data::{Bar, BarSpecification, BarType, Data, QuoteTick},
31 enums::{AggregationSource, BarAggregation, PriceType},
32 identifiers::InstrumentId,
33 instruments::{Instrument, any::InstrumentAny},
34 types::{Price, Quantity},
35};
36
37use crate::{
38 common::{
39 enums::IbHistoricalTickType,
40 shared_client::{self, SharedClientHandle},
41 symbology::is_crypto_contract,
42 },
43 config::{InteractiveBrokersDataClientConfig, MarketDataType},
44 data::{
45 convert::{
46 apply_bar_price_magnifier, apply_price_magnifier, bar_request_segments,
47 bar_type_to_ib_bar_size, calculate_duration_segments, extend_historical_tick_batch,
48 ib_bar_to_nautilus_bar, ib_timestamp_to_unix_nanos, jiff_to_ib_datetime,
49 price_type_to_ib_what_to_show_for_security, retain_historical_ticks_in_range,
50 },
51 parse::parse_trade_tick,
52 },
53 providers::instruments::InteractiveBrokersInstrumentProvider,
54};
55
56const HISTORICAL_TICK_DEFAULT_LIMIT: usize = 10_000;
57
58#[cfg_attr(
63 feature = "python",
64 pyo3::pyclass(
65 module = "nautilus_trader.adapters.interactive_brokers",
66 subclass,
67 from_py_object
68 )
69)]
70#[cfg_attr(
71 feature = "python",
72 pyo3_stub_gen::derive::gen_stub_pyclass(
73 module = "nautilus_trader.adapters.interactive_brokers"
74 )
75)]
76pub struct HistoricalInteractiveBrokersClient {
77 ib_client: Arc<Client>,
79 instrument_provider: Arc<InteractiveBrokersInstrumentProvider>,
81 _shared_client: Option<Arc<SharedClientHandle>>,
83}
84
85impl Clone for HistoricalInteractiveBrokersClient {
86 fn clone(&self) -> Self {
87 Self {
88 ib_client: Arc::clone(&self.ib_client),
89 instrument_provider: Arc::clone(&self.instrument_provider),
90 _shared_client: self._shared_client.clone(),
91 }
92 }
93}
94
95impl Debug for HistoricalInteractiveBrokersClient {
96 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97 f.debug_struct(stringify!(HistoricalInteractiveBrokersClient))
98 .field("ib_client", &"<Client>")
99 .field("instrument_provider", &"<InstrumentProvider>")
100 .finish()
101 }
102}
103
104impl HistoricalInteractiveBrokersClient {
105 pub fn new(
107 ib_client: Arc<Client>,
108 instrument_provider: Arc<InteractiveBrokersInstrumentProvider>,
109 ) -> Self {
110 Self {
111 ib_client,
112 instrument_provider,
113 _shared_client: None,
114 }
115 }
116
117 pub async fn connect(config: InteractiveBrokersDataClientConfig) -> anyhow::Result<Self> {
126 let instrument_provider = Arc::new(InteractiveBrokersInstrumentProvider::new(
127 config.instrument_provider.clone(),
128 ));
129 let shared_client = shared_client::get_or_connect(
130 &config.host,
131 config.port,
132 config.client_id,
133 config.connection_timeout,
134 )
135 .await?;
136 let client = shared_client.as_arc();
137
138 if config.market_data_type != MarketDataType::Realtime {
139 let market_data_type: ibapi::market_data::MarketDataType =
140 config.market_data_type.into();
141 client.switch_market_data_type(market_data_type).await?;
142 }
143
144 instrument_provider
145 .initialize_with_client(client.as_ref())
146 .await?;
147
148 Ok(Self::from_shared_client(shared_client, instrument_provider))
149 }
150
151 pub(crate) fn from_shared_client(
152 shared_client: SharedClientHandle,
153 instrument_provider: Arc<InteractiveBrokersInstrumentProvider>,
154 ) -> Self {
155 let ib_client = Arc::clone(shared_client.as_arc());
156
157 Self {
158 ib_client,
159 instrument_provider,
160 _shared_client: Some(Arc::new(shared_client)),
161 }
162 }
163
164 pub async fn connect_with_provider(
174 instrument_provider: InteractiveBrokersInstrumentProvider,
175 config: InteractiveBrokersDataClientConfig,
176 ) -> anyhow::Result<Self> {
177 let shared_client = shared_client::get_or_connect(
178 &config.host,
179 config.port,
180 config.client_id,
181 config.connection_timeout,
182 )
183 .await?;
184 let client = shared_client.as_arc();
185
186 if config.market_data_type != MarketDataType::Realtime {
187 let market_data_type: ibapi::market_data::MarketDataType =
188 config.market_data_type.into();
189 client.switch_market_data_type(market_data_type).await?;
190 }
191
192 instrument_provider
193 .initialize_with_client(client.as_ref())
194 .await?;
195
196 Ok(Self::from_shared_client(
197 shared_client,
198 Arc::new(instrument_provider),
199 ))
200 }
201
202 #[allow(clippy::too_many_arguments)]
217 pub async fn request_bars(
218 &self,
219 bar_specifications: Vec<&str>,
220 end_date_time: Timestamp,
221 start_date_time: Option<Timestamp>,
222 duration: Option<&str>,
223 contracts: Option<Vec<Contract>>,
224 instrument_ids: Option<Vec<InstrumentId>>,
225 use_rth: bool,
226 timeout: u64,
227 ) -> anyhow::Result<Vec<Bar>> {
228 if start_date_time.is_some() && duration.is_some() {
230 anyhow::bail!("Either start_date_time or duration should be provided, not both");
231 }
232
233 if let Some(start) = start_date_time
234 && start >= end_date_time
235 {
236 anyhow::bail!("Start date must be before end date");
237 }
238
239 let duration = duration
240 .map(str::parse::<historical::Duration>)
241 .transpose()
242 .with_context(|| {
243 format!(
244 "duration must be in format: 'int S|D|W|M|Y', was '{}'",
245 duration.unwrap_or_default()
246 )
247 })?;
248
249 let contracts = contracts.unwrap_or_default();
250 let instrument_ids = instrument_ids.unwrap_or_default();
251
252 if contracts.is_empty() && instrument_ids.is_empty() {
253 anyhow::bail!("Either contracts or instrument_ids must be provided");
254 }
255
256 let mut all_contracts = contracts;
258
259 for instrument_id in instrument_ids {
260 if self.instrument_provider.find(&instrument_id).is_none() {
262 if let Err(e) = self
264 .instrument_provider
265 .fetch_contract_details(&self.ib_client, instrument_id, false, None)
266 .await
267 {
268 tracing::warn!(
269 "Failed to auto-fetch contract details for {}: {}",
270 instrument_id,
271 e
272 );
273 }
274 }
275
276 if let Ok(contract) = self
278 .instrument_provider
279 .resolve_contract_for_instrument_async(&self.ib_client, instrument_id)
280 .await
281 {
282 all_contracts.push(contract);
283 } else {
284 tracing::warn!(
285 "Failed to convert instrument_id {} to IB contract, skipping",
286 instrument_id
287 );
288 }
289 }
290
291 for contract in &all_contracts {
293 if let Some(instrument_id) = self
294 .instrument_provider
295 .get_instrument_id_by_contract_id(contract.contract_id)
296 && self.instrument_provider.find(&instrument_id).is_none()
297 && let Err(e) = self
298 .instrument_provider
299 .fetch_contract_details(&self.ib_client, instrument_id, false, None)
300 .await
301 {
302 tracing::warn!(
303 "Failed to auto-fetch contract details for contract ID {}: {}",
304 contract.contract_id,
305 e
306 );
307 }
308 }
309
310 if all_contracts.is_empty() {
311 anyhow::bail!("No valid contracts found after conversion");
312 }
313
314 let trading_hours = if use_rth {
315 TradingHours::Regular
316 } else {
317 TradingHours::Extended
318 };
319
320 let mut all_bars = Vec::new();
321
322 for contract in all_contracts {
323 for bar_spec_str in &bar_specifications {
324 let parts: Vec<&str> = bar_spec_str.split('-').collect();
326 if parts.len() != 3 {
327 anyhow::bail!("Invalid bar specification format: {bar_spec_str}");
328 }
329
330 let step = parts[0].parse::<usize>()?;
331 let aggregation = parts[1].to_lowercase();
332 let price_type = parts[2].to_uppercase();
333 let price_type = PriceType::from_str(&price_type)
334 .with_context(|| format!("Invalid bar price type: {}", parts[2]))?;
335
336 let bar_spec = match aggregation.as_str() {
337 "second" => BarSpecification::new(step, BarAggregation::Second, price_type),
338 "minute" => BarSpecification::new(step, BarAggregation::Minute, price_type),
339 "hour" => BarSpecification::new(step, BarAggregation::Hour, price_type),
340 "day" => BarSpecification::new(step, BarAggregation::Day, price_type),
341 "week" => BarSpecification::new(step, BarAggregation::Week, price_type),
342 _ => anyhow::bail!("Unsupported aggregation: {aggregation}"),
343 };
344
345 let instrument_id = self.resolve_instrument_id(&contract).await?;
346 let bar_type_with_id =
347 BarType::new(instrument_id, bar_spec, AggregationSource::External);
348
349 let ib_bar_size = bar_type_to_ib_bar_size(&bar_type_with_id)?;
353 let is_crypto = is_crypto_contract(&contract);
354 let ib_what_to_show =
355 price_type_to_ib_what_to_show_for_security(bar_spec.price_type, is_crypto);
356
357 let is_continuous_future = contract.security_type == SecurityType::ContinuousFuture;
359 let segments = bar_request_segments(
360 calculate_duration_segments(start_date_time, Some(end_date_time), duration),
361 is_continuous_future,
362 );
363
364 for (segment_end, segment_duration) in segments {
365 tracing::debug!(
366 "Requesting historical bars ending on {:?} with duration {}",
367 segment_end,
368 segment_duration
369 );
370
371 let mut request = self
372 .ib_client
373 .historical_data(&contract, ib_bar_size)
374 .duration(segment_duration)
375 .what_to_show(ib_what_to_show)
376 .trading_hours(trading_hours);
377
378 if let Some(end) = segment_end {
379 request = request.ending(jiff_to_ib_datetime(&end));
380 }
381
382 let historical_data = tokio::time::timeout(
383 std::time::Duration::from_secs(timeout),
384 request.fetch(),
385 )
386 .await
387 .context(format!(
388 "Historical data request timed out after {timeout} seconds"
389 ))??;
390
391 let instrument =
392 self.instrument_provider
393 .find(&instrument_id)
394 .with_context(|| {
395 format!("Instrument {instrument_id} is missing from the provider")
396 })?;
397
398 let price_precision = instrument.price_precision();
399 let size_precision = instrument.size_precision();
400 let price_magnifier =
401 self.instrument_provider.get_price_magnifier(&instrument_id);
402
403 for ib_bar in &historical_data.bars {
405 let ib_bar = apply_bar_price_magnifier(ib_bar, price_magnifier);
406 let nautilus_bar = ib_bar_to_nautilus_bar(
407 &ib_bar,
408 bar_type_with_id,
409 price_precision,
410 size_precision,
411 )?;
412 all_bars.push(nautilus_bar);
413 }
414
415 tracing::debug!("Retrieved {} bars in batch", historical_data.bars.len());
416 }
417 }
418 }
419
420 all_bars.sort_by_key(|b| b.ts_event);
422
423 Ok(all_bars)
424 }
425
426 #[allow(clippy::too_many_arguments)]
432 pub async fn request_ticks(
433 &self,
434 tick_type: IbHistoricalTickType,
435 start_date_time: Timestamp,
436 end_date_time: Timestamp,
437 contracts: Option<Vec<Contract>>,
438 instrument_ids: Option<Vec<InstrumentId>>,
439 use_rth: bool,
440 timeout: u64,
441 limit: usize,
442 ) -> anyhow::Result<Vec<Data>> {
443 if start_date_time >= end_date_time {
444 anyhow::bail!("Start date must be before end date");
445 }
446
447 let limit = Some(if limit > 0 {
448 limit
449 } else {
450 HISTORICAL_TICK_DEFAULT_LIMIT
451 });
452
453 if end_date_time.duration_since(start_date_time) > jiff::SignedDuration::from_hours(24) {
454 tracing::warn!(
455 "Requesting tick data for more than 1 day may take a long time, particularly for liquid instruments"
456 );
457 }
458
459 let contracts = contracts.unwrap_or_default();
460 let instrument_ids = instrument_ids.unwrap_or_default();
461
462 if contracts.is_empty() && instrument_ids.is_empty() {
463 anyhow::bail!("Either contracts or instrument_ids must be provided");
464 }
465
466 let trading_hours = if use_rth {
467 TradingHours::Regular
468 } else {
469 TradingHours::Extended
470 };
471
472 let mut all_contracts = contracts;
474
475 for instrument_id in instrument_ids {
476 if self.instrument_provider.find(&instrument_id).is_none()
478 && let Err(e) = self
479 .instrument_provider
480 .fetch_contract_details(&self.ib_client, instrument_id, false, None)
481 .await
482 {
483 tracing::warn!(
484 "Failed to auto-fetch contract details for {}: {}",
485 instrument_id,
486 e
487 );
488 }
489
490 if let Ok(contract) = self
491 .instrument_provider
492 .resolve_contract_for_instrument_async(&self.ib_client, instrument_id)
493 .await
494 {
495 all_contracts.push(contract);
496 } else {
497 tracing::warn!(
498 "Failed to convert instrument_id {} to IB contract, skipping",
499 instrument_id
500 );
501 }
502 }
503
504 for contract in &all_contracts {
506 if let Some(instrument_id) = self
507 .instrument_provider
508 .get_instrument_id_by_contract_id(contract.contract_id)
509 && self.instrument_provider.find(&instrument_id).is_none()
510 && let Err(e) = self
511 .instrument_provider
512 .fetch_contract_details(&self.ib_client, instrument_id, false, None)
513 .await
514 {
515 tracing::warn!(
516 "Failed to auto-fetch contract details for contract ID {}: {}",
517 contract.contract_id,
518 e
519 );
520 }
521 }
522
523 if all_contracts.is_empty() {
524 anyhow::bail!("No valid contracts found after conversion");
525 }
526
527 let mut all_ticks = Vec::new();
528
529 for contract in all_contracts {
530 let instrument_id = self.resolve_instrument_id(&contract).await?;
531
532 let instrument = self
533 .instrument_provider
534 .find(&instrument_id)
535 .with_context(|| {
536 format!("Instrument {instrument_id} is missing from the provider")
537 })?;
538
539 let price_precision = instrument.price_precision();
540 let size_precision = instrument.size_precision();
541 let price_magnifier = self.instrument_provider.get_price_magnifier(&instrument_id);
542 let mut contract_ticks = Vec::new();
543
544 let mut current_end_date = Some(end_date_time);
546 let current_start_date = Some(start_date_time);
547 let start_date_time_ns = UnixNanos::from(
548 u64::try_from(start_date_time.as_nanosecond())
549 .context("Historical tick start date precedes the Unix epoch")?,
550 );
551 let end_date_time_ns = UnixNanos::from(
552 u64::try_from(end_date_time.as_nanosecond())
553 .context("Historical tick end date precedes the Unix epoch")?,
554 );
555
556 match tick_type {
557 IbHistoricalTickType::Trades => {
558 while let Some(request_end) = current_end_date {
559 let subscription = tokio::time::timeout(
561 std::time::Duration::from_secs(timeout),
562 self.ib_client
563 .historical_ticks(&contract, 1000)
564 .ending(jiff_to_ib_datetime(&request_end))
565 .trading_hours(trading_hours)
566 .trade(),
567 )
568 .await
569 .context(format!(
570 "Historical trades request timed out after {timeout} seconds"
571 ))??;
572
573 let mut subscription = subscription.filter_data();
574 let mut batch_ticks = Vec::new();
575
576 while let Some(tick_result) = subscription.next().await {
577 let tick = match tick_result {
578 Ok(tick) => tick,
579 Err(e) => {
580 tracing::warn!("Historical trade ticks stream error: {e:?}");
581 continue;
582 }
583 };
584
585 let ts_event = ib_timestamp_to_unix_nanos(&tick.timestamp);
586
587 if ts_event < start_date_time_ns || ts_event > end_date_time_ns {
588 continue;
589 }
590
591 let ts_init = ts_event;
592
593 let converted_price =
594 apply_price_magnifier(tick.price, price_magnifier);
595
596 let Some(raw_size) = tick.size else {
597 tracing::warn!(
598 "Skipping historical trade tick with no size for {}",
599 instrument_id
600 );
601 continue;
602 };
603
604 if raw_size == 0.0 {
605 tracing::warn!(
606 "Skipping historical trade tick with zero size for {instrument_id}"
607 );
608 continue;
609 }
610
611 let trade_tick = parse_trade_tick(
612 instrument_id,
613 converted_price,
614 raw_size,
615 price_precision,
616 size_precision,
617 ts_event,
618 ts_init,
619 None,
620 )
621 .with_context(|| {
622 format!("Invalid historical trade tick for {instrument_id}")
623 })?;
624
625 batch_ticks.push(Data::Trade(trade_tick));
626 }
627
628 if !extend_historical_tick_batch(
629 &mut contract_ticks,
630 batch_ticks,
631 current_start_date,
632 &mut current_end_date,
633 Some(start_date_time_ns),
634 Some(end_date_time_ns),
635 limit,
636 data_ts_event,
637 ) {
638 break;
639 }
640 }
641 }
642 IbHistoricalTickType::BidAsk => {
643 while let Some(request_end) = current_end_date {
644 let subscription = tokio::time::timeout(
646 std::time::Duration::from_secs(timeout),
647 self.ib_client
648 .historical_ticks(&contract, 1000)
649 .ending(jiff_to_ib_datetime(&request_end))
650 .trading_hours(trading_hours)
651 .bid_ask(IgnoreSize::No),
652 )
653 .await
654 .context(format!(
655 "Historical bid/ask ticks request timed out after {timeout} seconds"
656 ))??;
657
658 let mut subscription = subscription.filter_data();
659 let mut batch_ticks = Vec::new();
660
661 while let Some(tick_result) = subscription.next().await {
662 let tick = match tick_result {
663 Ok(tick) => tick,
664 Err(e) => {
665 tracing::warn!("Historical bid/ask ticks stream error: {e:?}");
666 continue;
667 }
668 };
669
670 let ts_event = ib_timestamp_to_unix_nanos(&tick.timestamp);
671
672 if ts_event < start_date_time_ns || ts_event > end_date_time_ns {
673 continue;
674 }
675
676 let ts_init = ts_event;
677
678 let raw_bid_price =
679 apply_price_magnifier(tick.price_bid, price_magnifier);
680
681 let bid_price = Price::new_checked(raw_bid_price, price_precision)
682 .with_context(|| {
683 format!(
684 "Invalid historical bid price {raw_bid_price} for {instrument_id}"
685 )
686 })?;
687
688 let (Some(raw_bid_size), Some(raw_ask_size)) =
689 (tick.size_bid, tick.size_ask)
690 else {
691 tracing::warn!(
692 "Skipping historical quote tick with an absent size for {}",
693 instrument_id
694 );
695 continue;
696 };
697
698 let bid_size = Quantity::new_checked(raw_bid_size, size_precision)
699 .with_context(|| {
700 format!(
701 "Invalid historical bid size {raw_bid_size} for {instrument_id}"
702 )
703 })?;
704
705 let raw_ask_price =
706 apply_price_magnifier(tick.price_ask, price_magnifier);
707
708 let ask_price = Price::new_checked(raw_ask_price, price_precision)
709 .with_context(|| {
710 format!(
711 "Invalid historical ask price {raw_ask_price} for {instrument_id}"
712 )
713 })?;
714
715 let ask_size = Quantity::new_checked(raw_ask_size, size_precision)
716 .with_context(|| {
717 format!(
718 "Invalid historical ask size {raw_ask_size} for {instrument_id}"
719 )
720 })?;
721
722 let quote_tick = QuoteTick::new(
723 instrument_id,
724 bid_price,
725 ask_price,
726 bid_size,
727 ask_size,
728 ts_event,
729 ts_init,
730 );
731
732 batch_ticks.push(Data::Quote(quote_tick));
733 }
734
735 if !extend_historical_tick_batch(
736 &mut contract_ticks,
737 batch_ticks,
738 current_start_date,
739 &mut current_end_date,
740 Some(start_date_time_ns),
741 Some(end_date_time_ns),
742 limit,
743 data_ts_event,
744 ) {
745 break;
746 }
747 }
748 }
749 }
750
751 retain_historical_ticks_in_range(
752 &mut contract_ticks,
753 Some(start_date_time_ns),
754 Some(end_date_time_ns),
755 data_ts_event,
756 );
757 contract_ticks.sort_by_key(data_ts_event);
758 if let Some(limit) = limit
759 && contract_ticks.len() > limit
760 {
761 contract_ticks = contract_ticks.split_off(contract_ticks.len() - limit);
762 }
763
764 all_ticks.extend(contract_ticks);
765 }
766
767 all_ticks.sort_by_key(data_ts_event);
769
770 Ok(all_ticks)
771 }
772
773 pub async fn request_instruments(
785 &self,
786 instrument_ids: Option<Vec<InstrumentId>>,
787 contracts: Option<Vec<Contract>>,
788 ) -> anyhow::Result<Vec<InstrumentAny>> {
789 let instrument_ids = instrument_ids.unwrap_or_default();
790 let contracts = contracts.unwrap_or_default();
791
792 if instrument_ids.is_empty() && contracts.is_empty() {
793 anyhow::bail!("Either instrument_ids or contracts must be provided");
794 }
795
796 let loaded_ids = self
797 .instrument_provider
798 .load_ids_with_return_async(&self.ib_client, instrument_ids, None)
799 .await?;
800 let mut loaded_instruments = self.instrument_provider.find_all(&loaded_ids);
801
802 for contract in contracts {
804 match self
805 .instrument_provider
806 .get_instrument(&self.ib_client, &contract)
807 .await
808 {
809 Ok(Some(instrument)) => {
810 if !loaded_instruments.iter().any(|i| i.id() == instrument.id()) {
811 loaded_instruments.push(instrument);
812 }
813
814 continue;
815 }
816 Ok(None) => {}
817 Err(e) => {
818 tracing::warn!(
819 "Failed to fetch contract details from original contract {:?}: {}",
820 contract,
821 e
822 );
823 }
824 }
825
826 let instrument_id = if let Some(cached_id) = self
828 .instrument_provider
829 .get_instrument_id_by_contract_id(contract.contract_id)
830 {
831 Some(cached_id)
832 } else {
833 let venue = self.instrument_provider.determine_venue(&contract, None);
836 self.instrument_provider
837 .instrument_id_from_contract(&contract, venue)
838 .ok()
839 };
840
841 if let Some(instrument_id) = instrument_id {
842 if loaded_instruments.iter().any(|i| i.id() == instrument_id) {
844 continue;
845 }
846
847 if self.instrument_provider.find(&instrument_id).is_none() {
849 tracing::debug!("Fetching Instrument for: {}", instrument_id);
850
851 if let Err(e) = self
852 .instrument_provider
853 .fetch_contract_details(&self.ib_client, instrument_id, false, None)
854 .await
855 {
856 tracing::warn!(
857 "Failed to fetch contract details for {}: {}",
858 instrument_id,
859 e
860 );
861 continue;
862 }
863 }
864
865 if let Some(instrument) = self.instrument_provider.find(&instrument_id) {
866 loaded_instruments.push(instrument);
867 }
868 } else {
869 if let Ok(Some(instrument)) = self
871 .instrument_provider
872 .get_instrument(&self.ib_client, &contract)
873 .await
874 && !loaded_instruments.iter().any(|i| i.id() == instrument.id())
875 {
876 loaded_instruments.push(instrument);
877 }
878 }
879 }
880
881 tracing::debug!("Loaded {} instruments", loaded_instruments.len());
882
883 Ok(loaded_instruments)
884 }
885
886 async fn resolve_instrument_id(&self, contract: &Contract) -> anyhow::Result<InstrumentId> {
887 if let Some(instrument_id) = self
888 .instrument_provider
889 .get_instrument_id_by_contract_id(contract.contract_id)
890 {
891 return Ok(instrument_id);
892 }
893
894 let venue = self.instrument_provider.determine_venue(contract, None);
895 let parsed = self
896 .instrument_provider
897 .instrument_id_from_contract(contract, venue)
898 .ok();
899
900 if let Some(instrument_id) = parsed {
901 return Ok(instrument_id);
902 }
903
904 if let Ok(Some(instrument)) = self
905 .instrument_provider
906 .get_instrument(&self.ib_client, contract)
907 .await
908 {
909 return Ok(instrument.id());
910 }
911
912 anyhow::bail!(
913 "Failed to resolve instrument ID for contract {}:{}:{}",
914 contract.symbol,
915 contract.security_type,
916 contract.exchange
917 );
918 }
919}
920
921fn data_ts_event(data: &Data) -> UnixNanos {
922 match data {
923 Data::Trade(tick) => tick.ts_event,
924 Data::Quote(tick) => tick.ts_event,
925 _ => UnixNanos::default(),
926 }
927}