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