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        instrument_provider.seed_from_cache(&cache.borrow());
105
106        let client = InteractiveBrokersDataClient::new(
107            ClientId::from(name),
108            ib_config,
109            instrument_provider,
110        )?;
111        Ok(Box::new(client))
112    }
113
114    fn name(&self) -> &'static str {
115        IB
116    }
117
118    fn config_type(&self) -> &'static str {
119        stringify!(InteractiveBrokersDataClientConfig)
120    }
121}
122
123/// Factory for creating Interactive Brokers execution clients.
124#[derive(Debug, Default, Clone)]
125#[cfg_attr(
126    feature = "python",
127    pyo3::pyclass(
128        module = "nautilus_trader.adapters.interactive_brokers",
129        from_py_object
130    )
131)]
132#[cfg_attr(
133    feature = "python",
134    pyo3_stub_gen::derive::gen_stub_pyclass(
135        module = "nautilus_trader.adapters.interactive_brokers"
136    )
137)]
138pub struct InteractiveBrokersExecutionClientFactory;
139
140impl InteractiveBrokersExecutionClientFactory {
141    /// Creates a new [`InteractiveBrokersExecutionClientFactory`] instance.
142    #[must_use]
143    pub const fn new() -> Self {
144        Self
145    }
146}
147
148impl ExecutionClientFactory for InteractiveBrokersExecutionClientFactory {
149    fn create(
150        &self,
151        trader_id: TraderId,
152        name: &str,
153        config: &dyn ClientConfig,
154        cache: CacheView,
155        _clock: Rc<RefCell<dyn Clock>>,
156    ) -> anyhow::Result<Box<dyn ExecutionClient>> {
157        let ib_config = config
158            .as_any()
159            .downcast_ref::<InteractiveBrokersExecutionClientConfig>()
160            .ok_or_else(|| {
161                anyhow::anyhow!(
162                    "Invalid config type for InteractiveBrokersExecutionClientFactory. Expected InteractiveBrokersExecutionClientConfig, was {config:?}",
163                )
164            })?
165            .clone();
166
167        let account_id = if let Some(account_id) = ib_config.account_id.as_deref() {
168            resolve_account_id(name, account_id)?
169        } else {
170            AccountId::from("IB-001")
171        };
172
173        let instrument_provider = Arc::new(InteractiveBrokersInstrumentProvider::new(
174            ib_config.instrument_provider.clone(),
175        )?);
176        instrument_provider.seed_from_cache(&cache.borrow());
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            ib_config.base_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
213#[cfg(test)]
214mod tests {
215    use std::{cell::RefCell, rc::Rc};
216
217    use nautilus_common::{
218        cache::Cache,
219        clock::VirtualClock,
220        factories::{ClientConfig, DataClientFactory, ExecutionClientFactory},
221        live::runner::{replace_data_event_sender, replace_exec_event_sender},
222        messages::ExecutionEvent,
223    };
224    use nautilus_core::UnixNanos;
225    use nautilus_model::types::{AccountBalance, Currency};
226    use rstest::rstest;
227    use rust_decimal::Decimal;
228
229    use super::*;
230
231    #[rstest]
232    fn test_interactive_brokers_data_client_factory_creation() {
233        let factory = InteractiveBrokersDataClientFactory::new();
234        assert_eq!(factory.name(), IB);
235        assert_eq!(factory.config_type(), "InteractiveBrokersDataClientConfig");
236    }
237
238    #[rstest]
239    fn test_interactive_brokers_data_client_factory_default() {
240        let factory = InteractiveBrokersDataClientFactory;
241        assert_eq!(factory.name(), IB);
242    }
243
244    #[rstest]
245    fn test_interactive_brokers_exec_client_factory_creation() {
246        let factory = InteractiveBrokersExecutionClientFactory::new();
247        assert_eq!(factory.name(), IB);
248        assert_eq!(
249            factory.config_type(),
250            "InteractiveBrokersExecutionClientConfig"
251        );
252    }
253
254    #[rstest]
255    fn test_interactive_brokers_configs_implement_client_config() {
256        let data_config = InteractiveBrokersDataClientConfig::default();
257        let exec_config = InteractiveBrokersExecutionClientConfig::default();
258
259        let boxed_data_config: Box<dyn ClientConfig> = Box::new(data_config);
260        let boxed_exec_config: Box<dyn ClientConfig> = Box::new(exec_config);
261
262        assert!(
263            boxed_data_config
264                .as_any()
265                .downcast_ref::<InteractiveBrokersDataClientConfig>()
266                .is_some()
267        );
268        assert!(
269            boxed_exec_config
270                .as_any()
271                .downcast_ref::<InteractiveBrokersExecutionClientConfig>()
272                .is_some()
273        );
274    }
275
276    #[rstest]
277    fn test_interactive_brokers_data_client_factory_creates_client() {
278        let factory = InteractiveBrokersDataClientFactory::new();
279        let config = InteractiveBrokersDataClientConfig::default();
280        let cache = Rc::new(RefCell::new(Cache::default()));
281        let clock = Rc::new(RefCell::new(VirtualClock::new()));
282        let (data_tx, _data_rx) = tokio::sync::mpsc::unbounded_channel();
283        replace_data_event_sender(data_tx);
284
285        let result = factory.create("IB-TEST", &config, cache.into(), clock);
286
287        assert!(result.is_ok());
288        let client = result.unwrap();
289        assert_eq!(client.client_id(), ClientId::from("IB-TEST"));
290    }
291
292    #[rstest]
293    fn test_interactive_brokers_exec_client_factory_creates_client() {
294        let factory = InteractiveBrokersExecutionClientFactory::new();
295        let config = InteractiveBrokersExecutionClientConfig::default();
296        let cache = Rc::new(RefCell::new(Cache::default()));
297
298        let result = factory.create(
299            TraderId::from("TRADER-001"),
300            "IB-TEST",
301            &config,
302            cache.into(),
303            Rc::new(RefCell::new(VirtualClock::new())),
304        );
305
306        assert!(result.is_ok());
307        let client = result.unwrap();
308        assert_eq!(client.client_id(), ClientId::from("IB-TEST"));
309        assert_eq!(client.account_id(), AccountId::from("IB-001"));
310    }
311
312    #[rstest]
313    fn test_interactive_brokers_exec_client_factory_uses_config_account_id() {
314        let factory = InteractiveBrokersExecutionClientFactory::new();
315
316        let config = InteractiveBrokersExecutionClientConfig {
317            account_id: Some(String::from("U7654321")),
318            ..Default::default()
319        };
320
321        let cache = Rc::new(RefCell::new(Cache::default()));
322
323        let result = factory.create(
324            TraderId::from("TRADER-001"),
325            "IB-CUSTOM",
326            &config,
327            cache.into(),
328            Rc::new(RefCell::new(VirtualClock::new())),
329        );
330
331        assert!(result.is_ok());
332        let client = result.unwrap();
333        assert_eq!(client.account_id(), AccountId::from("IB-CUSTOM-U7654321"));
334    }
335
336    #[rstest]
337    #[case(Some(Currency::USD()))]
338    #[case(Some(Currency::EUR()))]
339    #[case(None)]
340    fn test_interactive_brokers_exec_client_factory_passes_base_currency_to_account(
341        #[case] base_currency: Option<Currency>,
342    ) {
343        let factory = InteractiveBrokersExecutionClientFactory::new();
344        let config = InteractiveBrokersExecutionClientConfig {
345            base_currency,
346            ..Default::default()
347        };
348        let cache = Rc::new(RefCell::new(Cache::default()));
349        let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
350        replace_exec_event_sender(tx);
351
352        let client = factory
353            .create(
354                TraderId::from("TRADER-001"),
355                "IB-TEST",
356                &config,
357                cache.into(),
358                Rc::new(RefCell::new(VirtualClock::new())),
359            )
360            .unwrap();
361        let balances = vec![
362            AccountBalance::from_total_and_free(
363                Decimal::new(100050, 2),
364                Decimal::new(90025, 2),
365                Currency::USD(),
366            )
367            .unwrap(),
368        ];
369
370        client
371            .generate_account_state(balances, Vec::new(), true, UnixNanos::default(), None)
372            .unwrap();
373
374        match rx.try_recv().unwrap() {
375            ExecutionEvent::Account(state) => assert_eq!(state.base_currency, base_currency),
376            other => panic!("expected account state, was {other:?}"),
377        }
378    }
379}