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