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::builder()
239 .instrument_id(instrument_id)
240 .raw_symbol(Symbol::from(details.contract.local_symbol.as_str()))
241 .currency(Currency::from(details.contract.currency.to_string()))
242 .price_precision(price_precision)
243 .price_increment(Price::new(details.min_tick, price_precision))
244 .lot_size(Quantity::new(100.0, 0))
246 .info(ib_contract_info(details))
247 .ts_event(timestamp)
248 .ts_init(timestamp)
249 .build()
250 .unwrap();
251
252 InstrumentAny::from(instrument)
253}
254
255fn parse_forex_contract(
257 details: &ibapi::contracts::ContractDetails,
258 instrument_id: InstrumentId,
259) -> InstrumentAny {
260 let price_precision = tick_size_to_precision(details.min_tick);
261 let size_precision = tick_size_to_precision(details.min_size);
262 let timestamp = get_atomic_clock_realtime().get_time_ns();
263
264 let instrument = CurrencyPair::builder()
265 .instrument_id(instrument_id)
266 .raw_symbol(Symbol::from(details.contract.local_symbol.as_str()))
267 .base_currency(Currency::from(details.contract.symbol.to_string()))
268 .quote_currency(Currency::from(details.contract.currency.to_string()))
269 .price_precision(price_precision)
270 .size_precision(size_precision)
271 .price_increment(Price::new(details.min_tick, price_precision))
272 .size_increment(Quantity::new(details.size_increment, size_precision))
273 .info(ib_contract_info(details))
274 .ts_event(timestamp)
275 .ts_init(timestamp)
276 .build()
277 .unwrap();
278
279 InstrumentAny::from(instrument)
280}
281
282fn parse_crypto_contract(
284 details: &ibapi::contracts::ContractDetails,
285 instrument_id: InstrumentId,
286) -> InstrumentAny {
287 let price_precision = tick_size_to_precision(details.min_tick);
288 let size_precision = tick_size_to_precision(details.min_size);
289 let timestamp = get_atomic_clock_realtime().get_time_ns();
290
291 let instrument = CryptoPerpetual::builder()
292 .instrument_id(instrument_id)
293 .raw_symbol(Symbol::from(details.contract.local_symbol.as_str()))
294 .base_currency(Currency::from(details.contract.symbol.to_string()))
295 .quote_currency(Currency::from(details.contract.currency.to_string()))
296 .settlement_currency(Currency::from(details.contract.currency.to_string()))
297 .is_inverse(true)
298 .price_precision(price_precision)
299 .size_precision(size_precision)
300 .price_increment(Price::new(details.min_tick, price_precision))
301 .size_increment(Quantity::new(details.size_increment, size_precision))
302 .min_quantity(Quantity::new(details.min_size, size_precision))
303 .info(ib_contract_info(details))
304 .ts_event(timestamp)
305 .ts_init(timestamp)
306 .build()
307 .unwrap();
308
309 InstrumentAny::from(instrument)
310}
311
312fn parse_contract_multiplier(multiplier: &str, default: f64) -> Quantity {
313 if multiplier.is_empty() {
314 return Quantity::new(default, 0);
315 }
316
317 Quantity::from_str(multiplier).unwrap_or_else(|e| {
318 tracing::warn!(
319 "Failed to parse IB contract multiplier '{multiplier}', using default {default}: {e}"
320 );
321 Quantity::new(default, 0)
322 })
323}
324
325fn parse_futures_contract(
327 details: &ibapi::contracts::ContractDetails,
328 instrument_id: InstrumentId,
329) -> InstrumentAny {
330 let price_precision = tick_size_to_precision(details.min_tick);
331 let timestamp = get_atomic_clock_realtime().get_time_ns();
332
333 let expiration_ns = if !details
335 .contract
336 .last_trade_date_or_contract_month
337 .is_empty()
338 {
339 expiry_timestring_to_unix_nanos(
340 &details.contract.last_trade_date_or_contract_month,
341 Some(details),
342 )
343 .unwrap_or_else(|_| UnixNanos::from(timestamp.as_u64() + 90 * 24 * 60 * 60 * 1_000_000_000))
344 } else {
346 UnixNanos::from(timestamp.as_u64() + 90 * 24 * 60 * 60 * 1_000_000_000) };
348
349 let ninety_days_ns: u64 = 90 * 24 * 60 * 60 * 1_000_000_000;
350 let activation_ns = expiration_ns
351 .checked_sub(ninety_days_ns)
352 .unwrap_or(UnixNanos::from(0)); let multiplier = parse_contract_multiplier(&details.contract.multiplier, 1.0);
355
356 let raw_symbol = if matches!(
357 details.contract.security_type,
358 SecurityType::ContinuousFuture
359 ) && !details.contract.symbol.as_str().is_empty()
360 {
361 details.contract.symbol.as_str()
362 } else {
363 details.contract.local_symbol.as_str()
364 };
365
366 let instrument = FuturesContract::builder()
367 .instrument_id(instrument_id)
368 .raw_symbol(Symbol::from(raw_symbol))
369 .asset_class(sec_type_to_asset_class(
370 details.under_security_type.as_str(),
371 ))
372 .underlying(Ustr::from(details.under_symbol.as_str()))
373 .activation_ns(activation_ns)
374 .expiration_ns(expiration_ns)
375 .currency(Currency::from(details.contract.currency.to_string()))
376 .price_precision(price_precision)
377 .price_increment(Price::new(details.min_tick, price_precision))
378 .multiplier(multiplier)
379 .lot_size(Quantity::new(1.0, 0))
380 .info(ib_contract_info(details))
381 .ts_event(timestamp)
382 .ts_init(timestamp)
383 .build()
384 .unwrap();
385
386 InstrumentAny::from(instrument)
387}
388
389fn parse_option_contract(
391 details: &ibapi::contracts::ContractDetails,
392 instrument_id: InstrumentId,
393) -> anyhow::Result<InstrumentAny> {
394 let price_precision = tick_size_to_precision(details.min_tick);
395 let timestamp = get_atomic_clock_realtime().get_time_ns();
396
397 let expiration_ns = if !details
399 .contract
400 .last_trade_date_or_contract_month
401 .is_empty()
402 {
403 expiry_timestring_to_unix_nanos(
404 &details.contract.last_trade_date_or_contract_month,
405 Some(details),
406 )
407 .unwrap_or_else(|_| UnixNanos::from(timestamp.as_u64() + 90 * 24 * 60 * 60 * 1_000_000_000))
408 } else {
410 UnixNanos::from(timestamp.as_u64() + 90 * 24 * 60 * 60 * 1_000_000_000) };
412
413 let ninety_days_ns: u64 = 90 * 24 * 60 * 60 * 1_000_000_000;
414 let activation_ns = expiration_ns
415 .checked_sub(ninety_days_ns)
416 .unwrap_or(UnixNanos::from(0)); let option_kind = details
420 .contract
421 .right
422 .map(|right| IbOptionRight::from_str(right.as_str()))
423 .transpose()?
424 .context("Option contract missing right")?
425 .option_kind();
426
427 let multiplier = parse_contract_multiplier(&details.contract.multiplier, 100.0);
428 let asset_class = sec_type_to_asset_class(details.under_security_type.as_str());
429 let underlying =
430 if details.under_security_type == "IND" && !details.under_symbol.starts_with('^') {
431 format!("^{}", details.under_symbol)
432 } else {
433 details.under_symbol.clone()
434 };
435
436 let instrument = OptionContract::builder()
437 .instrument_id(instrument_id)
438 .raw_symbol(Symbol::from(details.contract.local_symbol.as_str()))
439 .asset_class(asset_class)
440 .underlying(Ustr::from(underlying.as_str()))
441 .option_kind(option_kind)
442 .strike_price(Price::new(details.contract.strike, price_precision))
443 .currency(Currency::from(details.contract.currency.to_string()))
444 .activation_ns(activation_ns)
445 .expiration_ns(expiration_ns)
446 .price_precision(price_precision)
447 .price_increment(Price::new(details.min_tick, price_precision))
448 .multiplier(multiplier)
449 .lot_size(multiplier)
450 .info(ib_contract_info(details))
451 .ts_event(timestamp)
452 .ts_init(timestamp)
453 .build()
454 .unwrap();
455
456 Ok(InstrumentAny::from(instrument))
457}
458
459#[allow(clippy::items_after_test_module)]
460#[cfg(test)]
461mod tests {
462 use ibapi::contracts::{
463 Contract, ContractDetails, Currency, Exchange, OptionRight, SecurityType, Symbol,
464 };
465 use nautilus_model::{
466 enums::AssetClass,
467 identifiers::{InstrumentId, Symbol as NautilusSymbol, Venue},
468 instruments::{Instrument, InstrumentAny},
469 types::{Price, Quantity},
470 };
471 use rstest::rstest;
472 use ustr::Ustr;
473
474 use super::{
475 parse_contract_multiplier, parse_ib_contract_to_instrument,
476 parse_option_spread_instrument_id,
477 };
478
479 #[rstest]
480 fn test_parse_option_contract_prefixes_index_underlying() {
481 let details = ContractDetails {
482 contract: Contract {
483 symbol: Symbol::from("SPXW"),
484 security_type: SecurityType::Option,
485 exchange: Exchange::from("SMART"),
486 currency: Currency::from("USD"),
487 local_symbol: "SPXW 260313P06630000".to_string(),
488 last_trade_date_or_contract_month: "20260313".to_string(),
489 right: Some(OptionRight::Put),
490 strike: 6630.0,
491 multiplier: "100".to_string(),
492 ..Default::default()
493 },
494 min_tick: 0.05,
495 under_symbol: "SPX".to_string(),
496 under_security_type: "IND".to_string(),
497 ..Default::default()
498 };
499 let instrument_id = InstrumentId::new(
500 NautilusSymbol::from("SPXW 260313P06630000"),
501 Venue::from("SMART"),
502 );
503
504 let instrument = parse_ib_contract_to_instrument(&details, instrument_id).unwrap();
505
506 let InstrumentAny::OptionContract(option) = instrument else {
507 panic!("expected option contract");
508 };
509
510 assert_eq!(option.asset_class(), AssetClass::Index);
511 assert_eq!(option.underlying(), Some(Ustr::from("^SPX")));
512 }
513
514 #[rstest]
515 fn test_parse_contract_preserves_price_magnifier_in_info() {
516 let details = ContractDetails {
517 contract: Contract {
518 symbol: Symbol::from("AAPL"),
519 security_type: SecurityType::Stock,
520 exchange: Exchange::from("SMART"),
521 primary_exchange: Exchange::from("NASDAQ"),
522 currency: Currency::from("USD"),
523 local_symbol: String::from("AAPL"),
524 ..Default::default()
525 },
526 min_tick: 0.01,
527 price_magnifier: 100,
528 ..Default::default()
529 };
530 let instrument_id = InstrumentId::new(NautilusSymbol::from("AAPL"), Venue::from("XNAS"));
531
532 let instrument = parse_ib_contract_to_instrument(&details, instrument_id).unwrap();
533 let InstrumentAny::Equity(equity) = instrument else {
534 panic!("expected equity");
535 };
536
537 assert_eq!(
538 equity.info.unwrap().get("priceMagnifier"),
539 Some(&serde_json::Value::from(100))
540 );
541 }
542
543 #[rstest]
544 #[case("100", 100.0)]
545 #[case("", 1.0)]
546 #[case("not-a-number", 1.0)]
547 fn test_parse_contract_multiplier_uses_quantity_parser(
548 #[case] multiplier: &str,
549 #[case] expected: f64,
550 ) {
551 assert_eq!(
552 parse_contract_multiplier(multiplier, 1.0),
553 Quantity::new(expected, 0)
554 );
555 }
556
557 #[rstest]
558 fn test_parse_continuous_future_contract_uses_symbol_as_raw_symbol() {
559 let details = ContractDetails {
560 contract: Contract {
561 symbol: Symbol::from("ES"),
562 security_type: SecurityType::ContinuousFuture,
563 exchange: Exchange::from("CME"),
564 currency: Currency::from("USD"),
565 local_symbol: String::new(),
566 multiplier: "50".to_string(),
567 ..Default::default()
568 },
569 min_tick: 0.25,
570 under_symbol: "ES".to_string(),
571 under_security_type: "IND".to_string(),
572 ..Default::default()
573 };
574 let instrument_id = InstrumentId::new(NautilusSymbol::from("ES"), Venue::from("CME"));
575
576 let instrument = parse_ib_contract_to_instrument(&details, instrument_id).unwrap();
577
578 let InstrumentAny::FuturesContract(future) = instrument else {
579 panic!("expected futures contract");
580 };
581
582 assert_eq!(future.raw_symbol().as_str(), "ES");
583 }
584
585 #[rstest]
586 fn test_parse_option_spread_uses_minimum_leg_tick() {
587 let leg1 = ContractDetails {
588 contract: Contract {
589 symbol: Symbol::from("SPY"),
590 security_type: SecurityType::Option,
591 exchange: Exchange::from("SMART"),
592 currency: Currency::from("USD"),
593 local_symbol: "SPY 260120C00400000".to_string(),
594 multiplier: "100".to_string(),
595 ..Default::default()
596 },
597 min_tick: 0.05,
598 under_symbol: "SPY".to_string(),
599 ..Default::default()
600 };
601 let leg2 = ContractDetails {
602 contract: Contract {
603 symbol: Symbol::from("SPY"),
604 security_type: SecurityType::Option,
605 exchange: Exchange::from("SMART"),
606 currency: Currency::from("USD"),
607 local_symbol: "SPY 260120C00410000".to_string(),
608 multiplier: "100".to_string(),
609 ..Default::default()
610 },
611 min_tick: 0.01,
612 under_symbol: "SPY".to_string(),
613 ..Default::default()
614 };
615 let instrument_id =
616 InstrumentId::from("(1)SPY 260120C00400000_((-1))SPY 260120C00410000.SMART");
617
618 let spread = parse_option_spread_instrument_id(
619 instrument_id,
620 &[(&leg1, 1), (&leg2, -1)],
621 None,
622 None,
623 )
624 .unwrap();
625
626 assert_eq!(spread.price_precision(), 2);
627 assert_eq!(spread.price_increment(), Price::from("0.01"));
628 }
629}
630
631fn parse_index_contract(
636 details: &ibapi::contracts::ContractDetails,
637 instrument_id: InstrumentId,
638) -> InstrumentAny {
639 let price_precision = tick_size_to_precision(details.min_tick);
640 let size_precision = tick_size_to_precision(details.min_size);
641 let timestamp = get_atomic_clock_realtime().get_time_ns();
642
643 let instrument = IndexInstrument::builder()
644 .instrument_id(instrument_id)
645 .raw_symbol(Symbol::from(details.contract.local_symbol.as_str()))
646 .currency(Currency::from(details.contract.currency.to_string()))
647 .price_precision(price_precision)
648 .size_precision(size_precision)
649 .price_increment(Price::new(details.min_tick, price_precision))
650 .size_increment(Quantity::new(details.size_increment, size_precision))
651 .info(ib_contract_info(details))
652 .ts_event(timestamp)
653 .ts_init(timestamp)
654 .build()
655 .unwrap();
656
657 InstrumentAny::from(instrument)
658}
659
660pub fn parse_spread_instrument_id(
669 instrument_id: InstrumentId,
670 leg_contract_details: &[(&ibapi::contracts::ContractDetails, i32)],
671 timestamp_ns: Option<UnixNanos>,
672) -> anyhow::Result<OptionSpread> {
673 if leg_contract_details.is_empty() {
674 anyhow::bail!("leg_contract_details must be provided");
675 }
676
677 let (first_details, _) = leg_contract_details[0];
679 let first_contract = &first_details.contract;
680
681 let currency = Currency::from(first_contract.currency.to_string());
683 let underlying = if !first_details.under_symbol.is_empty() {
684 Ustr::from(first_details.under_symbol.as_str())
685 } else {
686 Ustr::from(first_contract.symbol.as_str())
687 };
688
689 let multiplier_str = first_contract.multiplier.to_string();
691 let multiplier =
692 Quantity::from_str(&multiplier_str).unwrap_or_else(|_| Quantity::new(100.0, 0)); let asset_class = match first_contract.security_type {
696 ibapi::contracts::SecurityType::FuturesOption => AssetClass::Index, _ => AssetClass::Equity, };
699
700 let min_tick = leg_contract_details
702 .iter()
703 .map(|(details, _)| details.min_tick)
704 .fold(first_details.min_tick, f64::min);
705 let price_precision = tick_size_to_precision(min_tick);
706 let price_increment = Price::new(min_tick, price_precision);
707
708 let timestamp = timestamp_ns.unwrap_or_else(|| get_atomic_clock_realtime().get_time_ns());
710
711 let lot_size = multiplier;
713
714 let spread = OptionSpread::builder()
716 .instrument_id(instrument_id)
717 .raw_symbol(Symbol::from(instrument_id.symbol.as_str()))
718 .asset_class(asset_class)
719 .underlying(underlying)
720 .strategy_type(Ustr::from("SPREAD"))
721 .activation_ns(UnixNanos::new(0))
723 .expiration_ns(UnixNanos::new(0))
725 .currency(currency)
726 .price_precision(price_precision)
727 .price_increment(price_increment)
728 .multiplier(multiplier)
729 .lot_size(lot_size)
730 .margin_init(Decimal::ZERO)
731 .margin_maint(Decimal::ZERO)
732 .maker_fee(Decimal::ZERO)
733 .taker_fee(Decimal::ZERO)
734 .ts_event(timestamp)
735 .ts_init(timestamp)
736 .build()?;
737
738 Ok(spread)
739}
740
741pub fn parse_option_spread_instrument_id(
742 instrument_id: InstrumentId,
743 leg_contract_details: &[(&ibapi::contracts::ContractDetails, i32)],
744 bag_contract: Option<&ibapi::contracts::Contract>,
745 timestamp_ns: Option<UnixNanos>,
746) -> anyhow::Result<OptionSpread> {
747 let mut spread = parse_spread_instrument_id(instrument_id, leg_contract_details, timestamp_ns)?;
748 spread.info = bag_contract.map(ib_contract_info_for_contract);
749 Ok(spread)
750}
751
752pub fn parse_futures_spread_instrument_id(
753 instrument_id: InstrumentId,
754 leg_contract_details: &[(&ibapi::contracts::ContractDetails, i32)],
755 bag_contract: Option<&ibapi::contracts::Contract>,
756 timestamp_ns: Option<UnixNanos>,
757) -> anyhow::Result<FuturesSpread> {
758 if leg_contract_details.is_empty() {
759 anyhow::bail!("leg_contract_details must be provided");
760 }
761
762 let (first_details, _) = leg_contract_details[0];
763 let first_contract = &first_details.contract;
764 let currency = Currency::from(first_contract.currency.to_string());
765 let underlying = if !first_details.under_symbol.is_empty() {
766 Ustr::from(first_details.under_symbol.as_str())
767 } else {
768 Ustr::from(first_contract.symbol.as_str())
769 };
770 let multiplier = Quantity::from_str(&first_contract.multiplier.to_string())
771 .unwrap_or_else(|_| Quantity::new(1.0, 0));
772 let min_tick = leg_contract_details
773 .iter()
774 .map(|(details, _)| details.min_tick)
775 .fold(first_details.min_tick, f64::min);
776 let price_precision = tick_size_to_precision(min_tick);
777 let price_increment = Price::new(min_tick, price_precision);
778 let timestamp = timestamp_ns.unwrap_or_else(|| get_atomic_clock_realtime().get_time_ns());
779
780 Ok(FuturesSpread::builder()
781 .instrument_id(instrument_id)
782 .raw_symbol(Symbol::from(instrument_id.symbol.as_str()))
783 .asset_class(AssetClass::Index)
784 .underlying(underlying)
785 .strategy_type(Ustr::from("SPREAD"))
786 .activation_ns(UnixNanos::new(0))
787 .expiration_ns(UnixNanos::new(0))
788 .currency(currency)
789 .price_precision(price_precision)
790 .price_increment(price_increment)
791 .multiplier(multiplier)
792 .lot_size(Quantity::new(1.0, 0))
793 .margin_init(Decimal::ZERO)
794 .margin_maint(Decimal::ZERO)
795 .maker_fee(Decimal::ZERO)
796 .taker_fee(Decimal::ZERO)
797 .maybe_info(bag_contract.map(ib_contract_info_for_contract))
798 .ts_event(timestamp)
799 .ts_init(timestamp)
800 .build()?)
801}
802
803pub fn parse_spread_instrument_any(
804 instrument_id: InstrumentId,
805 leg_contract_details: &[(&ibapi::contracts::ContractDetails, i32)],
806 bag_contract: Option<&ibapi::contracts::Contract>,
807 timestamp_ns: Option<UnixNanos>,
808) -> anyhow::Result<InstrumentAny> {
809 let has_future = leg_contract_details.iter().any(|(details, _)| {
810 matches!(
811 details.contract.security_type,
812 SecurityType::Future | SecurityType::ContinuousFuture
813 )
814 });
815
816 if has_future {
817 Ok(InstrumentAny::from(parse_futures_spread_instrument_id(
818 instrument_id,
819 leg_contract_details,
820 bag_contract,
821 timestamp_ns,
822 )?))
823 } else {
824 Ok(InstrumentAny::from(parse_option_spread_instrument_id(
825 instrument_id,
826 leg_contract_details,
827 bag_contract,
828 timestamp_ns,
829 )?))
830 }
831}
832
833fn parse_cfd_contract(
835 details: &ibapi::contracts::ContractDetails,
836 instrument_id: InstrumentId,
837) -> InstrumentAny {
838 let price_precision = tick_size_to_precision(details.min_tick);
839 let size_precision = tick_size_to_precision(details.min_size);
840 let timestamp = get_atomic_clock_realtime().get_time_ns();
841
842 let base_currency = details
843 .contract
844 .local_symbol
845 .contains('.')
846 .then(|| Currency::from(details.contract.symbol.to_string()));
847
848 let instrument = Cfd::builder()
849 .instrument_id(instrument_id)
850 .raw_symbol(Symbol::from(details.contract.local_symbol.as_str()))
851 .asset_class(sec_type_to_asset_class(
852 details.under_security_type.as_str(),
853 ))
854 .maybe_base_currency(base_currency)
855 .quote_currency(Currency::from(details.contract.currency.to_string()))
856 .price_precision(price_precision)
857 .size_precision(size_precision)
858 .price_increment(Price::new(details.min_tick, price_precision))
859 .size_increment(Quantity::new(details.size_increment, size_precision))
860 .info(ib_contract_info(details))
861 .ts_event(timestamp)
862 .ts_init(timestamp)
863 .build()
864 .unwrap();
865
866 InstrumentAny::from(instrument)
867}
868
869fn parse_commodity_contract(
871 details: &ibapi::contracts::ContractDetails,
872 instrument_id: InstrumentId,
873) -> InstrumentAny {
874 let price_precision = tick_size_to_precision(details.min_tick);
875 let size_precision = tick_size_to_precision(details.min_size);
876 let timestamp = get_atomic_clock_realtime().get_time_ns();
877
878 let instrument = Commodity::builder()
879 .instrument_id(instrument_id)
880 .raw_symbol(Symbol::from(details.contract.local_symbol.as_str()))
881 .asset_class(AssetClass::Commodity)
882 .quote_currency(Currency::from(details.contract.currency.to_string()))
883 .price_precision(price_precision)
884 .size_precision(size_precision)
885 .price_increment(Price::new(details.min_tick, price_precision))
886 .size_increment(Quantity::new(details.size_increment, size_precision))
887 .info(ib_contract_info(details))
888 .ts_event(timestamp)
889 .ts_init(timestamp)
890 .build()
891 .unwrap();
892
893 InstrumentAny::from(instrument)
894}
895
896fn parse_bond_contract(
898 details: &ibapi::contracts::ContractDetails,
899 instrument_id: InstrumentId,
900) -> InstrumentAny {
901 let price_precision = tick_size_to_precision(details.min_tick);
904 let timestamp = get_atomic_clock_realtime().get_time_ns();
905
906 let instrument = Equity::builder()
908 .instrument_id(instrument_id)
909 .raw_symbol(Symbol::from(details.contract.local_symbol.as_str()))
910 .currency(Currency::from(details.contract.currency.to_string()))
911 .price_precision(price_precision)
912 .price_increment(Price::new(details.min_tick, price_precision))
913 .lot_size(Quantity::new(1.0, 0))
915 .info(ib_contract_info(details))
916 .ts_event(timestamp)
917 .ts_init(timestamp)
918 .build()
919 .unwrap();
920
921 InstrumentAny::from(instrument)
922}