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 ) -> anyhow::Result<Box<dyn ExecutionClient>> {
155 let mut ib_config = config
156 .as_any()
157 .downcast_ref::<InteractiveBrokersExecutionClientConfig>()
158 .ok_or_else(|| {
159 anyhow::anyhow!(
160 "Invalid config type for InteractiveBrokersExecutionClientFactory. Expected InteractiveBrokersExecutionClientConfig, was {config:?}",
161 )
162 })?
163 .clone();
164
165 let account_id = if let Some(account_id) = ib_config.account_id.as_deref() {
166 resolve_account_id(name, account_id)?
167 } else {
168 AccountId::from("IB-001")
169 };
170 ib_config.account_id = Some(account_id.to_string());
171
172 let instrument_provider = Arc::new(InteractiveBrokersInstrumentProvider::new(
173 ib_config.instrument_provider.clone(),
174 ));
175 seed_provider_from_cache(&instrument_provider, &cache);
176
177 let core = ExecutionClientCore::new(
178 trader_id,
179 ClientId::from(name),
180 *IB_VENUE,
181 OmsType::Netting,
182 account_id,
183 AccountType::Margin,
184 None, cache,
186 );
187
188 let client = InteractiveBrokersExecutionClient::new(core, ib_config, instrument_provider)?;
189 Ok(Box::new(client))
190 }
191
192 fn name(&self) -> &'static str {
193 IB
194 }
195
196 fn config_type(&self) -> &'static str {
197 stringify!(InteractiveBrokersExecutionClientConfig)
198 }
199}
200
201fn resolve_account_id(name: &str, account_id: &str) -> anyhow::Result<AccountId> {
202 if account_id.contains('-') {
203 return AccountId::new_checked(account_id)
204 .map_err(|e| anyhow::anyhow!("Invalid Interactive Brokers account_id: {e}"));
205 }
206
207 let issuer = if name.is_empty() { IB } else { name };
208 AccountId::new_checked(format!("{issuer}-{account_id}"))
209 .map_err(|e| anyhow::anyhow!("Invalid Interactive Brokers account_id: {e}"))
210}
211
212fn seed_provider_from_cache(
213 instrument_provider: &InteractiveBrokersInstrumentProvider,
214 cache: &CacheView,
215) {
216 let instruments = {
217 let cache = cache.borrow();
218 cache
219 .instrument_ids(None)
220 .into_iter()
221 .filter_map(|instrument_id| cache.instrument(instrument_id).cloned())
222 .collect::<Vec<_>>()
223 };
224
225 let count = instrument_provider.add_cached_instruments(instruments);
226 if count > 0 {
227 tracing::debug!(
228 "Seeded Interactive Brokers instrument provider with {} cached instruments",
229 count
230 );
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use std::{cell::RefCell, rc::Rc};
237
238 use nautilus_common::{
239 cache::Cache,
240 clock::TestClock,
241 factories::{ClientConfig, DataClientFactory, ExecutionClientFactory},
242 live::runner::replace_data_event_sender,
243 };
244 use rstest::rstest;
245
246 use super::*;
247
248 #[rstest]
249 fn test_interactive_brokers_data_client_factory_creation() {
250 let factory = InteractiveBrokersDataClientFactory::new();
251 assert_eq!(factory.name(), IB);
252 assert_eq!(factory.config_type(), "InteractiveBrokersDataClientConfig");
253 }
254
255 #[rstest]
256 fn test_interactive_brokers_data_client_factory_default() {
257 let factory = InteractiveBrokersDataClientFactory;
258 assert_eq!(factory.name(), IB);
259 }
260
261 #[rstest]
262 fn test_interactive_brokers_exec_client_factory_creation() {
263 let factory = InteractiveBrokersExecutionClientFactory::new();
264 assert_eq!(factory.name(), IB);
265 assert_eq!(
266 factory.config_type(),
267 "InteractiveBrokersExecutionClientConfig"
268 );
269 }
270
271 #[rstest]
272 fn test_interactive_brokers_configs_implement_client_config() {
273 let data_config = InteractiveBrokersDataClientConfig::default();
274 let exec_config = InteractiveBrokersExecutionClientConfig::default();
275
276 let boxed_data_config: Box<dyn ClientConfig> = Box::new(data_config);
277 let boxed_exec_config: Box<dyn ClientConfig> = Box::new(exec_config);
278
279 assert!(
280 boxed_data_config
281 .as_any()
282 .downcast_ref::<InteractiveBrokersDataClientConfig>()
283 .is_some()
284 );
285 assert!(
286 boxed_exec_config
287 .as_any()
288 .downcast_ref::<InteractiveBrokersExecutionClientConfig>()
289 .is_some()
290 );
291 }
292
293 #[rstest]
294 fn test_interactive_brokers_data_client_factory_creates_client() {
295 let factory = InteractiveBrokersDataClientFactory::new();
296 let config = InteractiveBrokersDataClientConfig::default();
297 let cache = Rc::new(RefCell::new(Cache::default()));
298 let clock = Rc::new(RefCell::new(TestClock::new()));
299 let (data_tx, _data_rx) = tokio::sync::mpsc::unbounded_channel();
300 replace_data_event_sender(data_tx);
301
302 let result = factory.create("IB-TEST", &config, cache.into(), clock);
303
304 assert!(result.is_ok());
305 let client = result.unwrap();
306 assert_eq!(client.client_id(), ClientId::from("IB-TEST"));
307 }
308
309 #[rstest]
310 fn test_interactive_brokers_exec_client_factory_creates_client() {
311 let factory = InteractiveBrokersExecutionClientFactory::new();
312 let config = InteractiveBrokersExecutionClientConfig::default();
313 let cache = Rc::new(RefCell::new(Cache::default()));
314
315 let result = factory.create(
316 TraderId::from("TRADER-001"),
317 "IB-TEST",
318 &config,
319 cache.into(),
320 );
321
322 assert!(result.is_ok());
323 let client = result.unwrap();
324 assert_eq!(client.client_id(), ClientId::from("IB-TEST"));
325 assert_eq!(client.account_id(), AccountId::from("IB-001"));
326 }
327
328 #[rstest]
329 fn test_interactive_brokers_exec_client_factory_uses_config_account_id() {
330 let factory = InteractiveBrokersExecutionClientFactory::new();
331 let config = InteractiveBrokersExecutionClientConfig {
332 account_id: Some(String::from("U7654321")),
333 ..Default::default()
334 };
335 let cache = Rc::new(RefCell::new(Cache::default()));
336
337 let result = factory.create(
338 TraderId::from("TRADER-001"),
339 "IB-CUSTOM",
340 &config,
341 cache.into(),
342 );
343
344 assert!(result.is_ok());
345 let client = result.unwrap();
346 assert_eq!(client.account_id(), AccountId::from("IB-CUSTOM-U7654321"));
347 }
348}