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    let get_str = |key: &str| -> String {
88        obj.get(key)
89            .and_then(|v| v.as_str())
90            .unwrap_or_default()
91            .to_string()
92    };
93
94    let get_i32 = |key: &str| -> i32 {
95        obj.get(key)
96            .and_then(|v| v.as_i64())
97            .map_or(0, |n| n as i32)
98    };
99
100    let get_f64 = |key: &str| -> f64 { obj.get(key).and_then(|v| v.as_f64()).unwrap_or(0.0) };
101
102    let get_bool = |key: &str| -> bool { obj.get(key).and_then(|v| v.as_bool()).unwrap_or(false) };
103
104    let parse_option_right = |key: &str| -> Option<OptionRight> {
105        match IbOptionRight::from_str(&get_str(key)).ok()? {
106            IbOptionRight::Call => Some(OptionRight::Call),
107            IbOptionRight::Put => Some(OptionRight::Put),
108        }
109    };
110
111    let parse_security_id_type = |key: &str| -> Option<SecurityIdType> {
112        match get_str(key).to_ascii_uppercase().as_str() {
113            "CUSIP" => Some(SecurityIdType::Cusip),
114            "ISIN" => Some(SecurityIdType::Isin),
115            "SEDOL" => Some(SecurityIdType::Sedol),
116            "RIC" => Some(SecurityIdType::Ric),
117            "FIGI" => Some(SecurityIdType::Figi),
118            _ => None,
119        }
120    };
121
122    // Parse security type
123    let sec_type_str = get_str("secType");
124    let security_type = if sec_type_str.is_empty() {
125        SecurityType::Stock
126    } else {
127        IbSecurityType::from_str(&sec_type_str).map_or_else(
128            |_| SecurityType::Other(sec_type_str.clone()),
129            IbSecurityType::ibapi_security_type,
130        )
131    };
132
133    Ok(Contract {
134        contract_id: get_i32("conId"),
135        symbol: Symbol::from(get_str("symbol")),
136        security_type,
137        last_trade_date_or_contract_month: get_str("lastTradeDateOrContractMonth"),
138        strike: get_f64("strike"),
139        right: parse_option_right("right"),
140        multiplier: get_str("multiplier"),
141        exchange: IBExchange::from(get_str("exchange")),
142        currency: IBCurrency::from(get_str("currency")),
143        local_symbol: get_str("localSymbol"),
144        primary_exchange: IBExchange::from(get_str("primaryExchange")),
145        trading_class: get_str("tradingClass"),
146        include_expired: get_bool("includeExpired"),
147        security_id_type: parse_security_id_type("secIdType"),
148        security_id: get_str("secId"),
149        last_trade_date: None,
150        combo_legs_description: get_str("comboLegsDescrip"),
151        combo_legs: Vec::new(),       // TODO: Parse combo_legs if needed
152        delta_neutral_contract: None, // TODO: Parse delta_neutral_contract if needed
153        issuer_id: get_str("issuerId"),
154        description: get_str("description"),
155    })
156}
157
158/// Parse multiple IB contracts from JSON array.
159///
160/// # Errors
161///
162/// Returns an error if the JSON string is invalid or if any contract fails to parse.
163pub fn parse_contracts_from_json_array(json_str: &str) -> anyhow::Result<Vec<Contract>> {
164    let value: Value = serde_json::from_str(json_str).context("Failed to parse JSON string")?;
165
166    let array = value
167        .as_array()
168        .ok_or_else(|| anyhow::anyhow!("Expected JSON array for contracts"))?;
169
170    let mut contracts = Vec::new();
171
172    for (idx, item) in array.iter().enumerate() {
173        match parse_contract_from_json(item) {
174            Ok(contract) => contracts.push(contract),
175            Err(e) => {
176                tracing::warn!("Failed to parse contract at index {}: {}", idx, e);
177            }
178        }
179    }
180
181    Ok(contracts)
182}
183
184use anyhow::Context;