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