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            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
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,
222    };
223    use rstest::rstest;
224
225    use super::*;
226
227    #[rstest]
228    fn test_interactive_brokers_data_client_factory_creation() {
229        let factory = InteractiveBrokersDataClientFactory::new();
230        assert_eq!(factory.name(), IB);
231        assert_eq!(factory.config_type(), "InteractiveBrokersDataClientConfig");
232    }
233
234    #[rstest]
235    fn test_interactive_brokers_data_client_factory_default() {
236        let factory = InteractiveBrokersDataClientFactory;
237        assert_eq!(factory.name(), IB);
238    }
239
240    #[rstest]
241    fn test_interactive_brokers_exec_client_factory_creation() {
242        let factory = InteractiveBrokersExecutionClientFactory::new();
243        assert_eq!(factory.name(), IB);
244        assert_eq!(
245            factory.config_type(),
246            "InteractiveBrokersExecutionClientConfig"
247        );
248    }
249
250    #[rstest]
251    fn test_interactive_brokers_configs_implement_client_config() {
252        let data_config = InteractiveBrokersDataClientConfig::default();
253        let exec_config = InteractiveBrokersExecutionClientConfig::default();
254
255        let boxed_data_config: Box<dyn ClientConfig> = Box::new(data_config);
256        let boxed_exec_config: Box<dyn ClientConfig> = Box::new(exec_config);
257
258        assert!(
259            boxed_data_config
260                .as_any()
261                .downcast_ref::<InteractiveBrokersDataClientConfig>()
262                .is_some()
263        );
264        assert!(
265            boxed_exec_config
266                .as_any()
267                .downcast_ref::<InteractiveBrokersExecutionClientConfig>()
268                .is_some()
269        );
270    }
271
272    #[rstest]
273    fn test_interactive_brokers_data_client_factory_creates_client() {
274        let factory = InteractiveBrokersDataClientFactory::new();
275        let config = InteractiveBrokersDataClientConfig::default();
276        let cache = Rc::new(RefCell::new(Cache::default()));
277        let clock = Rc::new(RefCell::new(VirtualClock::new()));
278        let (data_tx, _data_rx) = tokio::sync::mpsc::unbounded_channel();
279        replace_data_event_sender(data_tx);
280
281        let result = factory.create("IB-TEST", &config, cache.into(), clock);
282
283        assert!(result.is_ok());
284        let client = result.unwrap();
285        assert_eq!(client.client_id(), ClientId::from("IB-TEST"));
286    }
287
288    #[rstest]
289    fn test_interactive_brokers_exec_client_factory_creates_client() {
290        let factory = InteractiveBrokersExecutionClientFactory::new();
291        let config = InteractiveBrokersExecutionClientConfig::default();
292        let cache = Rc::new(RefCell::new(Cache::default()));
293
294        let result = factory.create(
295            TraderId::from("TRADER-001"),
296            "IB-TEST",
297            &config,
298            cache.into(),
299            Rc::new(RefCell::new(VirtualClock::new())),
300        );
301
302        assert!(result.is_ok());
303        let client = result.unwrap();
304        assert_eq!(client.client_id(), ClientId::from("IB-TEST"));
305        assert_eq!(client.account_id(), AccountId::from("IB-001"));
306    }
307
308    #[rstest]
309    fn test_interactive_brokers_exec_client_factory_uses_config_account_id() {
310        let factory = InteractiveBrokersExecutionClientFactory::new();
311
312        let config = InteractiveBrokersExecutionClientConfig {
313            account_id: Some(String::from("U7654321")),
314            ..Default::default()
315        };
316
317        let cache = Rc::new(RefCell::new(Cache::default()));
318
319        let result = factory.create(
320            TraderId::from("TRADER-001"),
321            "IB-CUSTOM",
322            &config,
323            cache.into(),
324            Rc::new(RefCell::new(VirtualClock::new())),
325        );
326
327        assert!(result.is_ok());
328        let client = result.unwrap();
329        assert_eq!(client.account_id(), AccountId::from("IB-CUSTOM-U7654321"));
330    }
331}