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 ) -> anyhow::Result<Box<dyn ExecutionClient>> {
131 let lighter_config = config
132 .as_any()
133 .downcast_ref::<LighterExecutionClientConfig>()
134 .ok_or_else(|| {
135 anyhow::anyhow!(
136 "Invalid config type for LighterExecutionClientFactory. Expected LighterExecutionClientConfig, was {config:?}",
137 )
138 })?
139 .clone();
140
141 let core = ExecutionClientCore::new(
144 trader_id,
145 ClientId::from(name),
146 lighter_config.resolved_venue(),
147 OmsType::Netting,
148 lighter_config.account_id,
149 AccountType::Margin,
150 None,
151 cache,
152 );
153
154 let client = LighterExecutionClient::new(core, lighter_config)?;
155 Ok(Box::new(client))
156 }
157
158 fn name(&self) -> &'static str {
159 LIGHTER
160 }
161
162 fn config_type(&self) -> &'static str {
163 "LighterExecutionClientConfig"
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use std::{cell::RefCell, rc::Rc};
170
171 use nautilus_common::{
172 cache::Cache,
173 clock::TestClock,
174 factories::{ClientConfig, DataClientFactory, ExecutionClientFactory},
175 live::runner::replace_data_event_sender,
176 messages::DataEvent,
177 };
178 use nautilus_model::identifiers::{AccountId, ClientId, TraderId, Venue};
179 use rstest::rstest;
180
181 use super::*;
182 use crate::common::{
183 consts::{
184 LIGHTER_CLIENT_ID, LIGHTER_ROBINHOOD_CLIENT_ID, LIGHTER_ROBINHOOD_VENUE, LIGHTER_VENUE,
185 },
186 enums::LighterDeployment,
187 };
188
189 const PRIVATE_KEY_HEX: &str =
190 "0b8e0f63c24d8baacd9d29ad4e9a4b73c4a8d2bb8b16dc4fa9d7c2e1d3a8b1f0e8d3a4c5b6e7f001";
191
192 fn exec_config() -> LighterExecutionClientConfig {
193 LighterExecutionClientConfig::builder()
194 .account_id(AccountId::from("LIGHTER-001"))
195 .account_index(12_345)
196 .api_key_index(5)
197 .private_key(PRIVATE_KEY_HEX.to_string())
198 .build()
199 }
200
201 #[rstest]
202 fn test_lighter_data_client_factory_creation() {
203 let factory = LighterDataClientFactory::new();
204 assert_eq!(factory.name(), LIGHTER);
205 assert_eq!(factory.config_type(), "LighterDataClientConfig");
206 }
207
208 #[rstest]
209 fn test_lighter_execution_client_factory_creation() {
210 let factory = LighterExecutionClientFactory::new();
211 assert_eq!(factory.name(), LIGHTER);
212 assert_eq!(factory.config_type(), "LighterExecutionClientConfig");
213 }
214
215 #[rstest]
216 fn test_lighter_exec_client_config_implements_client_config() {
217 let config = exec_config();
218 let boxed_config: Box<dyn ClientConfig> = Box::new(config);
219 let downcasted = boxed_config
220 .as_any()
221 .downcast_ref::<LighterExecutionClientConfig>();
222
223 assert!(downcasted.is_some());
224 }
225
226 #[rstest]
227 fn test_lighter_execution_client_factory_rejects_wrong_config_type() {
228 let factory = LighterExecutionClientFactory::new();
229 let wrong_config = LighterDataClientConfig::default();
230
231 let cache = Rc::new(RefCell::new(Cache::default()));
232
233 let result = factory.create(
234 TraderId::from("TRADER-001"),
235 "LIGHTER-TEST",
236 &wrong_config,
237 cache.into(),
238 );
239 assert!(result.is_err());
240 assert!(
241 result
242 .err()
243 .unwrap()
244 .to_string()
245 .contains("Invalid config type")
246 );
247 }
248
249 #[rstest]
250 fn test_lighter_execution_client_factory_constructs() {
251 let factory = LighterExecutionClientFactory::new();
252 let config = exec_config();
253 let cache = Rc::new(RefCell::new(Cache::default()));
254
255 let client = factory
256 .create(
257 TraderId::from("TRADER-001"),
258 "LIGHTER-TEST",
259 &config,
260 cache.into(),
261 )
262 .expect("expected client to construct");
263
264 assert!(!client.is_connected());
265 }
266
267 #[rstest]
268 fn test_factories_preserve_client_ids_and_custom_venue() {
269 let venue = Venue::from("LIGHTER_CUSTOM");
270 let data_config = LighterDataClientConfig {
271 deployment: LighterDeployment::Robinhood,
272 venue: Some(venue),
273 ..Default::default()
274 };
275
276 let exec_config = LighterExecutionClientConfig::builder()
277 .account_id(AccountId::from("LIGHTER_CUSTOM-001"))
278 .deployment(LighterDeployment::Robinhood)
279 .venue(venue)
280 .build();
281
282 let cache = Rc::new(RefCell::new(Cache::default()));
283 let clock = Rc::new(RefCell::new(TestClock::new()));
284 let (data_tx, _data_rx) = tokio::sync::mpsc::unbounded_channel::<DataEvent>();
285 replace_data_event_sender(data_tx);
286
287 let data_client = LighterDataClientFactory::new()
288 .create("RH-DATA", &data_config, cache.clone().into(), clock)
289 .expect("expected data client to construct");
290 let exec_client = LighterExecutionClientFactory::new()
291 .create(
292 TraderId::from("TRADER-001"),
293 "RH-EXEC",
294 &exec_config,
295 cache.into(),
296 )
297 .expect("expected execution client to construct");
298
299 assert_eq!(data_client.client_id(), ClientId::from("RH-DATA"));
300 assert_eq!(data_client.venue(), Some(venue));
301 assert_eq!(exec_client.client_id(), ClientId::from("RH-EXEC"));
302 assert_eq!(exec_client.venue(), venue);
303 assert_eq!(
304 exec_client.account_id(),
305 AccountId::from("LIGHTER_CUSTOM-001")
306 );
307 }
308
309 #[rstest]
310 fn test_factories_preserve_distinct_deployment_identities() {
311 let lighter_data_config = LighterDataClientConfig::default();
312 let robinhood_data_config = LighterDataClientConfig {
313 deployment: LighterDeployment::Robinhood,
314 ..Default::default()
315 };
316
317 let lighter_exec_config = exec_config();
318
319 let robinhood_exec_config = LighterExecutionClientConfig::builder()
320 .account_id(AccountId::from("LIGHTER_ROBINHOOD-001"))
321 .account_index(12_345)
322 .api_key_index(5)
323 .private_key(PRIVATE_KEY_HEX.to_string())
324 .deployment(LighterDeployment::Robinhood)
325 .build();
326
327 let cache = Rc::new(RefCell::new(Cache::default()));
328 let (data_tx, _data_rx) = tokio::sync::mpsc::unbounded_channel::<DataEvent>();
329 replace_data_event_sender(data_tx);
330
331 let lighter_data = LighterDataClientFactory::new()
332 .create(
333 LIGHTER_CLIENT_ID.as_str(),
334 &lighter_data_config,
335 cache.clone().into(),
336 Rc::new(RefCell::new(TestClock::new())),
337 )
338 .expect("expected Lighter data client to construct");
339
340 let robinhood_data = LighterDataClientFactory::new()
341 .create(
342 LIGHTER_ROBINHOOD_CLIENT_ID.as_str(),
343 &robinhood_data_config,
344 cache.clone().into(),
345 Rc::new(RefCell::new(TestClock::new())),
346 )
347 .expect("expected Robinhood data client to construct");
348
349 let lighter_exec = LighterExecutionClientFactory::new()
350 .create(
351 TraderId::from("TRADER-001"),
352 LIGHTER_CLIENT_ID.as_str(),
353 &lighter_exec_config,
354 cache.clone().into(),
355 )
356 .expect("expected Lighter execution client to construct");
357
358 let robinhood_exec = LighterExecutionClientFactory::new()
359 .create(
360 TraderId::from("TRADER-001"),
361 LIGHTER_ROBINHOOD_CLIENT_ID.as_str(),
362 &robinhood_exec_config,
363 cache.into(),
364 )
365 .expect("expected Robinhood execution client to construct");
366
367 assert_eq!(lighter_data.client_id(), *LIGHTER_CLIENT_ID);
368 assert_eq!(lighter_data.venue(), Some(*LIGHTER_VENUE));
369 assert_eq!(robinhood_data.client_id(), *LIGHTER_ROBINHOOD_CLIENT_ID);
370 assert_eq!(robinhood_data.venue(), Some(*LIGHTER_ROBINHOOD_VENUE));
371 assert_eq!(lighter_exec.client_id(), *LIGHTER_CLIENT_ID);
372 assert_eq!(lighter_exec.account_id(), AccountId::from("LIGHTER-001"));
373 assert_eq!(lighter_exec.venue(), *LIGHTER_VENUE);
374 assert_eq!(robinhood_exec.client_id(), *LIGHTER_ROBINHOOD_CLIENT_ID);
375 assert_eq!(
376 robinhood_exec.account_id(),
377 AccountId::from("LIGHTER_ROBINHOOD-001")
378 );
379 assert_eq!(robinhood_exec.venue(), *LIGHTER_ROBINHOOD_VENUE);
380 }
381
382 #[rstest]
383 fn test_execution_factory_rejects_account_issuer_venue_mismatch() {
384 let config = LighterExecutionClientConfig::builder()
385 .deployment(LighterDeployment::Robinhood)
386 .build();
387
388 let cache = Rc::new(RefCell::new(Cache::default()));
389
390 let result = LighterExecutionClientFactory::new().create(
391 TraderId::from("TRADER-001"),
392 "RH-EXEC",
393 &config,
394 cache.into(),
395 );
396
397 let error = match result {
398 Ok(_) => panic!("mismatched account issuer should fail"),
399 Err(e) => e,
400 };
401
402 assert!(error.to_string().contains(
403 "account ID issuer LIGHTER does not match configured venue LIGHTER_ROBINHOOD"
404 ));
405 }
406
407 #[rstest]
408 fn test_lighter_data_client_factory_rejects_wrong_config_type() {
409 let factory = LighterDataClientFactory::new();
410 let wrong_config = exec_config();
411 let cache = Rc::new(RefCell::new(Cache::default()));
412 let clock = Rc::new(RefCell::new(TestClock::new()));
413
414 let result = factory.create("LIGHTER-TEST", &wrong_config, cache.into(), clock);
415 assert!(result.is_err());
416 assert!(
417 result
418 .err()
419 .unwrap()
420 .to_string()
421 .contains("Invalid config type")
422 );
423 }
424}