Skip to main content

nautilus_interactive_brokers/common/
contracts.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//! Contract parsing utilities for Interactive Brokers adapter.
17
18use std::str::FromStr;
19
20use ibapi::contracts::{
21    Contract, Currency as IBCurrency, Exchange as IBExchange, OptionRight, SecurityIdType,
22    SecurityType, Symbol,
23};
24use nautilus_core::Params;
25use serde_json::Value;
26
27use crate::common::enums::{IbOptionRight, IbSecurityType};
28
29/// Convert an IB contract into JSON metadata suitable for instrument `info["contract"]`.
30#[must_use]
31pub fn contract_to_json_value(contract: &Contract) -> Value {
32    serde_json::json!({
33        "secType": security_type_to_code(&contract.security_type),
34        "conId": contract.contract_id,
35        "exchange": contract.exchange.to_string(),
36        "primaryExchange": contract.primary_exchange.to_string(),
37        "symbol": contract.symbol.to_string(),
38        "localSymbol": contract.local_symbol,
39        "currency": contract.currency.to_string(),
40        "tradingClass": contract.trading_class,
41        "lastTradeDateOrContractMonth": contract.last_trade_date_or_contract_month,
42        "multiplier": contract.multiplier,
43        "strike": contract.strike,
44        "right": contract.right,
45        "includeExpired": contract.include_expired,
46        "secIdType": contract.security_id_type,
47        "secId": contract.security_id,
48        "description": contract.description,
49        "issuerId": contract.issuer_id,
50        "comboLegsDescrip": contract.combo_legs_description,
51    })
52}
53
54#[must_use]
55pub fn contract_to_params(contract: &Contract) -> Params {
56    let mut params = Params::new();
57
58    if let Value::Object(map) = contract_to_json_value(contract) {
59        for (key, value) in map {
60            params.insert(key, value);
61        }
62    }
63
64    params
65}
66
67fn security_type_to_code(security_type: &SecurityType) -> String {
68    IbSecurityType::try_from(security_type).map_or_else(
69        |_| security_type.to_string(),
70        |security_type| security_type.to_string(),
71    )
72}
73
74/// Parse IB contract from JSON dictionary.
75///
76/// This function parses a JSON object (dictionary) representing an IBContract
77/// and converts it to a rust-ibapi Contract struct.
78///
79/// # Errors
80///
81/// Returns an error if the JSON is not a valid object or if required fields are missing.
82pub fn parse_contract_from_json(json: &Value) -> anyhow::Result<Contract> {
83    let obj = json
84        .as_object()
85        .ok_or_else(|| anyhow::anyhow!("Expected JSON object for contract"))?;
86
87    // Helper to get string field with default
88    let get_str = |key: &str| -> String {
89        obj.get(key)
90            .and_then(|v| v.as_str())
91            .unwrap_or_default()
92            .to_string()
93    };
94
95    // Helper to get i32 field with default
96    let get_i32 = |key: &str| -> i32 {
97        obj.get(key)
98            .and_then(|v| v.as_i64())
99            .map_or(0, |n| n as i32)
100    };
101
102    // Helper to get f64 field with default
103    let get_f64 = |key: &str| -> f64 { obj.get(key).and_then(|v| v.as_f64()).unwrap_or(0.0) };
104
105    // Helper to get bool field with default
106    let get_bool = |key: &str| -> bool { obj.get(key).and_then(|v| v.as_bool()).unwrap_or(false) };
107
108    let parse_option_right = |key: &str| -> Option<OptionRight> {
109        match IbOptionRight::from_str(&get_str(key)).ok()? {
110            IbOptionRight::Call => Some(OptionRight::Call),
111            IbOptionRight::Put => Some(OptionRight::Put),
112        }
113    };
114
115    let parse_security_id_type = |key: &str| -> Option<SecurityIdType> {
116        match get_str(key).to_ascii_uppercase().as_str() {
117            "CUSIP" => Some(SecurityIdType::Cusip),
118            "ISIN" => Some(SecurityIdType::Isin),
119            "SEDOL" => Some(SecurityIdType::Sedol),
120            "RIC" => Some(SecurityIdType::Ric),
121            "FIGI" => Some(SecurityIdType::Figi),
122            _ => None,
123        }
124    };
125
126    // Parse security type
127    let sec_type_str = get_str("secType");
128    let security_type = if sec_type_str.is_empty() {
129        SecurityType::Stock
130    } else {
131        IbSecurityType::from_str(&sec_type_str).map_or_else(
132            |_| SecurityType::Other(sec_type_str.clone()),
133            IbSecurityType::ibapi_security_type,
134        )
135    };
136
137    Ok(Contract {
138        contract_id: get_i32("conId"),
139        symbol: Symbol::from(get_str("symbol")),
140        security_type,
141        last_trade_date_or_contract_month: get_str("lastTradeDateOrContractMonth"),
142        strike: get_f64("strike"),
143        right: parse_option_right("right"),
144        multiplier: get_str("multiplier"),
145        exchange: IBExchange::from(get_str("exchange")),
146        currency: IBCurrency::from(get_str("currency")),
147        local_symbol: get_str("localSymbol"),
148        primary_exchange: IBExchange::from(get_str("primaryExchange")),
149        trading_class: get_str("tradingClass"),
150        include_expired: get_bool("includeExpired"),
151        security_id_type: parse_security_id_type("secIdType"),
152        security_id: get_str("secId"),
153        last_trade_date: None,
154        combo_legs_description: get_str("comboLegsDescrip"),
155        combo_legs: Vec::new(),       // TODO: Parse combo_legs if needed
156        delta_neutral_contract: None, // TODO: Parse delta_neutral_contract if needed
157        issuer_id: get_str("issuerId"),
158        description: get_str("description"),
159    })
160}
161
162/// Parse multiple IB contracts from JSON array.
163///
164/// # Errors
165///
166/// Returns an error if the JSON string is invalid or if any contract fails to parse.
167pub fn parse_contracts_from_json_array(json_str: &str) -> anyhow::Result<Vec<Contract>> {
168    let value: Value = serde_json::from_str(json_str).context("Failed to parse JSON string")?;
169
170    let array = value
171        .as_array()
172        .ok_or_else(|| anyhow::anyhow!("Expected JSON array for contracts"))?;
173
174    let mut contracts = Vec::new();
175
176    for (idx, item) in array.iter().enumerate() {
177        match parse_contract_from_json(item) {
178            Ok(contract) => contracts.push(contract),
179            Err(e) => {
180                tracing::warn!("Failed to parse contract at index {}: {}", idx, e);
181            }
182        }
183    }
184
185    Ok(contracts)
186}
187
188use anyhow::Context;