Skip to main content

nautilus_interactive_brokers/
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//! Configuration types for the Interactive Brokers adapter.
17
18use std::{
19    collections::{HashMap, HashSet},
20    fmt::Debug,
21};
22
23use nautilus_core::string::secret::SecretString;
24use nautilus_model::identifiers::InstrumentId;
25use serde::{Deserialize, Serialize};
26
27use crate::common::consts::{DEFAULT_CLIENT_ID, DEFAULT_HOST, DEFAULT_PORT};
28
29/// Market data type for switching between real-time and frozen/delayed.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
31#[cfg_attr(
32    feature = "python",
33    pyo3::pyclass(
34        module = "nautilus_trader.adapters.interactive_brokers",
35        from_py_object,
36        rename_all = "SCREAMING_SNAKE_CASE"
37    )
38)]
39#[cfg_attr(
40    feature = "python",
41    pyo3_stub_gen::derive::gen_stub_pyclass_enum(
42        module = "nautilus_trader.adapters.interactive_brokers"
43    )
44)]
45#[derive(Default)]
46pub enum MarketDataType {
47    /// Live market data
48    #[default]
49    Realtime = 1,
50    /// Frozen market data (for when market is closed)
51    Frozen = 2,
52    /// Delayed market data (usually 15-20 minutes)
53    Delayed = 3,
54    /// Delayed frozen market data
55    DelayedFrozen = 4,
56}
57
58impl From<MarketDataType> for ibapi::market_data::MarketDataType {
59    fn from(data_type: MarketDataType) -> Self {
60        match data_type {
61            MarketDataType::Realtime => Self::Realtime,
62            MarketDataType::Frozen => Self::Frozen,
63            MarketDataType::Delayed => Self::Delayed,
64            MarketDataType::DelayedFrozen => Self::DelayedFrozen,
65        }
66    }
67}
68
69/// Configuration for Interactive Brokers data client.
70#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
71#[serde(default)]
72#[cfg_attr(
73    feature = "python",
74    pyo3::pyclass(
75        module = "nautilus_trader.adapters.interactive_brokers",
76        subclass,
77        from_py_object
78    )
79)]
80#[cfg_attr(
81    feature = "python",
82    pyo3_stub_gen::derive::gen_stub_pyclass(
83        module = "nautilus_trader.adapters.interactive_brokers"
84    )
85)]
86pub struct InteractiveBrokersDataClientConfig {
87    /// Host for IB Gateway/TWS.
88    #[builder(default = DEFAULT_HOST.to_string())]
89    pub host: String,
90    /// Port for IB Gateway/TWS.
91    #[builder(default = DEFAULT_PORT)]
92    pub port: u16,
93    /// Client ID.
94    #[builder(default = DEFAULT_CLIENT_ID)]
95    pub client_id: i32,
96    /// Whether to use regular trading hours only (RTH filtering).
97    #[builder(default = true)]
98    pub use_regular_trading_hours: bool,
99    /// Market data type (realtime, delayed, frozen).
100    #[builder(default)]
101    pub market_data_type: MarketDataType,
102    /// Whether to ignore quote tick size updates (filters size-only updates).
103    #[builder(default)]
104    pub ignore_quote_tick_size_updates: bool,
105    /// Connection timeout in seconds.
106    #[builder(default = 300)]
107    pub connection_timeout: u64,
108    /// Request timeout in seconds. Applied to IB API requests (open orders, executions, positions,
109    /// account summary, order update stream, next order id). See execution/core.rs and
110    /// execution/account.rs for call sites.
111    #[builder(default = 60)]
112    pub request_timeout: u64,
113    /// Whether to handle revised bars.
114    #[builder(default)]
115    pub handle_revised_bars: bool,
116    /// Whether to use batch quotes (reqMktData) by default instead of tick-by-tick.
117    #[builder(default = true)]
118    pub batch_quotes: bool,
119    /// Instrument provider configuration.
120    #[builder(default)]
121    pub instrument_provider: InteractiveBrokersInstrumentProviderConfig,
122}
123
124impl Default for InteractiveBrokersDataClientConfig {
125    fn default() -> Self {
126        Self::builder().build()
127    }
128}
129
130/// Configuration for Interactive Brokers execution client.
131#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
132#[serde(default)]
133#[cfg_attr(
134    feature = "python",
135    pyo3::pyclass(
136        module = "nautilus_trader.adapters.interactive_brokers",
137        subclass,
138        from_py_object
139    )
140)]
141#[cfg_attr(
142    feature = "python",
143    pyo3_stub_gen::derive::gen_stub_pyclass(
144        module = "nautilus_trader.adapters.interactive_brokers"
145    )
146)]
147pub struct InteractiveBrokersExecutionClientConfig {
148    /// Host for IB Gateway/TWS.
149    #[builder(default = DEFAULT_HOST.to_string())]
150    pub host: String,
151    /// Port for IB Gateway/TWS.
152    #[builder(default = DEFAULT_PORT)]
153    pub port: u16,
154    /// Client ID.
155    #[builder(default = DEFAULT_CLIENT_ID)]
156    pub client_id: i32,
157    /// Account ID.
158    pub account_id: Option<String>,
159    /// Connection timeout in seconds.
160    #[builder(default = 300)]
161    pub connection_timeout: u64,
162    /// Request timeout in seconds for IB API requests (open orders, executions, positions, etc.).
163    #[builder(default = 60)]
164    pub request_timeout: u64,
165    /// Whether to fetch all open orders (reqAllOpenOrders vs reqOpenOrders).
166    #[builder(default)]
167    pub fetch_all_open_orders: bool,
168    /// Whether to track option exercise from position updates.
169    #[builder(default)]
170    pub track_option_exercise_from_position_update: bool,
171    /// Instrument provider configuration.
172    #[builder(default)]
173    pub instrument_provider: InteractiveBrokersInstrumentProviderConfig,
174}
175
176impl Default for InteractiveBrokersExecutionClientConfig {
177    fn default() -> Self {
178        Self::builder().build()
179    }
180}
181
182/// Symbology method for converting between IB contracts and Nautilus instrument IDs.
183#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
184#[cfg_attr(
185    feature = "python",
186    pyo3::pyclass(
187        module = "nautilus_trader.adapters.interactive_brokers",
188        from_py_object,
189        rename_all = "SCREAMING_SNAKE_CASE"
190    )
191)]
192#[cfg_attr(
193    feature = "python",
194    pyo3_stub_gen::derive::gen_stub_pyclass_enum(
195        module = "nautilus_trader.adapters.interactive_brokers"
196    )
197)]
198#[derive(Default)]
199pub enum SymbologyMethod {
200    /// Simplified symbology: clean, readable symbols (e.g., "EUR/USD", "ESM23")
201    #[serde(rename = "simplified")]
202    #[default]
203    Simplified,
204    /// Raw symbology: preserves IB raw format with security type suffix (e.g., "EUR.USD=CASH", "AAPL=STK")
205    #[serde(rename = "raw")]
206    Raw,
207}
208
209/// Configuration for Interactive Brokers instrument provider.
210#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
211#[serde(default)]
212#[cfg_attr(
213    feature = "python",
214    pyo3::pyclass(
215        module = "nautilus_trader.adapters.interactive_brokers",
216        subclass,
217        from_py_object
218    )
219)]
220#[cfg_attr(
221    feature = "python",
222    pyo3_stub_gen::derive::gen_stub_pyclass(
223        module = "nautilus_trader.adapters.interactive_brokers"
224    )
225)]
226pub struct InteractiveBrokersInstrumentProviderConfig {
227    /// Symbology method to use for instrument ID conversion.
228    #[builder(default)]
229    pub symbology_method: SymbologyMethod,
230    /// Instrument IDs to load on startup.
231    #[builder(default)]
232    pub load_ids: HashSet<InstrumentId>,
233    /// IB contracts to load on startup.
234    #[builder(default)]
235    pub load_contracts: Vec<serde_json::Value>,
236    /// Minimum expiry days for options and futures chains.
237    pub min_expiry_days: Option<u32>,
238    /// Maximum expiry days for options and futures chains.
239    pub max_expiry_days: Option<u32>,
240    /// Whether to build full options chain.
241    pub build_options_chain: Option<bool>,
242    /// Whether to build full futures chain.
243    pub build_futures_chain: Option<bool>,
244    /// Cache validity in days (None means no caching).
245    pub cache_validity_days: Option<u32>,
246    /// Whether to convert IB exchanges to MIC venues.
247    #[builder(default)]
248    pub convert_exchange_to_mic_venue: bool,
249    /// Symbol to MIC venue mapping override.
250    #[builder(default)]
251    pub symbol_to_mic_venue: HashMap<String, String>,
252    /// Security types to filter out.
253    #[builder(default)]
254    pub filter_sec_types: HashSet<String>,
255    /// Fully-qualified Python callable path for custom instrument filtering.
256    ///
257    /// Configuring this without the Python feature enabled is an error.
258    pub filter_callable: Option<String>,
259    /// Path to cache file for persistent instrument caching (equivalent to pickle_path in Python).
260    /// If provided, instruments will be cached to disk and loaded from cache if still valid.
261    pub cache_path: Option<String>,
262}
263
264impl Default for InteractiveBrokersInstrumentProviderConfig {
265    fn default() -> Self {
266        Self::builder().build()
267    }
268}
269
270/// Trading mode for Dockerized IB Gateway.
271#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
272#[cfg_attr(
273    feature = "python",
274    pyo3::pyclass(
275        module = "nautilus_trader.adapters.interactive_brokers",
276        from_py_object,
277        rename_all = "SCREAMING_SNAKE_CASE"
278    )
279)]
280#[cfg_attr(
281    feature = "python",
282    pyo3_stub_gen::derive::gen_stub_pyclass_enum(
283        module = "nautilus_trader.adapters.interactive_brokers"
284    )
285)]
286#[derive(Default)]
287pub enum TradingMode {
288    /// Paper trading mode.
289    #[serde(rename = "paper")]
290    #[default]
291    Paper,
292    /// Live trading mode.
293    #[serde(rename = "live")]
294    Live,
295}
296
297/// Configuration for Dockerized IB Gateway.
298///
299/// This configuration is for managing containerized IB Gateway instances.
300/// It supports environment variable loading and sensitive data masking.
301#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
302#[serde(default)]
303#[cfg_attr(
304    feature = "python",
305    pyo3::pyclass(
306        module = "nautilus_trader.adapters.interactive_brokers",
307        subclass,
308        from_py_object
309    )
310)]
311#[cfg_attr(
312    feature = "python",
313    pyo3_stub_gen::derive::gen_stub_pyclass(
314        module = "nautilus_trader.adapters.interactive_brokers"
315    )
316)]
317pub struct DockerizedIBGatewayConfig {
318    /// Username for IB account (falls back to `TWS_USERNAME` env var via [`Default`]).
319    pub username: Option<SecretString>,
320    /// Password for IB account (falls back to `TWS_PASSWORD` env var via [`Default`]).
321    pub password: Option<SecretString>,
322    /// Trading mode (paper or live).
323    #[builder(default)]
324    pub trading_mode: TradingMode,
325    /// Whether to enable read-only API mode.
326    #[builder(default = true)]
327    pub read_only_api: bool,
328    /// Timeout in seconds for container startup.
329    #[builder(default = 300)]
330    pub timeout: u64,
331    /// Container image reference.
332    #[builder(default = "ghcr.io/gnzsnz/ib-gateway:stable".to_string())]
333    pub container_image: String,
334    /// VNC port for remote desktop access (None to disable).
335    pub vnc_port: Option<u16>,
336}
337
338impl DockerizedIBGatewayConfig {
339    /// Mask sensitive information for display.
340    pub fn mask_sensitive_info(value: &str) -> String {
341        if value.len() <= 2 {
342            "*".repeat(value.len())
343        } else {
344            format!(
345                "{}{}{}",
346                &value[0..1],
347                "*".repeat(value.len() - 2),
348                &value[value.len() - 1..]
349            )
350        }
351    }
352
353    /// Validate configuration.
354    ///
355    /// # Errors
356    ///
357    /// Returns an error if validation fails.
358    pub fn validate(&self) -> anyhow::Result<()> {
359        if self.timeout == 0 {
360            anyhow::bail!("Timeout must be greater than 0");
361        }
362
363        if self.timeout > 3600 {
364            anyhow::bail!("Timeout must be less than 3600 seconds");
365        }
366
367        if let Some(port) = self.vnc_port
368            && (!(5900..=5999).contains(&port))
369        {
370            anyhow::bail!("VNC port must be between 5900 and 5999");
371        }
372
373        Ok(())
374    }
375}
376
377impl Default for DockerizedIBGatewayConfig {
378    fn default() -> Self {
379        Self::builder()
380            .maybe_username(std::env::var("TWS_USERNAME").ok().map(SecretString::from))
381            .maybe_password(std::env::var("TWS_PASSWORD").ok().map(SecretString::from))
382            .build()
383    }
384}
385
386#[cfg(test)]
387mod tests {
388    use rstest::rstest;
389
390    use super::*;
391
392    #[rstest]
393    fn dockerized_gateway_config_debug_redacts_credentials() {
394        let config = DockerizedIBGatewayConfig::builder()
395            .username("test-user".into())
396            .password("test-password".into())
397            .build();
398
399        let formatted = format!("{config:?}");
400
401        assert!(formatted.contains("username: Some(<redacted>)"));
402        assert!(formatted.contains("password: Some(<redacted>)"));
403        assert!(!formatted.contains("test-user"));
404        assert!(!formatted.contains("test-password"));
405    }
406}