nautilus_interactive_brokers/
config.rs1use 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#[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 #[default]
49 Realtime = 1,
50 Frozen = 2,
52 Delayed = 3,
54 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#[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 #[builder(default = DEFAULT_HOST.to_string())]
89 pub host: String,
90 #[builder(default = DEFAULT_PORT)]
92 pub port: u16,
93 #[builder(default = DEFAULT_CLIENT_ID)]
95 pub client_id: i32,
96 #[builder(default = true)]
98 pub use_regular_trading_hours: bool,
99 #[builder(default)]
101 pub market_data_type: MarketDataType,
102 #[builder(default)]
104 pub ignore_quote_tick_size_updates: bool,
105 #[builder(default = 300)]
107 pub connection_timeout: u64,
108 #[builder(default = 60)]
112 pub request_timeout: u64,
113 #[builder(default)]
115 pub handle_revised_bars: bool,
116 #[builder(default = true)]
118 pub batch_quotes: bool,
119 #[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#[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 #[builder(default = DEFAULT_HOST.to_string())]
150 pub host: String,
151 #[builder(default = DEFAULT_PORT)]
153 pub port: u16,
154 #[builder(default = DEFAULT_CLIENT_ID)]
156 pub client_id: i32,
157 pub account_id: Option<String>,
159 #[builder(default = 300)]
161 pub connection_timeout: u64,
162 #[builder(default = 60)]
164 pub request_timeout: u64,
165 #[builder(default)]
167 pub fetch_all_open_orders: bool,
168 #[builder(default)]
170 pub track_option_exercise_from_position_update: bool,
171 #[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#[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 #[serde(rename = "simplified")]
202 #[default]
203 Simplified,
204 #[serde(rename = "raw")]
206 Raw,
207}
208
209#[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 #[builder(default)]
229 pub symbology_method: SymbologyMethod,
230 #[builder(default)]
232 pub load_ids: HashSet<InstrumentId>,
233 #[builder(default)]
235 pub load_contracts: Vec<serde_json::Value>,
236 pub min_expiry_days: Option<u32>,
238 pub max_expiry_days: Option<u32>,
240 pub build_options_chain: Option<bool>,
242 pub build_futures_chain: Option<bool>,
244 pub cache_validity_days: Option<u32>,
246 #[builder(default)]
248 pub convert_exchange_to_mic_venue: bool,
249 #[builder(default)]
251 pub symbol_to_mic_venue: HashMap<String, String>,
252 #[builder(default)]
254 pub filter_sec_types: HashSet<String>,
255 pub filter_callable: Option<String>,
259 pub cache_path: Option<String>,
262}
263
264impl Default for InteractiveBrokersInstrumentProviderConfig {
265 fn default() -> Self {
266 Self::builder().build()
267 }
268}
269
270#[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 #[serde(rename = "paper")]
290 #[default]
291 Paper,
292 #[serde(rename = "live")]
294 Live,
295}
296
297#[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 pub username: Option<SecretString>,
320 pub password: Option<SecretString>,
322 #[builder(default)]
324 pub trading_mode: TradingMode,
325 #[builder(default = true)]
327 pub read_only_api: bool,
328 #[builder(default = 300)]
330 pub timeout: u64,
331 #[builder(default = "ghcr.io/gnzsnz/ib-gateway:stable".to_string())]
333 pub container_image: String,
334 pub vnc_port: Option<u16>,
336}
337
338impl DockerizedIBGatewayConfig {
339 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 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}