Skip to main content

nautilus_interactive_brokers/python/
config.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 bindings for Interactive Brokers configuration types.
17
18use 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    /// Creates a new `InteractiveBrokersDataClientConfig` instance.
53    #[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    /// Returns the host.
106    #[getter]
107    fn host(&self) -> &str {
108        &self.host
109    }
110
111    /// Returns the port.
112    #[getter]
113    fn port(&self) -> u16 {
114        self.port
115    }
116
117    /// Returns the client ID.
118    #[getter]
119    fn client_id(&self) -> i32 {
120        self.client_id
121    }
122
123    /// Returns whether to use regular trading hours only.
124    #[getter]
125    fn use_regular_trading_hours(&self) -> bool {
126        self.use_regular_trading_hours
127    }
128
129    /// Returns the market data type.
130    #[getter]
131    fn market_data_type(&self) -> MarketDataType {
132        self.market_data_type
133    }
134
135    /// Returns whether to ignore quote tick size updates.
136    #[getter]
137    fn ignore_quote_tick_size_updates(&self) -> bool {
138        self.ignore_quote_tick_size_updates
139    }
140
141    /// Returns the connection timeout in seconds.
142    #[getter]
143    fn connection_timeout(&self) -> u64 {
144        self.connection_timeout
145    }
146
147    /// Returns the request timeout in seconds.
148    #[getter]
149    fn request_timeout(&self) -> u64 {
150        self.request_timeout
151    }
152
153    /// Returns whether to handle revised bars.
154    #[getter]
155    fn handle_revised_bars(&self) -> bool {
156        self.handle_revised_bars
157    }
158
159    /// Returns whether to use batch quotes.
160    #[getter]
161    fn batch_quotes(&self) -> bool {
162        self.batch_quotes
163    }
164
165    /// Returns whether tick-by-tick subscriptions include special-condition trades.
166    #[getter]
167    fn all_last_trades(&self) -> bool {
168        self.all_last_trades
169    }
170
171    /// Returns the optional subscription idle interval in seconds.
172    #[getter]
173    fn subscription_idle_timeout_secs(&self) -> Option<u64> {
174        self.subscription_idle_timeout_secs
175    }
176
177    /// Returns the optional bound on ibapi's automatic reconnect attempts.
178    #[getter]
179    fn max_reconnect_attempts(&self) -> Option<u32> {
180        self.max_reconnect_attempts
181    }
182
183    /// Returns the instrument provider configuration.
184    #[getter]
185    fn instrument_provider(&self) -> InteractiveBrokersInstrumentProviderConfig {
186        self.instrument_provider.clone()
187    }
188
189    /// Sets the instrument provider configuration.
190    #[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    /// Creates a new `InteractiveBrokersExecutionClientConfig` instance.
203    #[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    /// Returns the host.
252    #[getter]
253    fn host(&self) -> &str {
254        &self.host
255    }
256
257    /// Returns the port.
258    #[getter]
259    fn port(&self) -> u16 {
260        self.port
261    }
262
263    /// Returns the client ID.
264    #[getter]
265    fn client_id(&self) -> i32 {
266        self.client_id
267    }
268
269    /// Returns the raw IB account code.
270    #[getter]
271    fn account_id(&self) -> Option<String> {
272        self.account_id.clone()
273    }
274
275    /// Returns the account base currency.
276    #[getter]
277    fn base_currency(&self) -> Option<Currency> {
278        self.base_currency
279    }
280
281    /// Returns the connection timeout in seconds.
282    #[getter]
283    fn connection_timeout(&self) -> u64 {
284        self.connection_timeout
285    }
286
287    /// Returns the request timeout in seconds.
288    #[getter]
289    fn request_timeout(&self) -> u64 {
290        self.request_timeout
291    }
292
293    /// Returns whether to fetch all open orders.
294    #[getter]
295    fn fetch_all_open_orders(&self) -> bool {
296        self.fetch_all_open_orders
297    }
298
299    /// Returns whether to track option exercise from position updates.
300    #[getter]
301    fn track_option_exercise_from_position_update(&self) -> bool {
302        self.track_option_exercise_from_position_update
303    }
304
305    /// Returns the optional bound on ibapi's automatic reconnect attempts.
306    #[getter]
307    fn max_reconnect_attempts(&self) -> Option<u32> {
308        self.max_reconnect_attempts
309    }
310
311    /// Returns the instrument provider configuration.
312    #[getter]
313    fn instrument_provider(&self) -> InteractiveBrokersInstrumentProviderConfig {
314        self.instrument_provider.clone()
315    }
316
317    /// Sets the instrument provider configuration.
318    #[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    /// Creates a new `InteractiveBrokersInstrumentProviderConfig` instance.
331    #[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    /// Returns the symbology method.
389    #[getter]
390    fn symbology_method(&self) -> SymbologyMethod {
391        self.symbology_method
392    }
393
394    /// Returns the instrument IDs to load on startup.
395    #[getter]
396    fn load_ids(&self) -> std::collections::HashSet<InstrumentId> {
397        self.load_ids.clone()
398    }
399
400    /// Returns the IB contracts to load on startup.
401    #[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    /// Returns the minimum expiry days.
416    #[getter]
417    fn min_expiry_days(&self) -> Option<u32> {
418        self.min_expiry_days
419    }
420
421    /// Returns the maximum expiry days.
422    #[getter]
423    fn max_expiry_days(&self) -> Option<u32> {
424        self.max_expiry_days
425    }
426
427    /// Returns whether to build full options chain.
428    #[getter]
429    fn build_options_chain(&self) -> Option<bool> {
430        self.build_options_chain
431    }
432
433    /// Returns whether to build full futures chain.
434    #[getter]
435    fn build_futures_chain(&self) -> Option<bool> {
436        self.build_futures_chain
437    }
438
439    /// Returns the cache validity in days.
440    #[getter]
441    fn cache_validity_days(&self) -> Option<u32> {
442        self.cache_validity_days
443    }
444
445    /// Returns whether to convert IB exchanges to MIC venues.
446    #[getter]
447    fn convert_exchange_to_mic_venue(&self) -> bool {
448        self.convert_exchange_to_mic_venue
449    }
450
451    /// Returns the symbol to MIC venue mapping.
452    #[getter]
453    fn symbol_to_mic_venue(&self) -> std::collections::HashMap<String, String> {
454        self.symbol_to_mic_venue.clone()
455    }
456
457    /// Returns the filter security types.
458    #[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    /// Returns the custom instrument filter callable path.
467    #[getter]
468    fn filter_callable(&self) -> Option<String> {
469        self.filter_callable.clone()
470    }
471
472    /// Returns the cache path for persistent instrument caching.
473    #[getter]
474    fn cache_path(&self) -> Option<String> {
475        self.cache_path.clone()
476    }
477
478    /// Sets the cache path for persistent instrument caching.
479    #[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    /// Creates a new `DockerizedIBGatewayConfig` instance.
489    #[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    /// Returns the username.
513    #[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    /// Returns the trading mode.
524    #[getter]
525    fn trading_mode(&self) -> TradingMode {
526        self.trading_mode
527    }
528
529    /// Returns whether read-only API is enabled.
530    #[getter]
531    fn read_only_api(&self) -> bool {
532        self.read_only_api
533    }
534
535    /// Returns the timeout in seconds.
536    #[getter]
537    fn timeout(&self) -> u64 {
538        self.timeout
539    }
540
541    /// Returns the container image.
542    #[getter]
543    fn container_image(&self) -> &str {
544        &self.container_image
545    }
546
547    /// Returns the VNC port.
548    #[getter]
549    fn vnc_port(&self) -> Option<u16> {
550        self.vnc_port
551    }
552}