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