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        _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        // Lighter is a perpetual futures DEX with margin accounts and one
143        // position per market on the L2.
144        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}