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_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    /// Creates a new `InteractiveBrokersDataClientConfig` instance.
52    #[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    /// Returns the host.
103    #[getter]
104    fn host(&self) -> &str {
105        &self.host
106    }
107
108    /// Returns the port.
109    #[getter]
110    fn port(&self) -> u16 {
111        self.port
112    }
113
114    /// Returns the client ID.
115    #[getter]
116    fn client_id(&self) -> i32 {
117        self.client_id
118    }
119
120    /// Returns whether to use regular trading hours only.
121    #[getter]
122    fn use_regular_trading_hours(&self) -> bool {
123        self.use_regular_trading_hours
124    }
125
126    /// Returns the market data type.
127    #[getter]
128    fn market_data_type(&self) -> MarketDataType {
129        self.market_data_type
130    }
131
132    /// Returns whether to ignore quote tick size updates.
133    #[getter]
134    fn ignore_quote_tick_size_updates(&self) -> bool {
135        self.ignore_quote_tick_size_updates
136    }
137
138    /// Returns the connection timeout in seconds.
139    #[getter]
140    fn connection_timeout(&self) -> u64 {
141        self.connection_timeout
142    }
143
144    /// Returns the request timeout in seconds.
145    #[getter]
146    fn request_timeout(&self) -> u64 {
147        self.request_timeout
148    }
149
150    /// Returns whether to handle revised bars.
151    #[getter]
152    fn handle_revised_bars(&self) -> bool {
153        self.handle_revised_bars
154    }
155
156    /// Returns whether to use batch quotes.
157    #[getter]
158    fn batch_quotes(&self) -> bool {
159        self.batch_quotes
160    }
161
162    /// Returns whether tick-by-tick subscriptions include special-condition trades.
163    #[getter]
164    fn all_last_trades(&self) -> bool {
165        self.all_last_trades
166    }
167
168    /// Returns the optional subscription idle interval in seconds.
169    #[getter]
170    fn subscription_idle_timeout_secs(&self) -> Option<u64> {
171        self.subscription_idle_timeout_secs
172    }
173
174    /// Returns the instrument provider configuration.
175    #[getter]
176    fn instrument_provider(&self) -> InteractiveBrokersInstrumentProviderConfig {
177        self.instrument_provider.clone()
178    }
179
180    /// Sets the instrument provider configuration.
181    #[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    /// Creates a new `InteractiveBrokersExecutionClientConfig` instance.
194    #[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    /// Returns the host.
236    #[getter]
237    fn host(&self) -> &str {
238        &self.host
239    }
240
241    /// Returns the port.
242    #[getter]
243    fn port(&self) -> u16 {
244        self.port
245    }
246
247    /// Returns the client ID.
248    #[getter]
249    fn client_id(&self) -> i32 {
250        self.client_id
251    }
252
253    /// Returns the raw IB account code.
254    #[getter]
255    fn account_id(&self) -> Option<String> {
256        self.account_id.clone()
257    }
258
259    /// Returns the connection timeout in seconds.
260    #[getter]
261    fn connection_timeout(&self) -> u64 {
262        self.connection_timeout
263    }
264
265    /// Returns the request timeout in seconds.
266    #[getter]
267    fn request_timeout(&self) -> u64 {
268        self.request_timeout
269    }
270
271    /// Returns whether to fetch all open orders.
272    #[getter]
273    fn fetch_all_open_orders(&self) -> bool {
274        self.fetch_all_open_orders
275    }
276
277    /// Returns whether to track option exercise from position updates.
278    #[getter]
279    fn track_option_exercise_from_position_update(&self) -> bool {
280        self.track_option_exercise_from_position_update
281    }
282
283    /// Returns the instrument provider configuration.
284    #[getter]
285    fn instrument_provider(&self) -> InteractiveBrokersInstrumentProviderConfig {
286        self.instrument_provider.clone()
287    }
288
289    /// Sets the instrument provider configuration.
290    #[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    /// Creates a new `InteractiveBrokersInstrumentProviderConfig` instance.
303    #[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    /// Returns the symbology method.
358    #[getter]
359    fn symbology_method(&self) -> SymbologyMethod {
360        self.symbology_method
361    }
362
363    /// Returns the instrument IDs to load on startup.
364    #[getter]
365    fn load_ids(&self) -> std::collections::HashSet<InstrumentId> {
366        self.load_ids.clone()
367    }
368
369    /// Returns the IB contracts to load on startup.
370    #[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    /// Returns the minimum expiry days.
385    #[getter]
386    fn min_expiry_days(&self) -> Option<u32> {
387        self.min_expiry_days
388    }
389
390    /// Returns the maximum expiry days.
391    #[getter]
392    fn max_expiry_days(&self) -> Option<u32> {
393        self.max_expiry_days
394    }
395
396    /// Returns whether to build full options chain.
397    #[getter]
398    fn build_options_chain(&self) -> Option<bool> {
399        self.build_options_chain
400    }
401
402    /// Returns whether to build full futures chain.
403    #[getter]
404    fn build_futures_chain(&self) -> Option<bool> {
405        self.build_futures_chain
406    }
407
408    /// Returns the cache validity in days.
409    #[getter]
410    fn cache_validity_days(&self) -> Option<u32> {
411        self.cache_validity_days
412    }
413
414    /// Returns whether to convert IB exchanges to MIC venues.
415    #[getter]
416    fn convert_exchange_to_mic_venue(&self) -> bool {
417        self.convert_exchange_to_mic_venue
418    }
419
420    /// Returns the symbol to MIC venue mapping.
421    #[getter]
422    fn symbol_to_mic_venue(&self) -> std::collections::HashMap<String, String> {
423        self.symbol_to_mic_venue.clone()
424    }
425
426    /// Returns the filter security types.
427    #[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    /// Returns the custom instrument filter callable path.
436    #[getter]
437    fn filter_callable(&self) -> Option<String> {
438        self.filter_callable.clone()
439    }
440
441    /// Returns the cache path for persistent instrument caching.
442    #[getter]
443    fn cache_path(&self) -> Option<String> {
444        self.cache_path.clone()
445    }
446
447    /// Sets the cache path for persistent instrument caching.
448    #[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    /// Creates a new `DockerizedIBGatewayConfig` instance.
458    #[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    /// Returns the username.
482    #[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    /// Returns the trading mode.
493    #[getter]
494    fn trading_mode(&self) -> TradingMode {
495        self.trading_mode
496    }
497
498    /// Returns whether read-only API is enabled.
499    #[getter]
500    fn read_only_api(&self) -> bool {
501        self.read_only_api
502    }
503
504    /// Returns the timeout in seconds.
505    #[getter]
506    fn timeout(&self) -> u64 {
507        self.timeout
508    }
509
510    /// Returns the container image.
511    #[getter]
512    fn container_image(&self) -> &str {
513        &self.container_image
514    }
515
516    /// Returns the VNC port.
517    #[getter]
518    fn vnc_port(&self) -> Option<u16> {
519        self.vnc_port
520    }
521}