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        _clock: Rc<RefCell<dyn Clock>>,
155    ) -> anyhow::Result<Box<dyn ExecutionClient>> {
156        let mut ib_config = config
157            .as_any()
158            .downcast_ref::<InteractiveBrokersExecutionClientConfig>()
159            .ok_or_else(|| {
160                anyhow::anyhow!(
161                    "Invalid config type for InteractiveBrokersExecutionClientFactory. Expected InteractiveBrokersExecutionClientConfig, was {config:?}",
162                )
163            })?
164            .clone();
165
166        let account_id = if let Some(account_id) = ib_config.account_id.as_deref() {
167            resolve_account_id(name, account_id)?
168        } else {
169            AccountId::from("IB-001")
170        };
171        ib_config.account_id = Some(account_id.to_string());
172
173        let instrument_provider = Arc::new(InteractiveBrokersInstrumentProvider::new(
174            ib_config.instrument_provider.clone(),
175        ));
176        seed_provider_from_cache(&instrument_provider, &cache);
177
178        let core = ExecutionClientCore::new(
179            trader_id,
180            ClientId::from(name),
181            *IB_VENUE,
182            OmsType::Netting,
183            account_id,
184            AccountType::Margin,
185            None, // base_currency: IB accounts can be multi-currency
186            cache,
187        );
188
189        let client = InteractiveBrokersExecutionClient::new(core, ib_config, instrument_provider)?;
190        Ok(Box::new(client))
191    }
192
193    fn name(&self) -> &'static str {
194        IB
195    }
196
197    fn config_type(&self) -> &'static str {
198        stringify!(InteractiveBrokersExecutionClientConfig)
199    }
200}
201
202fn resolve_account_id(name: &str, account_id: &str) -> anyhow::Result<AccountId> {
203    if account_id.contains('-') {
204        return AccountId::new_checked(account_id)
205            .map_err(|e| anyhow::anyhow!("Invalid Interactive Brokers account_id: {e}"));
206    }
207
208    let issuer = if name.is_empty() { IB } else { name };
209    AccountId::new_checked(format!("{issuer}-{account_id}"))
210        .map_err(|e| anyhow::anyhow!("Invalid Interactive Brokers account_id: {e}"))
211}
212
213fn seed_provider_from_cache(
214    instrument_provider: &InteractiveBrokersInstrumentProvider,
215    cache: &CacheView,
216) {
217    let instruments = {
218        let cache = cache.borrow();
219        cache
220            .instrument_ids(None)
221            .into_iter()
222            .filter_map(|instrument_id| cache.instrument(instrument_id).cloned())
223            .collect::<Vec<_>>()
224    };
225
226    let count = instrument_provider.add_cached_instruments(instruments);
227    if count > 0 {
228        tracing::debug!(
229            "Seeded Interactive Brokers instrument provider with {} cached instruments",
230            count
231        );
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use std::{cell::RefCell, rc::Rc};
238
239    use nautilus_common::{
240        cache::Cache,
241        clock::VirtualClock,
242        factories::{ClientConfig, DataClientFactory, ExecutionClientFactory},
243        live::runner::replace_data_event_sender,
244    };
245    use rstest::rstest;
246
247    use super::*;
248
249    #[rstest]
250    fn test_interactive_brokers_data_client_factory_creation() {
251        let factory = InteractiveBrokersDataClientFactory::new();
252        assert_eq!(factory.name(), IB);
253        assert_eq!(factory.config_type(), "InteractiveBrokersDataClientConfig");
254    }
255
256    #[rstest]
257    fn test_interactive_brokers_data_client_factory_default() {
258        let factory = InteractiveBrokersDataClientFactory;
259        assert_eq!(factory.name(), IB);
260    }
261
262    #[rstest]
263    fn test_interactive_brokers_exec_client_factory_creation() {
264        let factory = InteractiveBrokersExecutionClientFactory::new();
265        assert_eq!(factory.name(), IB);
266        assert_eq!(
267            factory.config_type(),
268            "InteractiveBrokersExecutionClientConfig"
269        );
270    }
271
272    #[rstest]
273    fn test_interactive_brokers_configs_implement_client_config() {
274        let data_config = InteractiveBrokersDataClientConfig::default();
275        let exec_config = InteractiveBrokersExecutionClientConfig::default();
276
277        let boxed_data_config: Box<dyn ClientConfig> = Box::new(data_config);
278        let boxed_exec_config: Box<dyn ClientConfig> = Box::new(exec_config);
279
280        assert!(
281            boxed_data_config
282                .as_any()
283                .downcast_ref::<InteractiveBrokersDataClientConfig>()
284                .is_some()
285        );
286        assert!(
287            boxed_exec_config
288                .as_any()
289                .downcast_ref::<InteractiveBrokersExecutionClientConfig>()
290                .is_some()
291        );
292    }
293
294    #[rstest]
295    fn test_interactive_brokers_data_client_factory_creates_client() {
296        let factory = InteractiveBrokersDataClientFactory::new();
297        let config = InteractiveBrokersDataClientConfig::default();
298        let cache = Rc::new(RefCell::new(Cache::default()));
299        let clock = Rc::new(RefCell::new(VirtualClock::new()));
300        let (data_tx, _data_rx) = tokio::sync::mpsc::unbounded_channel();
301        replace_data_event_sender(data_tx);
302
303        let result = factory.create("IB-TEST", &config, cache.into(), clock);
304
305        assert!(result.is_ok());
306        let client = result.unwrap();
307        assert_eq!(client.client_id(), ClientId::from("IB-TEST"));
308    }
309
310    #[rstest]
311    fn test_interactive_brokers_exec_client_factory_creates_client() {
312        let factory = InteractiveBrokersExecutionClientFactory::new();
313        let config = InteractiveBrokersExecutionClientConfig::default();
314        let cache = Rc::new(RefCell::new(Cache::default()));
315
316        let result = factory.create(
317            TraderId::from("TRADER-001"),
318            "IB-TEST",
319            &config,
320            cache.into(),
321            Rc::new(RefCell::new(VirtualClock::new())),
322        );
323
324        assert!(result.is_ok());
325        let client = result.unwrap();
326        assert_eq!(client.client_id(), ClientId::from("IB-TEST"));
327        assert_eq!(client.account_id(), AccountId::from("IB-001"));
328    }
329
330    #[rstest]
331    fn test_interactive_brokers_exec_client_factory_uses_config_account_id() {
332        let factory = InteractiveBrokersExecutionClientFactory::new();
333        let config = InteractiveBrokersExecutionClientConfig {
334            account_id: Some(String::from("U7654321")),
335            ..Default::default()
336        };
337        let cache = Rc::new(RefCell::new(Cache::default()));
338
339        let result = factory.create(
340            TraderId::from("TRADER-001"),
341            "IB-CUSTOM",
342            &config,
343            cache.into(),
344            Rc::new(RefCell::new(VirtualClock::new())),
345        );
346
347        assert!(result.is_ok());
348        let client = result.unwrap();
349        assert_eq!(client.account_id(), AccountId::from("IB-CUSTOM-U7654321"));
350    }
351}