nautilus_interactive_brokers/common/
spreads.rs1use nautilus_model::identifiers::{
19 InstrumentId, new_generic_spread_id, parse_generic_spread_id_legs,
20};
21
22#[must_use]
24pub fn is_spread_instrument_id(instrument_id: &InstrumentId) -> bool {
25 parse_generic_spread_id_legs(instrument_id).is_ok()
26}
27
28pub fn create_spread_instrument_id(legs: &[(InstrumentId, i32)]) -> anyhow::Result<InstrumentId> {
34 let legs = legs
35 .iter()
36 .map(|(instrument_id, ratio)| (*instrument_id, i64::from(*ratio)))
37 .collect::<Vec<_>>();
38 new_generic_spread_id(&legs)
39}
40
41pub fn parse_spread_instrument_id_to_legs(
48 instrument_id: &InstrumentId,
49) -> anyhow::Result<Vec<(InstrumentId, i32)>> {
50 parse_generic_spread_id_legs(instrument_id)?
51 .into_iter()
52 .map(|(instrument_id, ratio)| {
53 i32::try_from(ratio)
54 .map(|ratio| (instrument_id, ratio))
55 .map_err(|_| anyhow::anyhow!("Spread leg ratio {ratio} exceeds the IB i32 range"))
56 })
57 .collect()
58}
59
60#[cfg(test)]
61mod tests {
62 use nautilus_model::identifiers::{InstrumentId, new_generic_spread_id};
63 use rstest::rstest;
64
65 use super::{
66 create_spread_instrument_id, is_spread_instrument_id, parse_spread_instrument_id_to_legs,
67 };
68
69 #[rstest]
70 fn test_spread_adapter_matches_generic_spread_grammar() {
71 let legs = [
72 (InstrumentId::from("MSFT.NASDAQ"), -2),
73 (InstrumentId::from("AAPL.NASDAQ"), 1),
74 ];
75 let generic_legs = legs
76 .iter()
77 .map(|(instrument_id, ratio)| (*instrument_id, i64::from(*ratio)))
78 .collect::<Vec<_>>();
79
80 let spread = create_spread_instrument_id(&legs).unwrap();
81
82 assert_eq!(spread, new_generic_spread_id(&generic_legs).unwrap());
83 assert_eq!(spread, InstrumentId::from("(1)AAPL___((2))MSFT.NASDAQ"));
84 assert_eq!(
85 parse_spread_instrument_id_to_legs(&spread).unwrap(),
86 [
87 (InstrumentId::from("AAPL.NASDAQ"), 1),
88 (InstrumentId::from("MSFT.NASDAQ"), -2),
89 ]
90 );
91 assert!(is_spread_instrument_id(&spread));
92 }
93
94 #[rstest]
95 fn test_spread_adapter_rejects_legacy_single_underscore_separator() {
96 let instrument_id = InstrumentId::from("(1)AAPL_((2))MSFT.NASDAQ");
97
98 assert!(parse_spread_instrument_id_to_legs(&instrument_id).is_err());
99 assert!(!is_spread_instrument_id(&instrument_id));
100 }
101}