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