Skip to main content

nautilus_interactive_brokers/
factories.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//! Factory functions for creating Interactive Brokers clients and components.
17
18use std::{any::Any, cell::RefCell, rc::Rc, sync::Arc};
19
20use nautilus_common::{
21    cache::CacheView,
22    clients::{DataClient, ExecutionClient},
23    clock::Clock,
24    factories::{ClientConfig, DataClientFactory, ExecutionClientFactory},
25};
26use nautilus_live::ExecutionClientCore;
27use nautilus_model::{
28    enums::{AccountType, OmsType},
29    identifiers::{AccountId, ClientId, TraderId},
30};
31
32use crate::{
33    common::consts::{IB, IB_VENUE},
34    config::{InteractiveBrokersDataClientConfig, InteractiveBrokersExecutionClientConfig},
35    data::InteractiveBrokersDataClient,
36    execution::InteractiveBrokersExecutionClient,
37    providers::instruments::InteractiveBrokersInstrumentProvider,
38};
39
40impl ClientConfig for InteractiveBrokersDataClientConfig {
41    fn as_any(&self) -> &dyn Any {
42        self
43    }
44}
45
46impl ClientConfig for InteractiveBrokersExecutionClientConfig {
47    fn as_any(&self) -> &dyn Any {
48        self
49    }
50}
51
52/// Factory for creating Interactive Brokers data clients.
53#[derive(Debug, Clone)]
54#[cfg_attr(
55    feature = "python",
56    pyo3::pyclass(
57        module = "nautilus_trader.adapters.interactive_brokers",
58        from_py_object
59    )
60)]
61#[cfg_attr(
62    feature = "python",
63    pyo3_stub_gen::derive::gen_stub_pyclass(
64        module = "nautilus_trader.adapters.interactive_brokers"
65    )
66)]
67pub struct InteractiveBrokersDataClientFactory;
68
69impl InteractiveBrokersDataClientFactory {
70    /// Creates a new [`InteractiveBrokersDataClientFactory`] instance.
71    #[must_use]
72    pub const fn new() -> Self {
73        Self
74    }
75}
76
77impl Default for InteractiveBrokersDataClientFactory {
78    fn default() -> Self {
79        Self::new()
80    }
81}
82
83impl DataClientFactory for InteractiveBrokersDataClientFactory {
84    fn create(
85        &self,
86        name: &str,
87        config: &dyn ClientConfig,
88        cache: CacheView,
89        _clock: Rc<RefCell<dyn Clock>>,
90    ) -> anyhow::Result<Box<dyn DataClient>> {
91        let ib_config = config
92            .as_any()
93            .downcast_ref::<InteractiveBrokersDataClientConfig>()
94            .ok_or_else(|| {
95                anyhow::anyhow!(
96                    "Invalid config type for InteractiveBrokersDataClientFactory. Expected InteractiveBrokersDataClientConfig, was {config:?}",
97                )
98            })?
99            .clone();
100
101        let instrument_provider = Arc::new(InteractiveBrokersInstrumentProvider::new(
102            ib_config.instrument_provider.clone(),
103        ));
104        seed_provider_from_cache(&instrument_provider, &cache);
105        let client = InteractiveBrokersDataClient::new(
106            ClientId::from(name),
107            ib_config,
108            instrument_provider,
109        )?;
110        Ok(Box::new(client))
111    }
112
113    fn name(&self) -> &'static str {
114        IB
115    }
116
117    fn config_type(&self) -> &'static str {
118        stringify!(InteractiveBrokersDataClientConfig)
119    }
120}
121
122/// Factory for creating Interactive Brokers execution clients.
123#[derive(Debug, Default, Clone)]
124#[cfg_attr(
125    feature = "python",
126    pyo3::pyclass(
127        module = "nautilus_trader.adapters.interactive_brokers",
128        from_py_object
129    )
130)]
131#[cfg_attr(
132    feature = "python",
133    pyo3_stub_gen::derive::gen_stub_pyclass(
134        module = "nautilus_trader.adapters.interactive_brokers"
135    )
136)]
137pub struct InteractiveBrokersExecutionClientFactory;
138
139impl InteractiveBrokersExecutionClientFactory {
140    /// Creates a new [`InteractiveBrokersExecutionClientFactory`] instance.
141    #[must_use]
142    pub const fn new() -> Self {
143        Self
144    }
145}
146
147impl ExecutionClientFactory for InteractiveBrokersExecutionClientFactory {
148    fn create(
149        &self,
150        trader_id: TraderId,
151        name: &str,
152        config: &dyn ClientConfig,
153        cache: CacheView,
154    ) -> anyhow::Result<Box<dyn ExecutionClient>> {
155        let mut ib_config = config
156            .as_any()
157            .downcast_ref::<InteractiveBrokersExecutionClientConfig>()
158            .ok_or_else(|| {
159                anyhow::anyhow!(
160                    "Invalid config type for InteractiveBrokersExecutionClientFactory. Expected InteractiveBrokersExecutionClientConfig, was {config:?}",
161                )
162            })?
163            .clone();
164
165        let account_id = if let Some(account_id) = ib_config.account_id.as_deref() {
166            resolve_account_id(name, account_id)?
167        } else {
168            AccountId::from("IB-001")
169        };
170        ib_config.account_id = Some(account_id.to_string());
171
172        let instrument_provider = Arc::new(InteractiveBrokersInstrumentProvider::new(
173            ib_config.instrument_provider.clone(),
174        ));
175        seed_provider_from_cache(&instrument_provider, &cache);
176
177        let core = ExecutionClientCore::new(
178            trader_id,
179            ClientId::from(name),
180            *IB_VENUE,
181            OmsType::Netting,
182            account_id,
183            AccountType::Margin,
184            None, // base_currency: IB accounts can be multi-currency
185            cache,
186        );
187
188        let client = InteractiveBrokersExecutionClient::new(core, ib_config, instrument_provider)?;
189        Ok(Box::new(client))
190    }
191
192    fn name(&self) -> &'static str {
193        IB
194    }
195
196    fn config_type(&self) -> &'static str {
197        stringify!(InteractiveBrokersExecutionClientConfig)
198    }
199}
200
201fn resolve_account_id(name: &str, account_id: &str) -> anyhow::Result<AccountId> {
202    if account_id.contains('-') {
203        return AccountId::new_checked(account_id)
204            .map_err(|e| anyhow::anyhow!("Invalid Interactive Brokers account_id: {e}"));
205    }
206
207    let issuer = if name.is_empty() { IB } else { name };
208    AccountId::new_checked(format!("{issuer}-{account_id}"))
209        .map_err(|e| anyhow::anyhow!("Invalid Interactive Brokers account_id: {e}"))
210}
211
212fn seed_provider_from_cache(
213    instrument_provider: &InteractiveBrokersInstrumentProvider,
214    cache: &CacheView,
215) {
216    let instruments = {
217        let cache = cache.borrow();
218        cache
219            .instrument_ids(None)
220            .into_iter()
221            .filter_map(|instrument_id| cache.instrument(instrument_id).cloned())
222            .collect::<Vec<_>>()
223    };
224
225    let count = instrument_provider.add_cached_instruments(instruments);
226    if count > 0 {
227        tracing::debug!(
228            "Seeded Interactive Brokers instrument provider with {} cached instruments",
229            count
230        );
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use std::{cell::RefCell, rc::Rc};
237
238    use nautilus_common::{
239        cache::Cache,
240        clock::TestClock,
241        factories::{ClientConfig, DataClientFactory, ExecutionClientFactory},
242        live::runner::replace_data_event_sender,
243    };
244    use rstest::rstest;
245
246    use super::*;
247
248    #[rstest]
249    fn test_interactive_brokers_data_client_factory_creation() {
250        let factory = InteractiveBrokersDataClientFactory::new();
251        assert_eq!(factory.name(), IB);
252        assert_eq!(factory.config_type(), "InteractiveBrokersDataClientConfig");
253    }
254
255    #[rstest]
256    fn test_interactive_brokers_data_client_factory_default() {
257        let factory = InteractiveBrokersDataClientFactory;
258        assert_eq!(factory.name(), IB);
259    }
260
261    #[rstest]
262    fn test_interactive_brokers_exec_client_factory_creation() {
263        let factory = InteractiveBrokersExecutionClientFactory::new();
264        assert_eq!(factory.name(), IB);
265        assert_eq!(
266            factory.config_type(),
267            "InteractiveBrokersExecutionClientConfig"
268        );
269    }
270
271    #[rstest]
272    fn test_interactive_brokers_configs_implement_client_config() {
273        let data_config = InteractiveBrokersDataClientConfig::default();
274        let exec_config = InteractiveBrokersExecutionClientConfig::default();
275
276        let boxed_data_config: Box<dyn ClientConfig> = Box::new(data_config);
277        let boxed_exec_config: Box<dyn ClientConfig> = Box::new(exec_config);
278
279        assert!(
280            boxed_data_config
281                .as_any()
282                .downcast_ref::<InteractiveBrokersDataClientConfig>()
283                .is_some()
284        );
285        assert!(
286            boxed_exec_config
287                .as_any()
288                .downcast_ref::<InteractiveBrokersExecutionClientConfig>()
289                .is_some()
290        );
291    }
292
293    #[rstest]
294    fn test_interactive_brokers_data_client_factory_creates_client() {
295        let factory = InteractiveBrokersDataClientFactory::new();
296        let config = InteractiveBrokersDataClientConfig::default();
297        let cache = Rc::new(RefCell::new(Cache::default()));
298        let clock = Rc::new(RefCell::new(TestClock::new()));
299        let (data_tx, _data_rx) = tokio::sync::mpsc::unbounded_channel();
300        replace_data_event_sender(data_tx);
301
302        let result = factory.create("IB-TEST", &config, cache.into(), clock);
303
304        assert!(result.is_ok());
305        let client = result.unwrap();
306        assert_eq!(client.client_id(), ClientId::from("IB-TEST"));
307    }
308
309    #[rstest]
310    fn test_interactive_brokers_exec_client_factory_creates_client() {
311        let factory = InteractiveBrokersExecutionClientFactory::new();
312        let config = InteractiveBrokersExecutionClientConfig::default();
313        let cache = Rc::new(RefCell::new(Cache::default()));
314
315        let result = factory.create(
316            TraderId::from("TRADER-001"),
317            "IB-TEST",
318            &config,
319            cache.into(),
320        );
321
322        assert!(result.is_ok());
323        let client = result.unwrap();
324        assert_eq!(client.client_id(), ClientId::from("IB-TEST"));
325        assert_eq!(client.account_id(), AccountId::from("IB-001"));
326    }
327
328    #[rstest]
329    fn test_interactive_brokers_exec_client_factory_uses_config_account_id() {
330        let factory = InteractiveBrokersExecutionClientFactory::new();
331        let config = InteractiveBrokersExecutionClientConfig {
332            account_id: Some(String::from("U7654321")),
333            ..Default::default()
334        };
335        let cache = Rc::new(RefCell::new(Cache::default()));
336
337        let result = factory.create(
338            TraderId::from("TRADER-001"),
339            "IB-CUSTOM",
340            &config,
341            cache.into(),
342        );
343
344        assert!(result.is_ok());
345        let client = result.unwrap();
346        assert_eq!(client.account_id(), AccountId::from("IB-CUSTOM-U7654321"));
347    }
348}