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 ibapi::contracts::{
19    Contract, Currency as IBCurrency, Exchange as IBExchange, SecurityType, Symbol,
20};
21use nautilus_core::Params;
22use serde_json::Value;
23
24/// Convert an IB contract into JSON metadata suitable for instrument `info["contract"]`.
25#[must_use]
26pub fn contract_to_json_value(contract: &Contract) -> Value {
27    serde_json::json!({
28        "secType": security_type_to_code(&contract.security_type),
29        "conId": contract.contract_id,
30        "exchange": contract.exchange.to_string(),
31        "primaryExchange": contract.primary_exchange.to_string(),
32        "symbol": contract.symbol.to_string(),
33        "localSymbol": contract.local_symbol,
34        "currency": contract.currency.to_string(),
35        "tradingClass": contract.trading_class,
36        "lastTradeDateOrContractMonth": contract.last_trade_date_or_contract_month,
37        "multiplier": contract.multiplier,
38        "strike": contract.strike,
39        "right": contract.right,
40        "includeExpired": contract.include_expired,
41        "secIdType": contract.security_id_type,
42        "secId": contract.security_id,
43        "description": contract.description,
44        "issuerId": contract.issuer_id,
45        "comboLegsDescrip": contract.combo_legs_description,
46    })
47}
48
49#[must_use]
50pub fn contract_to_params(contract: &Contract) -> Params {
51    let mut params = Params::new();
52
53    if let Value::Object(map) = contract_to_json_value(contract) {
54        for (key, value) in map {
55            params.insert(key, value);
56        }
57    }
58
59    params
60}
61
62fn security_type_to_code(security_type: &SecurityType) -> &str {
63    match security_type {
64        SecurityType::Stock => "STK",
65        SecurityType::Option => "OPT",
66        SecurityType::Future => "FUT",
67        SecurityType::FuturesOption => "FOP",
68        SecurityType::ForexPair => "CASH",
69        SecurityType::Crypto => "CRYPTO",
70        SecurityType::ContinuousFuture => "CONTFUT",
71        SecurityType::Index => "IND",
72        SecurityType::CFD => "CFD",
73        SecurityType::Commodity => "CMDTY",
74        SecurityType::Bond => "BOND",
75        SecurityType::Warrant => "WAR",
76        SecurityType::News => "NEWS",
77        SecurityType::MutualFund => "FUND",
78        SecurityType::Spread => "BAG",
79        SecurityType::Other(other) => other.as_str(),
80    }
81}
82
83/// Parse IB contract from JSON dictionary.
84///
85/// This function parses a JSON object (dictionary) representing an IBContract
86/// and converts it to a rust-ibapi Contract struct.
87///
88/// # Arguments
89///
90/// * `json` - JSON value representing the contract dictionary
91///
92/// # Returns
93///
94/// Returns a Contract if parsing succeeds, or None if parsing fails.
95///
96/// # Errors
97///
98/// Returns an error if the JSON is not a valid object or if required fields are missing.
99pub fn parse_contract_from_json(json: &Value) -> anyhow::Result<Contract> {
100    let obj = json
101        .as_object()
102        .ok_or_else(|| anyhow::anyhow!("Expected JSON object for contract"))?;
103
104    // Helper to get string field with default
105    let get_str = |key: &str| -> String {
106        obj.get(key)
107            .and_then(|v| v.as_str())
108            .unwrap_or_default()
109            .to_string()
110    };
111
112    // Helper to get i32 field with default
113    let get_i32 = |key: &str| -> i32 {
114        obj.get(key)
115            .and_then(|v| v.as_i64())
116            .map_or(0, |n| n as i32)
117    };
118
119    // Helper to get f64 field with default
120    let get_f64 = |key: &str| -> f64 { obj.get(key).and_then(|v| v.as_f64()).unwrap_or(0.0) };
121
122    // Helper to get bool field with default
123    let get_bool = |key: &str| -> bool { obj.get(key).and_then(|v| v.as_bool()).unwrap_or(false) };
124
125    // Parse security type
126    let sec_type_str = get_str("secType");
127    let security_type = match sec_type_str.as_str() {
128        "STK" | "stk" => SecurityType::Stock,
129        "OPT" | "opt" => SecurityType::Option,
130        "FUT" | "fut" => SecurityType::Future,
131        "FOP" | "fop" => SecurityType::FuturesOption,
132        "CASH" | "cash" => SecurityType::ForexPair,
133        "CRYPTO" | "crypto" => SecurityType::Crypto,
134        "IND" | "ind" => SecurityType::Index,
135        "CFD" | "cfd" => SecurityType::CFD,
136        "CMDTY" | "cmdty" => SecurityType::Commodity,
137        "BOND" | "bond" => SecurityType::Bond,
138        "BAG" | "bag" => SecurityType::Spread,
139        "" => SecurityType::Stock, // Default to stock
140        other => SecurityType::Other(other.to_string()),
141    };
142
143    Ok(Contract {
144        contract_id: get_i32("conId"),
145        symbol: Symbol::from(get_str("symbol")),
146        security_type,
147        last_trade_date_or_contract_month: get_str("lastTradeDateOrContractMonth"),
148        strike: get_f64("strike"),
149        right: get_str("right"),
150        multiplier: get_str("multiplier"),
151        exchange: IBExchange::from(get_str("exchange")),
152        currency: IBCurrency::from(get_str("currency")),
153        local_symbol: get_str("localSymbol"),
154        primary_exchange: IBExchange::from(get_str("primaryExchange")),
155        trading_class: get_str("tradingClass"),
156        include_expired: get_bool("includeExpired"),
157        security_id_type: get_str("secIdType"),
158        security_id: get_str("secId"),
159        last_trade_date: None,
160        combo_legs_description: get_str("comboLegsDescrip"),
161        combo_legs: Vec::new(),       // TODO: Parse combo_legs if needed
162        delta_neutral_contract: None, // TODO: Parse delta_neutral_contract if needed
163        issuer_id: get_str("issuerId"),
164        description: get_str("description"),
165    })
166}
167
168/// Parse multiple IB contracts from JSON array.
169///
170/// # Arguments
171///
172/// * `json_str` - JSON string containing an array of contract dictionaries
173///
174/// # Returns
175///
176/// Returns a vector of parsed contracts.
177///
178/// # Errors
179///
180/// Returns an error if the JSON string is invalid or if any contract fails to parse.
181pub fn parse_contracts_from_json_array(json_str: &str) -> anyhow::Result<Vec<Contract>> {
182    let value: Value = serde_json::from_str(json_str).context("Failed to parse JSON string")?;
183
184    let array = value
185        .as_array()
186        .ok_or_else(|| anyhow::anyhow!("Expected JSON array for contracts"))?;
187
188    let mut contracts = Vec::new();
189
190    for (idx, item) in array.iter().enumerate() {
191        match parse_contract_from_json(item) {
192            Ok(contract) => contracts.push(contract),
193            Err(e) => {
194                tracing::warn!("Failed to parse contract at index {}: {}", idx, e);
195            }
196        }
197    }
198
199    Ok(contracts)
200}
201
202use anyhow::Context;