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 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#[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 #[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, 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}