Skip to main content

nautilus_interactive_brokers/python/
conversion.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//! Python conversion utilities for Interactive Brokers types.
17
18use ibapi::contracts::{Contract, ContractDetails, SecurityType};
19use nautilus_core::python::{to_pytype_err, to_pyvalue_err};
20use pyo3::{
21    prelude::*,
22    types::{PyBytes, PyDict, PyList},
23};
24
25use crate::common::{
26    contracts::parse_contract_from_json,
27    enums::{IbComboLegOpenClose, IbSecurityType},
28};
29
30/// Convert a Python object to a JSON value.
31pub fn py_to_json_value(obj: &Bound<'_, PyAny>) -> PyResult<serde_json::Value> {
32    // Try to call .json() first (NautilusConfig)
33    if let Ok(json_bytes) = obj.call_method0("json") {
34        if let Ok(bytes) = json_bytes.clone().cast_into::<PyBytes>() {
35            let json_str = std::str::from_utf8(bytes.as_bytes())
36                .map_err(|e| to_pyvalue_err(format!("Invalid UTF-8 in json output: {e}")))?;
37
38            let value: serde_json::Value = serde_json::from_str(json_str)
39                .map_err(|e| to_pyvalue_err(format!("Invalid JSON: {e}")))?;
40
41            return Ok(value);
42        }
43    }
44
45    // Try to treat as dict
46    if let Ok(dict) = obj.clone().cast_into::<pyo3::types::PyDict>() {
47        // Convert dict to JSON value using Python's json module
48        let json_mod = obj.py().import("json")?;
49        let json_str_obj = json_mod.call_method1("dumps", (dict,))?;
50        let json_str = json_str_obj.extract::<String>()?;
51
52        let value: serde_json::Value = serde_json::from_str(&json_str)
53            .map_err(|e| to_pyvalue_err(format!("Invalid JSON from dict: {e}")))?;
54
55        return Ok(value);
56    }
57
58    Err(to_pytype_err("Expected object with .json() or dict"))
59}
60
61/// Convert a Python object (IBContract or dict) to a Rust Contract.
62///
63/// # Errors
64///
65/// Returns a PyValueError if conversion fails.
66pub fn py_to_contract(obj: &Bound<'_, PyAny>) -> PyResult<Contract> {
67    let value = py_to_json_value(obj)?;
68    parse_contract_from_json(&value)
69        .map_err(|e| to_pyvalue_err(format!("Failed to parse contract: {e}")))
70}
71
72/// Convert a Python list of objects to a vector of Rust Contracts.
73pub fn py_list_to_contracts(obj: &Bound<'_, PyAny>) -> PyResult<Vec<Contract>> {
74    let list = obj.clone().cast_into::<pyo3::types::PyList>()?;
75    let mut contracts = Vec::with_capacity(list.len());
76    for item in list.iter() {
77        contracts.push(py_to_contract(&item)?);
78    }
79    Ok(contracts)
80}
81
82/// Convert a Python list of objects to a vector of JSON values.
83pub fn py_list_to_json_values(obj: &Bound<'_, PyAny>) -> PyResult<Vec<serde_json::Value>> {
84    let list = obj.clone().cast_into::<pyo3::types::PyList>()?;
85    let mut values = Vec::with_capacity(list.len());
86    for item in list.iter() {
87        values.push(py_to_json_value(&item)?);
88    }
89    Ok(values)
90}
91
92fn security_type_to_ib_str(security_type: &SecurityType) -> String {
93    IbSecurityType::try_from(security_type).map_or_else(
94        |_| security_type.to_string(),
95        |security_type| security_type.to_string(),
96    )
97}
98
99fn combo_leg_open_close_to_i32(open_close: ibapi::contracts::ComboLegOpenClose) -> i32 {
100    IbComboLegOpenClose::from(open_close).as_i32()
101}
102
103pub fn contract_to_pydict<'py>(
104    py: Python<'py>,
105    contract: &Contract,
106) -> PyResult<Bound<'py, PyDict>> {
107    let dict = PyDict::new(py);
108    dict.set_item("secType", security_type_to_ib_str(&contract.security_type))?;
109    dict.set_item("conId", contract.contract_id)?;
110    dict.set_item("exchange", contract.exchange.as_str())?;
111    dict.set_item("primaryExchange", contract.primary_exchange.as_str())?;
112    dict.set_item("symbol", contract.symbol.as_str())?;
113    dict.set_item("localSymbol", contract.local_symbol.as_str())?;
114    dict.set_item("currency", contract.currency.as_str())?;
115    dict.set_item("tradingClass", &contract.trading_class)?;
116    dict.set_item(
117        "lastTradeDateOrContractMonth",
118        &contract.last_trade_date_or_contract_month,
119    )?;
120    dict.set_item(
121        "lastTradeDate",
122        contract
123            .last_trade_date
124            .as_ref()
125            .map(ToString::to_string)
126            .unwrap_or_default(),
127    )?;
128    dict.set_item("multiplier", &contract.multiplier)?;
129    dict.set_item("strike", contract.strike)?;
130    dict.set_item(
131        "right",
132        contract.right.as_ref().map_or("", |right| right.as_str()),
133    )?;
134    dict.set_item("includeExpired", contract.include_expired)?;
135    dict.set_item(
136        "secIdType",
137        contract
138            .security_id_type
139            .as_ref()
140            .map_or("", |security_id_type| security_id_type.as_str()),
141    )?;
142    dict.set_item("secId", &contract.security_id)?;
143    dict.set_item("description", &contract.description)?;
144    dict.set_item("issuerId", &contract.issuer_id)?;
145    dict.set_item("comboLegsDescrip", &contract.combo_legs_description)?;
146
147    if !contract.combo_legs.is_empty() {
148        let combo_legs = PyList::empty(py);
149        for leg in &contract.combo_legs {
150            let leg_dict = PyDict::new(py);
151            leg_dict.set_item("conId", leg.contract_id)?;
152            leg_dict.set_item("ratio", leg.ratio)?;
153            leg_dict.set_item("action", leg.action.as_str())?;
154            leg_dict.set_item("exchange", &leg.exchange)?;
155            leg_dict.set_item("openClose", combo_leg_open_close_to_i32(leg.open_close))?;
156            leg_dict.set_item("shortSaleSlot", leg.short_sale_slot)?;
157            leg_dict.set_item("designatedLocation", &leg.designated_location)?;
158            leg_dict.set_item("exemptCode", leg.exempt_code)?;
159            combo_legs.append(leg_dict)?;
160        }
161        dict.set_item("comboLegs", combo_legs)?;
162    }
163
164    if let Some(delta_neutral) = &contract.delta_neutral_contract {
165        let delta_dict = PyDict::new(py);
166        delta_dict.set_item("conId", delta_neutral.contract_id)?;
167        delta_dict.set_item("delta", delta_neutral.delta)?;
168        delta_dict.set_item("price", delta_neutral.price)?;
169        dict.set_item("deltaNeutralContract", delta_dict)?;
170    }
171
172    Ok(dict)
173}
174
175pub fn contract_details_to_pyobject(
176    py: Python<'_>,
177    details: &ContractDetails,
178) -> PyResult<Py<PyAny>> {
179    let common = py.import("nautilus_trader.adapters.interactive_brokers.common")?;
180    let dict_to_contract_details = common.getattr("dict_to_contract_details")?;
181    let details_dict = PyDict::new(py);
182
183    details_dict.set_item("contract", contract_to_pydict(py, &details.contract)?)?;
184    details_dict.set_item("marketName", &details.market_name)?;
185    details_dict.set_item("minTick", details.min_tick)?;
186    details_dict.set_item("orderTypes", details.order_types.join(","))?;
187    details_dict.set_item("validExchanges", details.valid_exchanges.join(","))?;
188    details_dict.set_item("priceMagnifier", details.price_magnifier)?;
189    details_dict.set_item("underConId", details.under_contract_id)?;
190    details_dict.set_item("longName", &details.long_name)?;
191    details_dict.set_item("contractMonth", &details.contract_month)?;
192    details_dict.set_item("industry", &details.industry)?;
193    details_dict.set_item("category", &details.category)?;
194    details_dict.set_item("subcategory", &details.subcategory)?;
195    details_dict.set_item("timeZoneId", &details.time_zone_id)?;
196    details_dict.set_item("tradingHours", details.trading_hours.join(";"))?;
197    details_dict.set_item("liquidHours", details.liquid_hours.join(";"))?;
198    details_dict.set_item("evRule", &details.ev_rule)?;
199    details_dict.set_item("evMultiplier", details.ev_multiplier)?;
200    details_dict.set_item("aggGroup", details.agg_group)?;
201    details_dict.set_item("underSymbol", &details.under_symbol)?;
202    details_dict.set_item("underSecType", &details.under_security_type)?;
203    details_dict.set_item("marketRuleIds", details.market_rule_ids.join(","))?;
204    details_dict.set_item("realExpirationDate", &details.real_expiration_date)?;
205    details_dict.set_item("lastTradeTime", &details.last_trade_time)?;
206    details_dict.set_item("stockType", &details.stock_type)?;
207    details_dict.set_item("cusip", &details.cusip)?;
208    details_dict.set_item("ratings", &details.ratings)?;
209    details_dict.set_item("descAppend", &details.desc_append)?;
210    details_dict.set_item("bondType", &details.bond_type)?;
211    details_dict.set_item("couponType", &details.coupon_type)?;
212    details_dict.set_item("callable", details.callable)?;
213    details_dict.set_item("putable", details.putable)?;
214    details_dict.set_item("coupon", details.coupon)?;
215    details_dict.set_item("convertible", details.convertible)?;
216    details_dict.set_item("maturity", &details.maturity)?;
217    details_dict.set_item("issueDate", &details.issue_date)?;
218    details_dict.set_item("nextOptionDate", &details.next_option_date)?;
219    details_dict.set_item("nextOptionType", &details.next_option_type)?;
220    details_dict.set_item("nextOptionPartial", details.next_option_partial)?;
221    details_dict.set_item("notes", &details.notes)?;
222    details_dict.set_item("minSize", details.min_size.to_string())?;
223    details_dict.set_item("sizeIncrement", details.size_increment.to_string())?;
224    details_dict.set_item(
225        "suggestedSizeIncrement",
226        details.suggested_size_increment.to_string(),
227    )?;
228    details_dict.set_item("fundName", &details.fund_name)?;
229    details_dict.set_item("fundFamily", &details.fund_family)?;
230    details_dict.set_item("fundType", &details.fund_type)?;
231    details_dict.set_item("fundFrontLoad", &details.fund_front_load)?;
232    details_dict.set_item("fundBackLoad", &details.fund_back_load)?;
233    details_dict.set_item(
234        "fundBackLoadTimeInterval",
235        &details.fund_back_load_time_interval,
236    )?;
237    details_dict.set_item("fundManagementFee", &details.fund_management_fee)?;
238    details_dict.set_item("fundClosed", details.fund_closed)?;
239    details_dict.set_item(
240        "fundClosedForNewInvestors",
241        details.fund_closed_for_new_investors,
242    )?;
243    details_dict.set_item("fundClosedForNewMoney", details.fund_closed_for_new_money)?;
244    details_dict.set_item("fundNotifyAmount", &details.fund_notify_amount)?;
245    details_dict.set_item(
246        "fundMinimumInitialPurchase",
247        &details.fund_minimum_initial_purchase,
248    )?;
249    details_dict.set_item(
250        "fundSubsequentMinimumPurchase",
251        &details.fund_subsequent_minimum_purchase,
252    )?;
253    details_dict.set_item("fundBlueSkyStates", &details.fund_blue_sky_states)?;
254    details_dict.set_item("fundBlueSkyTerritories", &details.fund_blue_sky_territories)?;
255
256    if !details.sec_id_list.is_empty() {
257        let sec_id_list = PyDict::new(py);
258        for item in &details.sec_id_list {
259            sec_id_list.set_item(&item.tag, &item.value)?;
260        }
261        details_dict.set_item("secIdList", sec_id_list)?;
262    }
263
264    let result = dict_to_contract_details.call1((details_dict,))?;
265    Ok(result.unbind())
266}