1use std::{any::Any, cell::RefCell, rc::Rc};
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::{ClientId, TraderId},
30};
31
32use crate::{
33 common::consts::LIGHTER,
34 config::{LighterDataClientConfig, LighterExecutionClientConfig},
35 data::LighterDataClient,
36 execution::LighterExecutionClient,
37};
38
39impl ClientConfig for LighterDataClientConfig {
40 fn as_any(&self) -> &dyn Any {
41 self
42 }
43}
44
45impl ClientConfig for LighterExecutionClientConfig {
46 fn as_any(&self) -> &dyn Any {
47 self
48 }
49}
50
51#[derive(Debug, Clone, Default)]
53#[cfg_attr(
54 feature = "python",
55 pyo3::pyclass(module = "nautilus_trader.adapters.lighter", from_py_object,)
56)]
57#[cfg_attr(
58 feature = "python",
59 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.lighter")
60)]
61pub struct LighterDataClientFactory;
62
63impl LighterDataClientFactory {
64 #[must_use]
66 pub const fn new() -> Self {
67 Self
68 }
69}
70
71impl DataClientFactory for LighterDataClientFactory {
72 fn create(
73 &self,
74 name: &str,
75 config: &dyn ClientConfig,
76 _cache: CacheView,
77 _clock: Rc<RefCell<dyn Clock>>,
78 ) -> anyhow::Result<Box<dyn DataClient>> {
79 let lighter_config = config
80 .as_any()
81 .downcast_ref::<LighterDataClientConfig>()
82 .ok_or_else(|| {
83 anyhow::anyhow!(
84 "Invalid config type for LighterDataClientFactory. Expected LighterDataClientConfig, was {config:?}",
85 )
86 })?
87 .clone();
88
89 let client_id = ClientId::from(name);
90 let client = LighterDataClient::new(client_id, lighter_config)?;
91 Ok(Box::new(client))
92 }
93
94 fn name(&self) -> &'static str {
95 LIGHTER
96 }
97
98 fn config_type(&self) -> &'static str {
99 "LighterDataClientConfig"
100 }
101}
102
103#[derive(Debug, Clone, Default)]
105#[cfg_attr(
106 feature = "python",
107 pyo3::pyclass(module = "nautilus_trader.adapters.lighter", from_py_object,)
108)]
109#[cfg_attr(
110 feature = "python",
111 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.lighter")
112)]
113pub struct LighterExecutionClientFactory;
114
115impl LighterExecutionClientFactory {
116 #[must_use]
118 pub const fn new() -> Self {
119 Self
120 }
121}
122
123impl ExecutionClientFactory for LighterExecutionClientFactory {
124 fn create(
125 &self,
126 trader_id: TraderId,
127 name: &str,
128 config: &dyn ClientConfig,
129 cache: CacheView,
130 _clock: Rc<RefCell<dyn Clock>>,
131 ) -> anyhow::Result<Box<dyn ExecutionClient>> {
132 let lighter_config = config
133 .as_any()
134 .downcast_ref::<LighterExecutionClientConfig>()
135 .ok_or_else(|| {
136 anyhow::anyhow!(
137 "Invalid config type for LighterExecutionClientFactory. Expected LighterExecutionClientConfig, was {config:?}",
138 )
139 })?
140 .clone();
141
142 let core = ExecutionClientCore::new(
145 trader_id,
146 ClientId::from(name),
147 lighter_config.resolved_venue(),
148 OmsType::Netting,
149 lighter_config.account_id,
150 AccountType::Margin,
151 None,
152 cache,
153 );
154
155 let client = LighterExecutionClient::new(core, lighter_config)?;
156 Ok(Box::new(client))
157 }
158
159 fn name(&self) -> &'static str {
160 LIGHTER
161 }
162
163 fn config_type(&self) -> &'static str {
164 "LighterExecutionClientConfig"
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use std::{cell::RefCell, rc::Rc};
171
172 use nautilus_common::{
173 cache::Cache,
174 clock::VirtualClock,
175 factories::{ClientConfig, DataClientFactory, ExecutionClientFactory},
176 live::runner::replace_data_event_sender,
177 messages::DataEvent,
178 };
179 use nautilus_model::identifiers::{AccountId, ClientId, TraderId, Venue};
180 use rstest::rstest;
181
182 use super::*;
183 use crate::common::{
184 consts::{
185 LIGHTER_CLIENT_ID, LIGHTER_ROBINHOOD_CLIENT_ID, LIGHTER_ROBINHOOD_VENUE, LIGHTER_VENUE,
186 },
187 enums::LighterDeployment,
188 };
189
190 const PRIVATE_KEY_HEX: &str =
191 "0b8e0f63c24d8baacd9d29ad4e9a4b73c4a8d2bb8b16dc4fa9d7c2e1d3a8b1f0e8d3a4c5b6e7f001";
192
193 fn exec_config() -> LighterExecutionClientConfig {
194 LighterExecutionClientConfig::builder()
195 .account_id(AccountId::from("LIGHTER-001"))
196 .account_index(12_345)
197 .api_key_index(5)
198 .private_key(PRIVATE_KEY_HEX.into())
199 .build()
200 }
201
202 #[rstest]
203 fn test_lighter_data_client_factory_creation() {
204 let factory = LighterDataClientFactory::new();
205 assert_eq!(factory.name(), LIGHTER);
206 assert_eq!(factory.config_type(), "LighterDataClientConfig");
207 }
208
209 #[rstest]
210 fn test_lighter_execution_client_factory_creation() {
211 let factory = LighterExecutionClientFactory::new();
212 assert_eq!(factory.name(), LIGHTER);
213 assert_eq!(factory.config_type(), "LighterExecutionClientConfig");
214 }
215
216 #[rstest]
217 fn test_lighter_exec_client_config_implements_client_config() {
218 let config = exec_config();
219 let boxed_config: Box<dyn ClientConfig> = Box::new(config);
220 let downcasted = boxed_config
221 .as_any()
222 .downcast_ref::<LighterExecutionClientConfig>();
223
224 assert!(downcasted.is_some());
225 }
226
227 #[rstest]
228 fn test_lighter_execution_client_factory_rejects_wrong_config_type() {
229 let factory = LighterExecutionClientFactory::new();
230 let wrong_config = LighterDataClientConfig::default();
231
232 let cache = Rc::new(RefCell::new(Cache::default()));
233
234 let result = factory.create(
235 TraderId::from("TRADER-001"),
236 "LIGHTER-TEST",
237 &wrong_config,
238 cache.into(),
239 Rc::new(RefCell::new(VirtualClock::new())),
240 );
241 assert!(result.is_err());
242 assert!(
243 result
244 .err()
245 .unwrap()
246 .to_string()
247 .contains("Invalid config type")
248 );
249 }
250
251 #[rstest]
252 fn test_lighter_execution_client_factory_constructs() {
253 let factory = LighterExecutionClientFactory::new();
254 let config = exec_config();
255 let cache = Rc::new(RefCell::new(Cache::default()));
256
257 let client = factory
258 .create(
259 TraderId::from("TRADER-001"),
260 "LIGHTER-TEST",
261 &config,
262 cache.into(),
263 Rc::new(RefCell::new(VirtualClock::new())),
264 )
265 .expect("expected client to construct");
266
267 assert!(!client.is_connected());
268 }
269
270 #[rstest]
271 fn test_factories_preserve_client_ids_and_custom_venue() {
272 let venue = Venue::from("LIGHTER_CUSTOM");
273 let data_config = LighterDataClientConfig {
274 deployment: LighterDeployment::Robinhood,
275 venue: Some(venue),
276 ..Default::default()
277 };
278
279 let exec_config = LighterExecutionClientConfig::builder()
280 .account_id(AccountId::from("LIGHTER_CUSTOM-001"))
281 .deployment(LighterDeployment::Robinhood)
282 .venue(venue)
283 .build();
284
285 let cache = Rc::new(RefCell::new(Cache::default()));
286 let clock = Rc::new(RefCell::new(VirtualClock::new()));
287 let (data_tx, _data_rx) = tokio::sync::mpsc::unbounded_channel::<DataEvent>();
288 replace_data_event_sender(data_tx);
289
290 let data_client = LighterDataClientFactory::new()
291 .create("RH-DATA", &data_config, cache.clone().into(), clock)
292 .expect("expected data client to construct");
293 let exec_client = LighterExecutionClientFactory::new()
294 .create(
295 TraderId::from("TRADER-001"),
296 "RH-EXEC",
297 &exec_config,
298 cache.into(),
299 Rc::new(RefCell::new(VirtualClock::new())),
300 )
301 .expect("expected execution client to construct");
302
303 assert_eq!(data_client.client_id(), ClientId::from("RH-DATA"));
304 assert_eq!(data_client.venue(), Some(venue));
305 assert_eq!(exec_client.client_id(), ClientId::from("RH-EXEC"));
306 assert_eq!(exec_client.venue(), venue);
307 assert_eq!(
308 exec_client.account_id(),
309 AccountId::from("LIGHTER_CUSTOM-001")
310 );
311 }
312
313 #[rstest]
314 fn test_factories_preserve_distinct_deployment_identities() {
315 let lighter_data_config = LighterDataClientConfig::default();
316 let robinhood_data_config = LighterDataClientConfig {
317 deployment: LighterDeployment::Robinhood,
318 ..Default::default()
319 };
320
321 let lighter_exec_config = exec_config();
322
323 let robinhood_exec_config = LighterExecutionClientConfig::builder()
324 .account_id(AccountId::from("LIGHTER_ROBINHOOD-001"))
325 .account_index(12_345)
326 .api_key_index(5)
327 .private_key(PRIVATE_KEY_HEX.into())
328 .deployment(LighterDeployment::Robinhood)
329 .build();
330
331 let cache = Rc::new(RefCell::new(Cache::default()));
332 let (data_tx, _data_rx) = tokio::sync::mpsc::unbounded_channel::<DataEvent>();
333 replace_data_event_sender(data_tx);
334
335 let lighter_data = LighterDataClientFactory::new()
336 .create(
337 LIGHTER_CLIENT_ID.as_str(),
338 &lighter_data_config,
339 cache.clone().into(),
340 Rc::new(RefCell::new(VirtualClock::new())),
341 )
342 .expect("expected Lighter data client to construct");
343
344 let robinhood_data = LighterDataClientFactory::new()
345 .create(
346 LIGHTER_ROBINHOOD_CLIENT_ID.as_str(),
347 &robinhood_data_config,
348 cache.clone().into(),
349 Rc::new(RefCell::new(VirtualClock::new())),
350 )
351 .expect("expected Robinhood data client to construct");
352
353 let lighter_exec = LighterExecutionClientFactory::new()
354 .create(
355 TraderId::from("TRADER-001"),
356 LIGHTER_CLIENT_ID.as_str(),
357 &lighter_exec_config,
358 cache.clone().into(),
359 Rc::new(RefCell::new(VirtualClock::new())),
360 )
361 .expect("expected Lighter execution client to construct");
362
363 let robinhood_exec = LighterExecutionClientFactory::new()
364 .create(
365 TraderId::from("TRADER-001"),
366 LIGHTER_ROBINHOOD_CLIENT_ID.as_str(),
367 &robinhood_exec_config,
368 cache.into(),
369 Rc::new(RefCell::new(VirtualClock::new())),
370 )
371 .expect("expected Robinhood execution client to construct");
372
373 assert_eq!(lighter_data.client_id(), *LIGHTER_CLIENT_ID);
374 assert_eq!(lighter_data.venue(), Some(*LIGHTER_VENUE));
375 assert_eq!(robinhood_data.client_id(), *LIGHTER_ROBINHOOD_CLIENT_ID);
376 assert_eq!(robinhood_data.venue(), Some(*LIGHTER_ROBINHOOD_VENUE));
377 assert_eq!(lighter_exec.client_id(), *LIGHTER_CLIENT_ID);
378 assert_eq!(lighter_exec.account_id(), AccountId::from("LIGHTER-001"));
379 assert_eq!(lighter_exec.venue(), *LIGHTER_VENUE);
380 assert_eq!(robinhood_exec.client_id(), *LIGHTER_ROBINHOOD_CLIENT_ID);
381 assert_eq!(
382 robinhood_exec.account_id(),
383 AccountId::from("LIGHTER_ROBINHOOD-001")
384 );
385 assert_eq!(robinhood_exec.venue(), *LIGHTER_ROBINHOOD_VENUE);
386 }
387
388 #[rstest]
389 fn test_execution_factory_rejects_account_issuer_venue_mismatch() {
390 let config = LighterExecutionClientConfig::builder()
391 .deployment(LighterDeployment::Robinhood)
392 .build();
393
394 let cache = Rc::new(RefCell::new(Cache::default()));
395
396 let result = LighterExecutionClientFactory::new().create(
397 TraderId::from("TRADER-001"),
398 "RH-EXEC",
399 &config,
400 cache.into(),
401 Rc::new(RefCell::new(VirtualClock::new())),
402 );
403
404 let error = match result {
405 Ok(_) => panic!("mismatched account issuer should fail"),
406 Err(e) => e,
407 };
408
409 assert!(error.to_string().contains(
410 "account ID issuer LIGHTER does not match configured venue LIGHTER_ROBINHOOD"
411 ));
412 }
413
414 #[rstest]
415 fn test_lighter_data_client_factory_rejects_wrong_config_type() {
416 let factory = LighterDataClientFactory::new();
417 let wrong_config = exec_config();
418 let cache = Rc::new(RefCell::new(Cache::default()));
419 let clock = Rc::new(RefCell::new(VirtualClock::new()));
420
421 let result = factory.create("LIGHTER-TEST", &wrong_config, cache.into(), clock);
422 assert!(result.is_err());
423 assert!(
424 result
425 .err()
426 .unwrap()
427 .to_string()
428 .contains("Invalid config type")
429 );
430 }
431}