Skip to main content

nautilus_lighter/
factories.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Factory functions for creating Lighter clients and components.
17
18use 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/// Factory for creating Lighter data clients.
52#[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    /// Creates a new [`LighterDataClientFactory`] instance.
65    #[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/// Factory for creating Lighter execution clients.
104#[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    /// Creates a new [`LighterExecutionClientFactory`] instance.
117    #[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        // Lighter is a perpetual futures DEX with margin accounts and one
142        // position per market on the L2.
143        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}