1use std::str::FromStr;
19
20use anyhow::Context;
21use ibapi::contracts::SecurityType;
22use nautilus_core::{UnixNanos, time::get_atomic_clock_realtime};
23use nautilus_model::{
24 enums::AssetClass,
25 identifiers::{InstrumentId, Symbol},
26 instruments::{
27 Cfd, Commodity, CryptoPerpetual, CurrencyPair, Equity, FuturesContract, FuturesSpread,
28 IndexInstrument, InstrumentAny, OptionContract, OptionSpread,
29 },
30 types::{Currency, Price, Quantity},
31};
32use rust_decimal::Decimal;
33use ustr::Ustr;
34
35use crate::common::{
36 contract_to_params,
37 enums::{IbOptionRight, IbSecurityType},
38};
39
40#[must_use]
42pub fn tick_size_to_precision(tick_size: f64) -> u8 {
43 if tick_size <= 0.0 {
44 return 8; }
46
47 let s = format!("{:.10}", tick_size);
49 let s = s.trim_end_matches('0');
50 let parts: Vec<&str> = s.split('.').collect();
51
52 if parts.len() == 2 {
53 parts[1].len().min(8) as u8
54 } else {
55 0
56 }
57}
58
59pub fn expiry_timestring_to_unix_nanos(
67 expiry: &str,
68 details: Option<&ibapi::contracts::ContractDetails>,
69) -> anyhow::Result<UnixNanos> {
70 if expiry.is_empty() {
71 anyhow::bail!("Empty expiry string");
72 }
73
74 let dt = if expiry.len() == 8 {
77 let year = &expiry[0..4];
79 let month = &expiry[4..6];
80 let day = &expiry[6..8];
81 let date = time::Date::from_calendar_date(
82 year.parse()?,
83 time::Month::try_from(month.parse::<u8>()?)?,
84 day.parse()?,
85 )?;
86
87 let mut expiry_time = time::Time::MIDNIGHT;
90
91 if let Some(details) = details {
92 if !details.trading_hours.is_empty()
93 && !details.trading_hours.contains(&"CLOSED".to_string())
94 {
95 let expiry_str: &str = expiry;
97 for session in &details.trading_hours {
98 if session.as_str().starts_with(expiry_str) && session.as_str().contains('-') {
99 let parts: Vec<&str> = session.as_str().split('-').collect();
100 if let Some(end_part) = parts.get(1) {
101 let inner_parts: Vec<&str> = end_part.split(':').collect();
102 if let Some(time_part) = inner_parts.get(1) {
103 if time_part.len() >= 4 {
104 let hour = time_part
105 .get(0..2)
106 .and_then(|s: &str| s.parse::<u8>().ok())
107 .unwrap_or(0);
108 let minute = time_part
109 .get(2..4)
110 .and_then(|s: &str| s.parse::<u8>().ok())
111 .unwrap_or(0);
112 expiry_time = time::Time::from_hms(hour, minute, 0)
113 .unwrap_or(time::Time::MIDNIGHT);
114 }
115 }
116 }
117 break;
118 }
119 }
120 }
121 }
122 time::PrimitiveDateTime::new(date, expiry_time)
123 } else {
124 let parts: Vec<&str> = expiry.split(' ').collect();
126 if parts.len() >= 3 {
127 let date_part = parts[0];
128 let time_part = parts[1];
129 let year = &date_part[0..4];
130 let month = &date_part[4..6];
131 let day = &date_part[6..8];
132
133 let time_parts: Vec<&str> = time_part.split(':').collect();
134 let hour = time_parts.first().unwrap_or(&"0").parse::<u8>()?;
135 let minute = time_parts.get(1).unwrap_or(&"0").parse::<u8>()?;
136 let second = time_parts.get(2).unwrap_or(&"0").parse::<u8>()?;
137
138 let date = time::Date::from_calendar_date(
139 year.parse()?,
140 time::Month::try_from(month.parse::<u8>()?)?,
141 day.parse()?,
142 )?;
143 let time_obj = time::Time::from_hms(hour, minute, second)?;
144 time::PrimitiveDateTime::new(date, time_obj)
145 } else {
146 anyhow::bail!("Invalid expiry format: {}", expiry);
147 }
148 };
149
150 let offset_dt = dt.assume_utc();
153 let nanos = offset_dt.unix_timestamp_nanos();
154 Ok(UnixNanos::new(nanos as u64))
155}
156
157pub fn parse_ib_contract_to_instrument(
163 details: &ibapi::contracts::ContractDetails,
164 instrument_id: InstrumentId,
165) -> anyhow::Result<InstrumentAny> {
166 let sec_type = &details.contract.security_type;
167
168 match sec_type {
169 SecurityType::Stock => Ok(parse_equity_contract(details, instrument_id)),
170 SecurityType::ForexPair => Ok(parse_forex_contract(details, instrument_id)),
171 SecurityType::Crypto => Ok(parse_crypto_contract(details, instrument_id)),
172 SecurityType::Future | SecurityType::ContinuousFuture => {
173 Ok(parse_futures_contract(details, instrument_id))
174 }
175 SecurityType::Option => parse_option_contract(details, instrument_id),
176 SecurityType::FuturesOption => parse_option_contract(details, instrument_id), SecurityType::Index => Ok(parse_index_contract(details, instrument_id)),
178 SecurityType::CFD => Ok(parse_cfd_contract(details, instrument_id)),
179 SecurityType::Commodity => Ok(parse_commodity_contract(details, instrument_id)),
180 SecurityType::Bond => Ok(parse_bond_contract(details, instrument_id)),
181 _ => anyhow::bail!("Unsupported security type: {:?}", sec_type),
182 }
183}
184
185fn ib_contract_info(details: &ibapi::contracts::ContractDetails) -> nautilus_core::Params {
186 let mut info = nautilus_core::Params::new();
187 let mut contract = serde_json::Map::new();
188
189 let contract_params = contract_to_params(&details.contract);
190 for (key, value) in &contract_params {
191 contract.insert(key.clone(), value.clone());
192 }
193
194 info.insert("contract".to_string(), serde_json::Value::Object(contract));
195 info.insert(
196 "priceMagnifier".to_string(),
197 serde_json::Value::from(details.price_magnifier),
198 );
199 info
200}
201
202fn ib_contract_info_for_contract(contract: &ibapi::contracts::Contract) -> nautilus_core::Params {
203 let mut info = nautilus_core::Params::new();
204 let mut contract_map = serde_json::Map::new();
205 let contract_params = contract_to_params(contract);
206
207 for (key, value) in &contract_params {
208 contract_map.insert(key.clone(), value.clone());
209 }
210
211 info.insert(
212 "contract".to_string(),
213 serde_json::Value::Object(contract_map),
214 );
215 info
216}
217
218fn sec_type_to_asset_class(sec_type: &str) -> AssetClass {
219 match IbSecurityType::from_str(sec_type).ok() {
220 Some(IbSecurityType::Stock) => AssetClass::Equity,
221 Some(IbSecurityType::Index) => AssetClass::Index,
222 Some(IbSecurityType::ForexPair) => AssetClass::FX,
223 Some(IbSecurityType::Bond) => AssetClass::Debt,
224 Some(IbSecurityType::Commodity) => AssetClass::Commodity,
225 Some(IbSecurityType::Future) => AssetClass::Index,
226 _ => AssetClass::Equity,
227 }
228}
229
230fn parse_equity_contract(
232 details: &ibapi::contracts::ContractDetails,
233 instrument_id: InstrumentId,
234) -> InstrumentAny {
235 let price_precision = tick_size_to_precision(details.min_tick);
236 let timestamp = get_atomic_clock_realtime().get_time_ns();
237
238 let instrument = Equity::new(
239 instrument_id,
240 Symbol::from(details.contract.local_symbol.as_str()),
241 None, Currency::from(details.contract.currency.to_string()),
243 price_precision,
244 Price::new(details.min_tick, price_precision),
245 Some(Quantity::new(100.0, 0)), None, None, None, None, None, None, None, None, None, Some(ib_contract_info(details)), timestamp,
257 timestamp,
258 );
259
260 InstrumentAny::from(instrument)
261}
262
263fn parse_forex_contract(
265 details: &ibapi::contracts::ContractDetails,
266 instrument_id: InstrumentId,
267) -> InstrumentAny {
268 let price_precision = tick_size_to_precision(details.min_tick);
269 let size_precision = tick_size_to_precision(details.min_size);
270 let timestamp = get_atomic_clock_realtime().get_time_ns();
271
272 let instrument = CurrencyPair::new(
273 instrument_id,
274 Symbol::from(details.contract.local_symbol.as_str()),
275 Currency::from(details.contract.symbol.to_string()),
276 Currency::from(details.contract.currency.to_string()),
277 price_precision,
278 size_precision,
279 Price::new(details.min_tick, price_precision),
280 Quantity::new(details.size_increment, size_precision),
281 None, None, None, None, None, None, None, None, None, None, None, None, None, Some(ib_contract_info(details)), timestamp,
296 timestamp,
297 );
298
299 InstrumentAny::from(instrument)
300}
301
302fn parse_crypto_contract(
304 details: &ibapi::contracts::ContractDetails,
305 instrument_id: InstrumentId,
306) -> InstrumentAny {
307 let price_precision = tick_size_to_precision(details.min_tick);
308 let size_precision = tick_size_to_precision(details.min_size);
309 let timestamp = get_atomic_clock_realtime().get_time_ns();
310
311 let instrument = CryptoPerpetual::new(
312 instrument_id,
313 Symbol::from(details.contract.local_symbol.as_str()),
314 Currency::from(details.contract.symbol.to_string()),
315 Currency::from(details.contract.currency.to_string()),
316 Currency::from(details.contract.currency.to_string()),
317 true, price_precision,
319 size_precision,
320 Price::new(details.min_tick, price_precision),
321 Quantity::new(details.size_increment, size_precision),
322 None, None, None, Some(Quantity::new(details.min_size, size_precision)),
326 None, None, None, None, None, None, None, None, None, Some(ib_contract_info(details)), timestamp,
337 timestamp,
338 );
339
340 InstrumentAny::from(instrument)
341}
342
343fn parse_contract_multiplier(multiplier: &str, default: f64) -> Quantity {
344 if multiplier.is_empty() {
345 return Quantity::new(default, 0);
346 }
347
348 Quantity::from_str(multiplier).unwrap_or_else(|e| {
349 tracing::warn!(
350 "Failed to parse IB contract multiplier '{multiplier}', using default {default}: {e}"
351 );
352 Quantity::new(default, 0)
353 })
354}
355
356fn parse_futures_contract(
358 details: &ibapi::contracts::ContractDetails,
359 instrument_id: InstrumentId,
360) -> InstrumentAny {
361 let price_precision = tick_size_to_precision(details.min_tick);
362 let timestamp = get_atomic_clock_realtime().get_time_ns();
363
364 let expiration_ns = if !details
366 .contract
367 .last_trade_date_or_contract_month
368 .is_empty()
369 {
370 expiry_timestring_to_unix_nanos(
371 &details.contract.last_trade_date_or_contract_month,
372 Some(details),
373 )
374 .unwrap_or_else(|_| UnixNanos::from(timestamp.as_u64() + 90 * 24 * 60 * 60 * 1_000_000_000))
375 } else {
377 UnixNanos::from(timestamp.as_u64() + 90 * 24 * 60 * 60 * 1_000_000_000) };
379
380 let ninety_days_ns: u64 = 90 * 24 * 60 * 60 * 1_000_000_000;
381 let activation_ns = expiration_ns
382 .checked_sub(ninety_days_ns)
383 .unwrap_or(UnixNanos::from(0)); let multiplier = parse_contract_multiplier(&details.contract.multiplier, 1.0);
386
387 let raw_symbol = if matches!(
388 details.contract.security_type,
389 SecurityType::ContinuousFuture
390 ) && !details.contract.symbol.as_str().is_empty()
391 {
392 details.contract.symbol.as_str()
393 } else {
394 details.contract.local_symbol.as_str()
395 };
396
397 let instrument = FuturesContract::new(
398 instrument_id,
399 Symbol::from(raw_symbol),
400 sec_type_to_asset_class(details.under_security_type.as_str()),
401 None, Ustr::from(details.under_symbol.as_str()),
403 activation_ns,
404 expiration_ns,
405 Currency::from(details.contract.currency.to_string()),
406 price_precision,
407 Price::new(details.min_tick, price_precision),
408 multiplier,
409 Quantity::new(1.0, 0),
410 None, None, None, None, None, None, None, None, None, Some(ib_contract_info(details)), timestamp,
421 timestamp,
422 );
423
424 InstrumentAny::from(instrument)
425}
426
427fn parse_option_contract(
429 details: &ibapi::contracts::ContractDetails,
430 instrument_id: InstrumentId,
431) -> anyhow::Result<InstrumentAny> {
432 let price_precision = tick_size_to_precision(details.min_tick);
433 let timestamp = get_atomic_clock_realtime().get_time_ns();
434
435 let expiration_ns = if !details
437 .contract
438 .last_trade_date_or_contract_month
439 .is_empty()
440 {
441 expiry_timestring_to_unix_nanos(
442 &details.contract.last_trade_date_or_contract_month,
443 Some(details),
444 )
445 .unwrap_or_else(|_| UnixNanos::from(timestamp.as_u64() + 90 * 24 * 60 * 60 * 1_000_000_000))
446 } else {
448 UnixNanos::from(timestamp.as_u64() + 90 * 24 * 60 * 60 * 1_000_000_000) };
450
451 let ninety_days_ns: u64 = 90 * 24 * 60 * 60 * 1_000_000_000;
452 let activation_ns = expiration_ns
453 .checked_sub(ninety_days_ns)
454 .unwrap_or(UnixNanos::from(0)); let option_kind = details
458 .contract
459 .right
460 .map(|right| IbOptionRight::from_str(right.as_str()))
461 .transpose()?
462 .context("Option contract missing right")?
463 .option_kind();
464
465 let multiplier = parse_contract_multiplier(&details.contract.multiplier, 100.0);
466 let asset_class = sec_type_to_asset_class(details.under_security_type.as_str());
467 let underlying =
468 if details.under_security_type == "IND" && !details.under_symbol.starts_with('^') {
469 format!("^{}", details.under_symbol)
470 } else {
471 details.under_symbol.clone()
472 };
473
474 let instrument = OptionContract::new(
475 instrument_id,
476 Symbol::from(details.contract.local_symbol.as_str()),
477 asset_class,
478 None, Ustr::from(underlying.as_str()),
480 option_kind,
481 Price::new(details.contract.strike, price_precision),
482 Currency::from(details.contract.currency.to_string()),
483 activation_ns,
484 expiration_ns,
485 price_precision,
486 Price::new(details.min_tick, price_precision),
487 multiplier,
488 multiplier,
489 None, None, None, None, None, None, None, None, None, Some(ib_contract_info(details)), timestamp,
500 timestamp,
501 );
502
503 Ok(InstrumentAny::from(instrument))
504}
505
506#[allow(clippy::items_after_test_module)]
507#[cfg(test)]
508mod tests {
509 use ibapi::contracts::{
510 Contract, ContractDetails, Currency, Exchange, OptionRight, SecurityType, Symbol,
511 };
512 use nautilus_model::{
513 enums::AssetClass,
514 identifiers::{InstrumentId, Symbol as NautilusSymbol, Venue},
515 instruments::{Instrument, InstrumentAny},
516 types::{Price, Quantity},
517 };
518 use rstest::rstest;
519 use ustr::Ustr;
520
521 use super::{
522 parse_contract_multiplier, parse_ib_contract_to_instrument,
523 parse_option_spread_instrument_id,
524 };
525
526 #[rstest]
527 fn test_parse_option_contract_prefixes_index_underlying() {
528 let details = ContractDetails {
529 contract: Contract {
530 symbol: Symbol::from("SPXW"),
531 security_type: SecurityType::Option,
532 exchange: Exchange::from("SMART"),
533 currency: Currency::from("USD"),
534 local_symbol: "SPXW 260313P06630000".to_string(),
535 last_trade_date_or_contract_month: "20260313".to_string(),
536 right: Some(OptionRight::Put),
537 strike: 6630.0,
538 multiplier: "100".to_string(),
539 ..Default::default()
540 },
541 min_tick: 0.05,
542 under_symbol: "SPX".to_string(),
543 under_security_type: "IND".to_string(),
544 ..Default::default()
545 };
546 let instrument_id = InstrumentId::new(
547 NautilusSymbol::from("SPXW 260313P06630000"),
548 Venue::from("SMART"),
549 );
550
551 let instrument = parse_ib_contract_to_instrument(&details, instrument_id).unwrap();
552
553 let InstrumentAny::OptionContract(option) = instrument else {
554 panic!("expected option contract");
555 };
556
557 assert_eq!(option.asset_class(), AssetClass::Index);
558 assert_eq!(option.underlying(), Some(Ustr::from("^SPX")));
559 }
560
561 #[rstest]
562 fn test_parse_contract_preserves_price_magnifier_in_info() {
563 let details = ContractDetails {
564 contract: Contract {
565 symbol: Symbol::from("AAPL"),
566 security_type: SecurityType::Stock,
567 exchange: Exchange::from("SMART"),
568 primary_exchange: Exchange::from("NASDAQ"),
569 currency: Currency::from("USD"),
570 local_symbol: String::from("AAPL"),
571 ..Default::default()
572 },
573 min_tick: 0.01,
574 price_magnifier: 100,
575 ..Default::default()
576 };
577 let instrument_id = InstrumentId::new(NautilusSymbol::from("AAPL"), Venue::from("XNAS"));
578
579 let instrument = parse_ib_contract_to_instrument(&details, instrument_id).unwrap();
580 let InstrumentAny::Equity(equity) = instrument else {
581 panic!("expected equity");
582 };
583
584 assert_eq!(
585 equity.info.unwrap().get("priceMagnifier"),
586 Some(&serde_json::Value::from(100))
587 );
588 }
589
590 #[rstest]
591 #[case("100", 100.0)]
592 #[case("", 1.0)]
593 #[case("not-a-number", 1.0)]
594 fn test_parse_contract_multiplier_uses_quantity_parser(
595 #[case] multiplier: &str,
596 #[case] expected: f64,
597 ) {
598 assert_eq!(
599 parse_contract_multiplier(multiplier, 1.0),
600 Quantity::new(expected, 0)
601 );
602 }
603
604 #[rstest]
605 fn test_parse_continuous_future_contract_uses_symbol_as_raw_symbol() {
606 let details = ContractDetails {
607 contract: Contract {
608 symbol: Symbol::from("ES"),
609 security_type: SecurityType::ContinuousFuture,
610 exchange: Exchange::from("CME"),
611 currency: Currency::from("USD"),
612 local_symbol: String::new(),
613 multiplier: "50".to_string(),
614 ..Default::default()
615 },
616 min_tick: 0.25,
617 under_symbol: "ES".to_string(),
618 under_security_type: "IND".to_string(),
619 ..Default::default()
620 };
621 let instrument_id = InstrumentId::new(NautilusSymbol::from("ES"), Venue::from("CME"));
622
623 let instrument = parse_ib_contract_to_instrument(&details, instrument_id).unwrap();
624
625 let InstrumentAny::FuturesContract(future) = instrument else {
626 panic!("expected futures contract");
627 };
628
629 assert_eq!(future.raw_symbol().as_str(), "ES");
630 }
631
632 #[rstest]
633 fn test_parse_option_spread_uses_minimum_leg_tick() {
634 let leg1 = ContractDetails {
635 contract: Contract {
636 symbol: Symbol::from("SPY"),
637 security_type: SecurityType::Option,
638 exchange: Exchange::from("SMART"),
639 currency: Currency::from("USD"),
640 local_symbol: "SPY 260120C00400000".to_string(),
641 multiplier: "100".to_string(),
642 ..Default::default()
643 },
644 min_tick: 0.05,
645 under_symbol: "SPY".to_string(),
646 ..Default::default()
647 };
648 let leg2 = ContractDetails {
649 contract: Contract {
650 symbol: Symbol::from("SPY"),
651 security_type: SecurityType::Option,
652 exchange: Exchange::from("SMART"),
653 currency: Currency::from("USD"),
654 local_symbol: "SPY 260120C00410000".to_string(),
655 multiplier: "100".to_string(),
656 ..Default::default()
657 },
658 min_tick: 0.01,
659 under_symbol: "SPY".to_string(),
660 ..Default::default()
661 };
662 let instrument_id =
663 InstrumentId::from("(1)SPY 260120C00400000_((-1))SPY 260120C00410000.SMART");
664
665 let spread = parse_option_spread_instrument_id(
666 instrument_id,
667 &[(&leg1, 1), (&leg2, -1)],
668 None,
669 None,
670 )
671 .unwrap();
672
673 assert_eq!(spread.price_precision(), 2);
674 assert_eq!(spread.price_increment(), Price::from("0.01"));
675 }
676}
677
678fn parse_index_contract(
683 details: &ibapi::contracts::ContractDetails,
684 instrument_id: InstrumentId,
685) -> InstrumentAny {
686 let price_precision = tick_size_to_precision(details.min_tick);
687 let size_precision = tick_size_to_precision(details.min_size);
688 let timestamp = get_atomic_clock_realtime().get_time_ns();
689
690 let instrument = IndexInstrument::new(
691 instrument_id,
692 Symbol::from(details.contract.local_symbol.as_str()),
693 Currency::from(details.contract.currency.to_string()),
694 price_precision,
695 size_precision,
696 Price::new(details.min_tick, price_precision),
697 Quantity::new(details.size_increment, size_precision),
698 None,
699 Some(ib_contract_info(details)), timestamp,
701 timestamp,
702 );
703
704 InstrumentAny::from(instrument)
705}
706
707pub fn parse_spread_instrument_id(
716 instrument_id: InstrumentId,
717 leg_contract_details: &[(&ibapi::contracts::ContractDetails, i32)],
718 timestamp_ns: Option<UnixNanos>,
719) -> anyhow::Result<OptionSpread> {
720 if leg_contract_details.is_empty() {
721 anyhow::bail!("leg_contract_details must be provided");
722 }
723
724 let (first_details, _) = leg_contract_details[0];
726 let first_contract = &first_details.contract;
727
728 let currency = Currency::from(first_contract.currency.to_string());
730 let underlying = if !first_details.under_symbol.is_empty() {
731 Ustr::from(first_details.under_symbol.as_str())
732 } else {
733 Ustr::from(first_contract.symbol.as_str())
734 };
735
736 let multiplier_str = first_contract.multiplier.to_string();
738 let multiplier =
739 Quantity::from_str(&multiplier_str).unwrap_or_else(|_| Quantity::new(100.0, 0)); let asset_class = match first_contract.security_type {
743 ibapi::contracts::SecurityType::FuturesOption => AssetClass::Index, _ => AssetClass::Equity, };
746
747 let min_tick = leg_contract_details
749 .iter()
750 .map(|(details, _)| details.min_tick)
751 .fold(first_details.min_tick, f64::min);
752 let price_precision = tick_size_to_precision(min_tick);
753 let price_increment = Price::new(min_tick, price_precision);
754
755 let timestamp = timestamp_ns.unwrap_or_else(|| get_atomic_clock_realtime().get_time_ns());
757
758 let lot_size = multiplier;
760
761 let spread = OptionSpread::new_checked(
763 instrument_id,
764 Symbol::from(instrument_id.symbol.as_str()), asset_class,
766 None, underlying,
768 Ustr::from("SPREAD"), UnixNanos::new(0), UnixNanos::new(0), currency,
772 price_precision,
773 price_increment,
774 multiplier,
775 lot_size,
776 None, None, None, None, Some(Decimal::ZERO), Some(Decimal::ZERO), Some(Decimal::ZERO), Some(Decimal::ZERO), None, None, timestamp,
787 timestamp,
788 )?;
789
790 Ok(spread)
791}
792
793pub fn parse_option_spread_instrument_id(
794 instrument_id: InstrumentId,
795 leg_contract_details: &[(&ibapi::contracts::ContractDetails, i32)],
796 bag_contract: Option<&ibapi::contracts::Contract>,
797 timestamp_ns: Option<UnixNanos>,
798) -> anyhow::Result<OptionSpread> {
799 let mut spread = parse_spread_instrument_id(instrument_id, leg_contract_details, timestamp_ns)?;
800 spread.info = bag_contract.map(ib_contract_info_for_contract);
801 Ok(spread)
802}
803
804pub fn parse_futures_spread_instrument_id(
805 instrument_id: InstrumentId,
806 leg_contract_details: &[(&ibapi::contracts::ContractDetails, i32)],
807 bag_contract: Option<&ibapi::contracts::Contract>,
808 timestamp_ns: Option<UnixNanos>,
809) -> anyhow::Result<FuturesSpread> {
810 if leg_contract_details.is_empty() {
811 anyhow::bail!("leg_contract_details must be provided");
812 }
813
814 let (first_details, _) = leg_contract_details[0];
815 let first_contract = &first_details.contract;
816 let currency = Currency::from(first_contract.currency.to_string());
817 let underlying = if !first_details.under_symbol.is_empty() {
818 Ustr::from(first_details.under_symbol.as_str())
819 } else {
820 Ustr::from(first_contract.symbol.as_str())
821 };
822 let multiplier = Quantity::from_str(&first_contract.multiplier.to_string())
823 .unwrap_or_else(|_| Quantity::new(1.0, 0));
824 let min_tick = leg_contract_details
825 .iter()
826 .map(|(details, _)| details.min_tick)
827 .fold(first_details.min_tick, f64::min);
828 let price_precision = tick_size_to_precision(min_tick);
829 let price_increment = Price::new(min_tick, price_precision);
830 let timestamp = timestamp_ns.unwrap_or_else(|| get_atomic_clock_realtime().get_time_ns());
831
832 Ok(FuturesSpread::new_checked(
833 instrument_id,
834 Symbol::from(instrument_id.symbol.as_str()),
835 AssetClass::Index,
836 None,
837 underlying,
838 Ustr::from("SPREAD"),
839 UnixNanos::new(0),
840 UnixNanos::new(0),
841 currency,
842 price_precision,
843 price_increment,
844 multiplier,
845 Quantity::new(1.0, 0),
846 None,
847 None,
848 None,
849 None,
850 Some(Decimal::ZERO),
851 Some(Decimal::ZERO),
852 Some(Decimal::ZERO),
853 Some(Decimal::ZERO),
854 None,
855 bag_contract.map(ib_contract_info_for_contract),
856 timestamp,
857 timestamp,
858 )?)
859}
860
861pub fn parse_spread_instrument_any(
862 instrument_id: InstrumentId,
863 leg_contract_details: &[(&ibapi::contracts::ContractDetails, i32)],
864 bag_contract: Option<&ibapi::contracts::Contract>,
865 timestamp_ns: Option<UnixNanos>,
866) -> anyhow::Result<InstrumentAny> {
867 let has_future = leg_contract_details.iter().any(|(details, _)| {
868 matches!(
869 details.contract.security_type,
870 SecurityType::Future | SecurityType::ContinuousFuture
871 )
872 });
873
874 if has_future {
875 Ok(InstrumentAny::from(parse_futures_spread_instrument_id(
876 instrument_id,
877 leg_contract_details,
878 bag_contract,
879 timestamp_ns,
880 )?))
881 } else {
882 Ok(InstrumentAny::from(parse_option_spread_instrument_id(
883 instrument_id,
884 leg_contract_details,
885 bag_contract,
886 timestamp_ns,
887 )?))
888 }
889}
890
891fn parse_cfd_contract(
893 details: &ibapi::contracts::ContractDetails,
894 instrument_id: InstrumentId,
895) -> InstrumentAny {
896 let price_precision = tick_size_to_precision(details.min_tick);
897 let size_precision = tick_size_to_precision(details.min_size);
898 let timestamp = get_atomic_clock_realtime().get_time_ns();
899
900 let base_currency = details
901 .contract
902 .local_symbol
903 .contains('.')
904 .then(|| Currency::from(details.contract.symbol.to_string()));
905
906 let instrument = Cfd::new(
907 instrument_id,
908 Symbol::from(details.contract.local_symbol.as_str()),
909 sec_type_to_asset_class(details.under_security_type.as_str()),
910 base_currency,
911 Currency::from(details.contract.currency.to_string()),
912 price_precision,
913 size_precision,
914 Price::new(details.min_tick, price_precision),
915 Quantity::new(details.size_increment, size_precision),
916 None,
917 None,
918 None,
919 None,
920 None,
921 None,
922 None,
923 None,
924 None,
925 None,
926 None,
927 None,
928 Some(ib_contract_info(details)),
929 timestamp,
930 timestamp,
931 );
932
933 InstrumentAny::from(instrument)
934}
935
936fn parse_commodity_contract(
938 details: &ibapi::contracts::ContractDetails,
939 instrument_id: InstrumentId,
940) -> InstrumentAny {
941 let price_precision = tick_size_to_precision(details.min_tick);
942 let size_precision = tick_size_to_precision(details.min_size);
943 let timestamp = get_atomic_clock_realtime().get_time_ns();
944
945 let instrument = Commodity::new(
946 instrument_id,
947 Symbol::from(details.contract.local_symbol.as_str()),
948 AssetClass::Commodity,
949 Currency::from(details.contract.currency.to_string()),
950 price_precision,
951 size_precision,
952 Price::new(details.min_tick, price_precision),
953 Quantity::new(details.size_increment, size_precision),
954 None,
955 None,
956 None,
957 None,
958 None,
959 None,
960 None,
961 None,
962 None,
963 None,
964 None,
965 None,
966 Some(ib_contract_info(details)),
967 timestamp,
968 timestamp,
969 );
970
971 InstrumentAny::from(instrument)
972}
973
974fn parse_bond_contract(
976 details: &ibapi::contracts::ContractDetails,
977 instrument_id: InstrumentId,
978) -> InstrumentAny {
979 let price_precision = tick_size_to_precision(details.min_tick);
982 let timestamp = get_atomic_clock_realtime().get_time_ns();
983
984 let instrument = Equity::new(
985 instrument_id,
986 Symbol::from(details.contract.local_symbol.as_str()),
987 None, Currency::from(details.contract.currency.to_string()),
989 price_precision,
990 Price::new(details.min_tick, price_precision),
991 Some(Quantity::new(1.0, 0)), None, None, None, None, None, None, None, None, None, Some(ib_contract_info(details)), timestamp,
1003 timestamp,
1004 );
1005
1006 InstrumentAny::from(instrument)
1007}