Skip to main content

nautilus_interactive_brokers/common/
spreads.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Generic spread identifier adaptation for Interactive Brokers combo contracts.
17
18use nautilus_model::identifiers::{
19    InstrumentId, new_generic_spread_id, parse_generic_spread_id_legs,
20};
21
22/// Returns whether an instrument ID uses the generic spread grammar.
23#[must_use]
24pub fn is_spread_instrument_id(instrument_id: &InstrumentId) -> bool {
25    parse_generic_spread_id_legs(instrument_id).is_ok()
26}
27
28/// Creates a generic spread instrument ID from IB-compatible leg ratios.
29///
30/// # Errors
31///
32/// Returns an error if the legs do not satisfy the generic spread grammar.
33pub 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
41/// Parses a generic spread instrument ID into IB-compatible leg ratios.
42///
43/// # Errors
44///
45/// Returns an error if the ID does not use the generic spread grammar or a ratio is outside the
46/// range supported by IB combo legs.
47pub 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}