nautilus_interactive_brokers/python/
config.rs1use std::str::FromStr;
19
20use nautilus_core::{python::to_pyvalue_err, string::secret::SecretString};
21use nautilus_model::identifiers::InstrumentId;
22use pyo3::prelude::*;
23
24use crate::{
25 common::{
26 consts::{DEFAULT_CLIENT_ID, DEFAULT_HOST, DEFAULT_PORT},
27 contracts::parse_configured_contract_from_json,
28 enums::IbSecurityType,
29 },
30 config::{
31 DockerizedIBGatewayConfig, InteractiveBrokersDataClientConfig,
32 InteractiveBrokersExecutionClientConfig, InteractiveBrokersInstrumentProviderConfig,
33 MarketDataType, SymbologyMethod, TradingMode,
34 },
35 python::conversion::py_list_to_json_values,
36};
37
38fn validate_order_id_client_slot(client_id: i32) -> PyResult<()> {
39 if client_id.unsigned_abs().is_multiple_of(1000) {
40 return Err(to_pyvalue_err(format!(
41 "`client_id` must not be a multiple of 1000 for the Rust/PyO3 IB execution client because order ID partitioning uses client_id % 1000; got {client_id}"
42 )));
43 }
44
45 Ok(())
46}
47
48#[pymethods]
49#[pyo3_stub_gen::derive::gen_stub_pymethods]
50impl InteractiveBrokersDataClientConfig {
51 #[new]
53 #[pyo3(signature = (host=None, port=None, client_id=None, use_regular_trading_hours=None, market_data_type=None, ignore_quote_tick_size_updates=None, connection_timeout=None, request_timeout=None, handle_revised_bars=None, batch_quotes=None, instrument_provider=None, dockerized_gateway=None, all_last_trades=None, subscription_idle_timeout_secs=None))]
54 #[allow(clippy::too_many_arguments)]
55 fn py_new(
56 host: Option<String>,
57 port: Option<u16>,
58 client_id: Option<i32>,
59 use_regular_trading_hours: Option<bool>,
60 market_data_type: Option<MarketDataType>,
61 ignore_quote_tick_size_updates: Option<bool>,
62 connection_timeout: Option<u64>,
63 request_timeout: Option<u64>,
64 handle_revised_bars: Option<bool>,
65 batch_quotes: Option<bool>,
66 instrument_provider: Option<InteractiveBrokersInstrumentProviderConfig>,
67 dockerized_gateway: Option<&DockerizedIBGatewayConfig>,
68 all_last_trades: Option<bool>,
69 subscription_idle_timeout_secs: Option<u64>,
70 ) -> PyResult<Self> {
71 if dockerized_gateway.is_some() {
72 return Err(to_pyvalue_err(
73 "`dockerized_gateway` is not wired into the Rust/PyO3 IB data client; start `DockerizedIBGateway` separately and pass `host`/`port`",
74 ));
75 }
76
77 let host = host.unwrap_or_else(|| DEFAULT_HOST.to_string());
78 let port = port.unwrap_or(DEFAULT_PORT);
79 let client_id = client_id.unwrap_or(DEFAULT_CLIENT_ID);
80 let request_timeout = request_timeout.unwrap_or(60);
81
82 let config = Self {
83 host,
84 port,
85 client_id,
86 use_regular_trading_hours: use_regular_trading_hours.unwrap_or(true),
87 market_data_type: market_data_type.unwrap_or_default(),
88 ignore_quote_tick_size_updates: ignore_quote_tick_size_updates.unwrap_or(false),
89 connection_timeout: connection_timeout.unwrap_or(300),
90 request_timeout,
91 handle_revised_bars: handle_revised_bars.unwrap_or(false),
92 batch_quotes: batch_quotes.unwrap_or(true),
93 all_last_trades: all_last_trades.unwrap_or_else(|| Self::default().all_last_trades),
94 subscription_idle_timeout_secs,
95 instrument_provider: instrument_provider.unwrap_or_default(),
96 };
97
98 config.validate().map_err(to_pyvalue_err)?;
99 Ok(config)
100 }
101
102 #[getter]
104 fn host(&self) -> &str {
105 &self.host
106 }
107
108 #[getter]
110 fn port(&self) -> u16 {
111 self.port
112 }
113
114 #[getter]
116 fn client_id(&self) -> i32 {
117 self.client_id
118 }
119
120 #[getter]
122 fn use_regular_trading_hours(&self) -> bool {
123 self.use_regular_trading_hours
124 }
125
126 #[getter]
128 fn market_data_type(&self) -> MarketDataType {
129 self.market_data_type
130 }
131
132 #[getter]
134 fn ignore_quote_tick_size_updates(&self) -> bool {
135 self.ignore_quote_tick_size_updates
136 }
137
138 #[getter]
140 fn connection_timeout(&self) -> u64 {
141 self.connection_timeout
142 }
143
144 #[getter]
146 fn request_timeout(&self) -> u64 {
147 self.request_timeout
148 }
149
150 #[getter]
152 fn handle_revised_bars(&self) -> bool {
153 self.handle_revised_bars
154 }
155
156 #[getter]
158 fn batch_quotes(&self) -> bool {
159 self.batch_quotes
160 }
161
162 #[getter]
164 fn all_last_trades(&self) -> bool {
165 self.all_last_trades
166 }
167
168 #[getter]
170 fn subscription_idle_timeout_secs(&self) -> Option<u64> {
171 self.subscription_idle_timeout_secs
172 }
173
174 #[getter]
176 fn instrument_provider(&self) -> InteractiveBrokersInstrumentProviderConfig {
177 self.instrument_provider.clone()
178 }
179
180 #[setter]
182 fn set_instrument_provider(
183 &mut self,
184 instrument_provider: InteractiveBrokersInstrumentProviderConfig,
185 ) {
186 self.instrument_provider = instrument_provider;
187 }
188}
189
190#[pymethods]
191#[pyo3_stub_gen::derive::gen_stub_pymethods]
192impl InteractiveBrokersExecutionClientConfig {
193 #[new]
195 #[pyo3(signature = (host=None, port=None, client_id=None, account_id=None, connection_timeout=None, request_timeout=None, fetch_all_open_orders=None, track_option_exercise_from_position_update=None, instrument_provider=None, dockerized_gateway=None))]
196 #[allow(clippy::too_many_arguments)]
197 fn py_new(
198 host: Option<String>,
199 port: Option<u16>,
200 client_id: Option<i32>,
201 account_id: Option<String>,
202 connection_timeout: Option<u64>,
203 request_timeout: Option<u64>,
204 fetch_all_open_orders: Option<bool>,
205 track_option_exercise_from_position_update: Option<bool>,
206 instrument_provider: Option<InteractiveBrokersInstrumentProviderConfig>,
207 dockerized_gateway: Option<&DockerizedIBGatewayConfig>,
208 ) -> PyResult<Self> {
209 if dockerized_gateway.is_some() {
210 return Err(to_pyvalue_err(
211 "`dockerized_gateway` is not wired into the Rust/PyO3 IB execution client; start `DockerizedIBGateway` separately and pass `host`/`port`",
212 ));
213 }
214
215 let host = host.unwrap_or_else(|| DEFAULT_HOST.to_string());
216 let port = port.unwrap_or(DEFAULT_PORT);
217 let client_id = client_id.unwrap_or(DEFAULT_CLIENT_ID);
218 validate_order_id_client_slot(client_id)?;
219 let request_timeout = request_timeout.unwrap_or(60);
220
221 Ok(Self {
222 host,
223 port,
224 client_id,
225 account_id,
226 connection_timeout: connection_timeout.unwrap_or(300),
227 request_timeout,
228 fetch_all_open_orders: fetch_all_open_orders.unwrap_or(false),
229 track_option_exercise_from_position_update: track_option_exercise_from_position_update
230 .unwrap_or(false),
231 instrument_provider: instrument_provider.unwrap_or_default(),
232 })
233 }
234
235 #[getter]
237 fn host(&self) -> &str {
238 &self.host
239 }
240
241 #[getter]
243 fn port(&self) -> u16 {
244 self.port
245 }
246
247 #[getter]
249 fn client_id(&self) -> i32 {
250 self.client_id
251 }
252
253 #[getter]
255 fn account_id(&self) -> Option<String> {
256 self.account_id.clone()
257 }
258
259 #[getter]
261 fn connection_timeout(&self) -> u64 {
262 self.connection_timeout
263 }
264
265 #[getter]
267 fn request_timeout(&self) -> u64 {
268 self.request_timeout
269 }
270
271 #[getter]
273 fn fetch_all_open_orders(&self) -> bool {
274 self.fetch_all_open_orders
275 }
276
277 #[getter]
279 fn track_option_exercise_from_position_update(&self) -> bool {
280 self.track_option_exercise_from_position_update
281 }
282
283 #[getter]
285 fn instrument_provider(&self) -> InteractiveBrokersInstrumentProviderConfig {
286 self.instrument_provider.clone()
287 }
288
289 #[setter]
291 fn set_instrument_provider(
292 &mut self,
293 instrument_provider: InteractiveBrokersInstrumentProviderConfig,
294 ) {
295 self.instrument_provider = instrument_provider;
296 }
297}
298
299#[pymethods]
300#[pyo3_stub_gen::derive::gen_stub_pymethods]
301impl InteractiveBrokersInstrumentProviderConfig {
302 #[new]
304 #[pyo3(signature = (symbology_method=None, load_ids=None, load_contracts=None, min_expiry_days=None, max_expiry_days=None, build_options_chain=None, build_futures_chain=None, cache_validity_days=None, convert_exchange_to_mic_venue=None, symbol_to_mic_venue=None, filter_sec_types=None, filter_callable=None, cache_path=None))]
305 #[allow(clippy::too_many_arguments)]
306 fn py_new(
307 py: Python<'_>,
308 symbology_method: Option<SymbologyMethod>,
309 load_ids: Option<std::collections::HashSet<InstrumentId>>,
310 load_contracts: Option<Py<pyo3::types::PyList>>,
311 min_expiry_days: Option<u32>,
312 max_expiry_days: Option<u32>,
313 build_options_chain: Option<bool>,
314 build_futures_chain: Option<bool>,
315 cache_validity_days: Option<u32>,
316 convert_exchange_to_mic_venue: Option<bool>,
317 symbol_to_mic_venue: Option<std::collections::HashMap<String, String>>,
318 filter_sec_types: Option<std::collections::HashSet<String>>,
319 filter_callable: Option<String>,
320 cache_path: Option<String>,
321 ) -> PyResult<Self> {
322 Ok(Self {
323 symbology_method: symbology_method.unwrap_or_default(),
324 load_ids: load_ids.unwrap_or_default(),
325 load_contracts: if let Some(c) = load_contracts {
326 py_list_to_json_values(c.bind(py))?
327 .iter()
328 .enumerate()
329 .map(|(index, value)| {
330 parse_configured_contract_from_json(value).map_err(|e| {
331 to_pyvalue_err(format!(
332 "Invalid load_contracts entry at index {index}: {e}"
333 ))
334 })
335 })
336 .collect::<PyResult<Vec<_>>>()?
337 } else {
338 Vec::new()
339 },
340 min_expiry_days,
341 max_expiry_days,
342 build_options_chain,
343 build_futures_chain,
344 cache_validity_days,
345 convert_exchange_to_mic_venue: convert_exchange_to_mic_venue.unwrap_or(false),
346 symbol_to_mic_venue: symbol_to_mic_venue.unwrap_or_default(),
347 filter_sec_types: filter_sec_types
348 .unwrap_or_default()
349 .into_iter()
350 .map(|value| IbSecurityType::from_str(&value).map_err(to_pyvalue_err))
351 .collect::<PyResult<_>>()?,
352 filter_callable,
353 cache_path,
354 })
355 }
356
357 #[getter]
359 fn symbology_method(&self) -> SymbologyMethod {
360 self.symbology_method
361 }
362
363 #[getter]
365 fn load_ids(&self) -> std::collections::HashSet<InstrumentId> {
366 self.load_ids.clone()
367 }
368
369 #[getter]
371 fn load_contracts(&self, py: Python<'_>) -> PyResult<Py<pyo3::types::PyList>> {
372 let json_mod = py.import("json")?;
373 let list = pyo3::types::PyList::empty(py);
374
375 for contract in &self.load_contracts {
376 let json_str = contract.to_json_value().to_string();
377 let dict = json_mod.call_method1("loads", (json_str,))?;
378 list.append(dict)?;
379 }
380
381 Ok(list.unbind())
382 }
383
384 #[getter]
386 fn min_expiry_days(&self) -> Option<u32> {
387 self.min_expiry_days
388 }
389
390 #[getter]
392 fn max_expiry_days(&self) -> Option<u32> {
393 self.max_expiry_days
394 }
395
396 #[getter]
398 fn build_options_chain(&self) -> Option<bool> {
399 self.build_options_chain
400 }
401
402 #[getter]
404 fn build_futures_chain(&self) -> Option<bool> {
405 self.build_futures_chain
406 }
407
408 #[getter]
410 fn cache_validity_days(&self) -> Option<u32> {
411 self.cache_validity_days
412 }
413
414 #[getter]
416 fn convert_exchange_to_mic_venue(&self) -> bool {
417 self.convert_exchange_to_mic_venue
418 }
419
420 #[getter]
422 fn symbol_to_mic_venue(&self) -> std::collections::HashMap<String, String> {
423 self.symbol_to_mic_venue.clone()
424 }
425
426 #[getter]
428 fn filter_sec_types(&self) -> Vec<String> {
429 self.filter_sec_types
430 .iter()
431 .map(ToString::to_string)
432 .collect()
433 }
434
435 #[getter]
437 fn filter_callable(&self) -> Option<String> {
438 self.filter_callable.clone()
439 }
440
441 #[getter]
443 fn cache_path(&self) -> Option<String> {
444 self.cache_path.clone()
445 }
446
447 #[setter]
449 fn set_cache_path(&mut self, cache_path: Option<String>) {
450 self.cache_path = cache_path;
451 }
452}
453
454#[pymethods]
455#[pyo3_stub_gen::derive::gen_stub_pymethods]
456impl DockerizedIBGatewayConfig {
457 #[new]
459 #[pyo3(signature = (username=None, password=None, trading_mode=None, read_only_api=None, timeout=None, container_image=None, vnc_port=None))]
460 fn py_new(
461 username: Option<String>,
462 password: Option<String>,
463 trading_mode: Option<TradingMode>,
464 read_only_api: Option<bool>,
465 timeout: Option<u64>,
466 container_image: Option<String>,
467 vnc_port: Option<u16>,
468 ) -> Self {
469 Self {
470 username: username.map(SecretString::from),
471 password: password.map(SecretString::from),
472 trading_mode: trading_mode.unwrap_or_default(),
473 read_only_api: read_only_api.unwrap_or(true),
474 timeout: timeout.unwrap_or(300),
475 container_image: container_image
476 .unwrap_or_else(|| "ghcr.io/gnzsnz/ib-gateway:stable".to_string()),
477 vnc_port,
478 }
479 }
480
481 #[getter]
483 fn username(&self) -> Option<&str> {
484 self.username.as_ref().map(SecretString::expose_secret)
485 }
486
487 #[getter]
488 const fn has_password(&self) -> bool {
489 self.password.is_some()
490 }
491
492 #[getter]
494 fn trading_mode(&self) -> TradingMode {
495 self.trading_mode
496 }
497
498 #[getter]
500 fn read_only_api(&self) -> bool {
501 self.read_only_api
502 }
503
504 #[getter]
506 fn timeout(&self) -> u64 {
507 self.timeout
508 }
509
510 #[getter]
512 fn container_image(&self) -> &str {
513 &self.container_image
514 }
515
516 #[getter]
518 fn vnc_port(&self) -> Option<u16> {
519 self.vnc_port
520 }
521}