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 nautilus_core::python::to_pyvalue_err;
19use nautilus_model::identifiers::InstrumentId;
20use pyo3::prelude::*;
21
22use crate::config::{
23    DockerizedIBGatewayConfig, InteractiveBrokersDataClientConfig,
24    InteractiveBrokersExecClientConfig, InteractiveBrokersInstrumentProviderConfig, MarketDataType,
25    SymbologyMethod, TradingMode,
26};
27
28fn validate_order_id_client_slot(client_id: i32) -> PyResult<()> {
29    if client_id.unsigned_abs().is_multiple_of(1000) {
30        return Err(to_pyvalue_err(format!(
31            "`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}"
32        )));
33    }
34
35    Ok(())
36}
37
38#[pymethods]
39impl MarketDataType {
40    #[classattr]
41    const REALTIME: Self = Self::Realtime;
42
43    #[classattr]
44    const FROZEN: Self = Self::Frozen;
45
46    #[classattr]
47    const DELAYED: Self = Self::Delayed;
48
49    #[classattr]
50    const DELAYED_FROZEN: Self = Self::DelayedFrozen;
51}
52
53#[pymethods]
54impl SymbologyMethod {
55    #[classattr]
56    const SIMPLIFIED: Self = Self::Simplified;
57
58    #[classattr]
59    const RAW: Self = Self::Raw;
60}
61
62#[pymethods]
63impl TradingMode {
64    #[classattr]
65    const PAPER: Self = Self::Paper;
66
67    #[classattr]
68    const LIVE: Self = Self::Live;
69}
70
71#[pymethods]
72impl InteractiveBrokersDataClientConfig {
73    /// Creates a new `InteractiveBrokersDataClientConfig` instance.
74    #[new]
75    #[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))]
76    #[allow(clippy::too_many_arguments)]
77    fn py_new(
78        host: Option<String>,
79        port: Option<u16>,
80        client_id: Option<i32>,
81        use_regular_trading_hours: Option<bool>,
82        market_data_type: Option<MarketDataType>,
83        ignore_quote_tick_size_updates: Option<bool>,
84        connection_timeout: Option<u64>,
85        request_timeout: Option<u64>,
86        handle_revised_bars: Option<bool>,
87        batch_quotes: Option<bool>,
88        instrument_provider: Option<InteractiveBrokersInstrumentProviderConfig>,
89        dockerized_gateway: Option<&DockerizedIBGatewayConfig>,
90    ) -> PyResult<Self> {
91        if dockerized_gateway.is_some() {
92            return Err(to_pyvalue_err(
93                "`dockerized_gateway` is not wired into the Rust/PyO3 IB data client; start `DockerizedIBGateway` separately and pass `host`/`port`",
94            ));
95        }
96
97        let host = host.unwrap_or_else(|| crate::common::consts::DEFAULT_HOST.to_string());
98        let port = port.unwrap_or(crate::common::consts::DEFAULT_PORT);
99        let client_id = client_id.unwrap_or(crate::common::consts::DEFAULT_CLIENT_ID);
100        let request_timeout = request_timeout.unwrap_or(60);
101
102        Ok(Self {
103            host,
104            port,
105            client_id,
106            use_regular_trading_hours: use_regular_trading_hours.unwrap_or(true),
107            market_data_type: market_data_type.unwrap_or_default(),
108            ignore_quote_tick_size_updates: ignore_quote_tick_size_updates.unwrap_or(false),
109            connection_timeout: connection_timeout.unwrap_or(300),
110            request_timeout,
111            handle_revised_bars: handle_revised_bars.unwrap_or(false),
112            batch_quotes: batch_quotes.unwrap_or(true),
113            instrument_provider: instrument_provider.unwrap_or_default(),
114        })
115    }
116
117    /// Returns the host.
118    #[getter]
119    fn host(&self) -> &str {
120        &self.host
121    }
122
123    /// Returns the port.
124    #[getter]
125    fn port(&self) -> u16 {
126        self.port
127    }
128
129    /// Returns the client ID.
130    #[getter]
131    fn client_id(&self) -> i32 {
132        self.client_id
133    }
134
135    /// Returns whether to use regular trading hours only.
136    #[getter]
137    fn use_regular_trading_hours(&self) -> bool {
138        self.use_regular_trading_hours
139    }
140
141    /// Returns the market data type.
142    #[getter]
143    fn market_data_type(&self) -> MarketDataType {
144        self.market_data_type
145    }
146
147    /// Returns whether to ignore quote tick size updates.
148    #[getter]
149    fn ignore_quote_tick_size_updates(&self) -> bool {
150        self.ignore_quote_tick_size_updates
151    }
152
153    /// Returns the connection timeout in seconds.
154    #[getter]
155    fn connection_timeout(&self) -> u64 {
156        self.connection_timeout
157    }
158
159    /// Returns the request timeout in seconds.
160    #[getter]
161    fn request_timeout(&self) -> u64 {
162        self.request_timeout
163    }
164
165    /// Returns whether to handle revised bars.
166    #[getter]
167    fn handle_revised_bars(&self) -> bool {
168        self.handle_revised_bars
169    }
170
171    /// Returns whether to use batch quotes.
172    #[getter]
173    fn batch_quotes(&self) -> bool {
174        self.batch_quotes
175    }
176
177    /// Returns the instrument provider configuration.
178    #[getter]
179    fn instrument_provider(&self) -> InteractiveBrokersInstrumentProviderConfig {
180        self.instrument_provider.clone()
181    }
182
183    /// Sets the instrument provider configuration.
184    #[setter]
185    fn set_instrument_provider(
186        &mut self,
187        instrument_provider: InteractiveBrokersInstrumentProviderConfig,
188    ) {
189        self.instrument_provider = instrument_provider;
190    }
191}
192
193#[pymethods]
194impl InteractiveBrokersExecClientConfig {
195    /// Creates a new `InteractiveBrokersExecClientConfig` instance.
196    #[new]
197    #[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))]
198    #[allow(clippy::too_many_arguments)]
199    fn py_new(
200        host: Option<String>,
201        port: Option<u16>,
202        client_id: Option<i32>,
203        account_id: Option<String>,
204        connection_timeout: Option<u64>,
205        request_timeout: Option<u64>,
206        fetch_all_open_orders: Option<bool>,
207        track_option_exercise_from_position_update: Option<bool>,
208        instrument_provider: Option<InteractiveBrokersInstrumentProviderConfig>,
209        dockerized_gateway: Option<&DockerizedIBGatewayConfig>,
210    ) -> PyResult<Self> {
211        if dockerized_gateway.is_some() {
212            return Err(to_pyvalue_err(
213                "`dockerized_gateway` is not wired into the Rust/PyO3 IB execution client; start `DockerizedIBGateway` separately and pass `host`/`port`",
214            ));
215        }
216
217        let host = host.unwrap_or_else(|| crate::common::consts::DEFAULT_HOST.to_string());
218        let port = port.unwrap_or(crate::common::consts::DEFAULT_PORT);
219        let client_id = client_id.unwrap_or(crate::common::consts::DEFAULT_CLIENT_ID);
220        validate_order_id_client_slot(client_id)?;
221        let request_timeout = request_timeout.unwrap_or(60);
222
223        Ok(Self {
224            host,
225            port,
226            client_id,
227            account_id,
228            connection_timeout: connection_timeout.unwrap_or(300),
229            request_timeout,
230            fetch_all_open_orders: fetch_all_open_orders.unwrap_or(false),
231            track_option_exercise_from_position_update: track_option_exercise_from_position_update
232                .unwrap_or(false),
233            instrument_provider: instrument_provider.unwrap_or_default(),
234        })
235    }
236
237    /// Returns the host.
238    #[getter]
239    fn host(&self) -> &str {
240        &self.host
241    }
242
243    /// Returns the port.
244    #[getter]
245    fn port(&self) -> u16 {
246        self.port
247    }
248
249    /// Returns the client ID.
250    #[getter]
251    fn client_id(&self) -> i32 {
252        self.client_id
253    }
254
255    /// Returns the account ID.
256    #[getter]
257    fn account_id(&self) -> Option<String> {
258        self.account_id.clone()
259    }
260
261    /// Returns the connection timeout in seconds.
262    #[getter]
263    fn connection_timeout(&self) -> u64 {
264        self.connection_timeout
265    }
266
267    /// Returns the request timeout in seconds.
268    #[getter]
269    fn request_timeout(&self) -> u64 {
270        self.request_timeout
271    }
272
273    /// Returns whether to fetch all open orders.
274    #[getter]
275    fn fetch_all_open_orders(&self) -> bool {
276        self.fetch_all_open_orders
277    }
278
279    /// Returns whether to track option exercise from position updates.
280    #[getter]
281    fn track_option_exercise_from_position_update(&self) -> bool {
282        self.track_option_exercise_from_position_update
283    }
284
285    /// Returns the instrument provider configuration.
286    #[getter]
287    fn instrument_provider(&self) -> InteractiveBrokersInstrumentProviderConfig {
288        self.instrument_provider.clone()
289    }
290
291    /// Sets the instrument provider configuration.
292    #[setter]
293    fn set_instrument_provider(
294        &mut self,
295        instrument_provider: InteractiveBrokersInstrumentProviderConfig,
296    ) {
297        self.instrument_provider = instrument_provider;
298    }
299}
300
301#[pymethods]
302impl InteractiveBrokersInstrumentProviderConfig {
303    /// Creates a new `InteractiveBrokersInstrumentProviderConfig` instance.
304    #[new]
305    #[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))]
306    #[allow(clippy::too_many_arguments)]
307    fn py_new(
308        py: Python<'_>,
309        symbology_method: Option<crate::config::SymbologyMethod>,
310        load_ids: Option<std::collections::HashSet<InstrumentId>>,
311        load_contracts: Option<Py<pyo3::types::PyList>>,
312        min_expiry_days: Option<u32>,
313        max_expiry_days: Option<u32>,
314        build_options_chain: Option<bool>,
315        build_futures_chain: Option<bool>,
316        cache_validity_days: Option<u32>,
317        convert_exchange_to_mic_venue: Option<bool>,
318        symbol_to_mic_venue: Option<std::collections::HashMap<String, String>>,
319        filter_sec_types: Option<std::collections::HashSet<String>>,
320        filter_callable: Option<String>,
321        cache_path: Option<String>,
322    ) -> PyResult<Self> {
323        Ok(Self {
324            symbology_method: symbology_method.unwrap_or_default(),
325            load_ids: load_ids.unwrap_or_default(),
326            load_contracts: if let Some(c) = load_contracts {
327                crate::python::conversion::py_list_to_json_values(c.bind(py))?
328            } else {
329                Vec::new()
330            },
331            min_expiry_days,
332            max_expiry_days,
333            build_options_chain,
334            build_futures_chain,
335            cache_validity_days,
336            convert_exchange_to_mic_venue: convert_exchange_to_mic_venue.unwrap_or(false),
337            symbol_to_mic_venue: symbol_to_mic_venue.unwrap_or_default(),
338            filter_sec_types: filter_sec_types.unwrap_or_default(),
339            filter_callable,
340            cache_path,
341        })
342    }
343
344    /// Returns the symbology method.
345    #[getter]
346    fn symbology_method(&self) -> crate::config::SymbologyMethod {
347        self.symbology_method
348    }
349
350    /// Returns the instrument IDs to load on startup.
351    #[getter]
352    fn load_ids(&self) -> std::collections::HashSet<InstrumentId> {
353        self.load_ids.clone()
354    }
355
356    /// Returns the IB contracts to load on startup.
357    #[getter]
358    fn load_contracts(&self, py: Python<'_>) -> PyResult<Py<pyo3::types::PyList>> {
359        let json_mod = py.import("json")?;
360        let list = pyo3::types::PyList::empty(py);
361
362        for value in &self.load_contracts {
363            let json_str = value.to_string();
364            let dict = json_mod.call_method1("loads", (json_str,))?;
365            list.append(dict)?;
366        }
367        Ok(list.unbind())
368    }
369
370    /// Returns the minimum expiry days.
371    #[getter]
372    fn min_expiry_days(&self) -> Option<u32> {
373        self.min_expiry_days
374    }
375
376    /// Returns the maximum expiry days.
377    #[getter]
378    fn max_expiry_days(&self) -> Option<u32> {
379        self.max_expiry_days
380    }
381
382    /// Returns whether to build full options chain.
383    #[getter]
384    fn build_options_chain(&self) -> Option<bool> {
385        self.build_options_chain
386    }
387
388    /// Returns whether to build full futures chain.
389    #[getter]
390    fn build_futures_chain(&self) -> Option<bool> {
391        self.build_futures_chain
392    }
393
394    /// Returns the cache validity in days.
395    #[getter]
396    fn cache_validity_days(&self) -> Option<u32> {
397        self.cache_validity_days
398    }
399
400    /// Returns whether to convert IB exchanges to MIC venues.
401    #[getter]
402    fn convert_exchange_to_mic_venue(&self) -> bool {
403        self.convert_exchange_to_mic_venue
404    }
405
406    /// Returns the symbol to MIC venue mapping.
407    #[getter]
408    fn symbol_to_mic_venue(&self) -> std::collections::HashMap<String, String> {
409        self.symbol_to_mic_venue.clone()
410    }
411
412    /// Returns the filter security types.
413    #[getter]
414    fn filter_sec_types(&self) -> Vec<String> {
415        self.filter_sec_types.iter().cloned().collect()
416    }
417
418    /// Returns the custom instrument filter callable path.
419    #[getter]
420    fn filter_callable(&self) -> Option<String> {
421        self.filter_callable.clone()
422    }
423
424    /// Returns the cache path for persistent instrument caching.
425    #[getter]
426    fn cache_path(&self) -> Option<String> {
427        self.cache_path.clone()
428    }
429
430    /// Sets the cache path for persistent instrument caching.
431    #[setter]
432    fn set_cache_path(&mut self, cache_path: Option<String>) {
433        self.cache_path = cache_path;
434    }
435}
436
437#[pymethods]
438impl DockerizedIBGatewayConfig {
439    /// Creates a new `DockerizedIBGatewayConfig` instance.
440    #[new]
441    #[pyo3(signature = (username=None, password=None, trading_mode=None, read_only_api=None, timeout=None, container_image=None, vnc_port=None))]
442    fn py_new(
443        username: Option<String>,
444        password: Option<String>,
445        trading_mode: Option<TradingMode>,
446        read_only_api: Option<bool>,
447        timeout: Option<u64>,
448        container_image: Option<String>,
449        vnc_port: Option<u16>,
450    ) -> Self {
451        Self {
452            username,
453            password,
454            trading_mode: trading_mode.unwrap_or_default(),
455            read_only_api: read_only_api.unwrap_or(true),
456            timeout: timeout.unwrap_or(300),
457            container_image: container_image
458                .unwrap_or_else(|| "ghcr.io/gnzsnz/ib-gateway:stable".to_string()),
459            vnc_port,
460        }
461    }
462
463    /// Returns the username.
464    #[getter]
465    fn username(&self) -> Option<String> {
466        self.username.clone()
467    }
468
469    /// Returns the password (masked for security).
470    #[getter]
471    fn password(&self) -> Option<String> {
472        self.password.as_ref().map(|_| "********".to_string())
473    }
474
475    /// Returns the trading mode.
476    #[getter]
477    fn trading_mode(&self) -> TradingMode {
478        self.trading_mode
479    }
480
481    /// Returns whether read-only API is enabled.
482    #[getter]
483    fn read_only_api(&self) -> bool {
484        self.read_only_api
485    }
486
487    /// Returns the timeout in seconds.
488    #[getter]
489    fn timeout(&self) -> u64 {
490        self.timeout
491    }
492
493    /// Returns the container image.
494    #[getter]
495    fn container_image(&self) -> &str {
496        &self.container_image
497    }
498
499    /// Returns the VNC port.
500    #[getter]
501    fn vnc_port(&self) -> Option<u16> {
502        self.vnc_port
503    }
504}