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