nautilus_interactive_brokers/
factories.rs1use 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#[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 #[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#[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 #[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}