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,
30};
31
32use crate::{
33    common::consts::{LIGHTER, LIGHTER_VENUE},
34    config::{LighterDataClientConfig, LighterExecClientConfig},
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 LighterExecClientConfig {
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.core.nautilus_pyo3.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.core.nautilus_pyo3.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        name: &str,
127        config: &dyn ClientConfig,
128        cache: CacheView,
129    ) -> anyhow::Result<Box<dyn ExecutionClient>> {
130        let lighter_config = config
131            .as_any()
132            .downcast_ref::<LighterExecClientConfig>()
133            .ok_or_else(|| {
134                anyhow::anyhow!(
135                    "Invalid config type for LighterExecutionClientFactory. Expected LighterExecClientConfig, was {config:?}",
136                )
137            })?
138            .clone();
139
140        // Lighter is a perpetual futures DEX with margin accounts and one
141        // position per market on the L2.
142        let core = ExecutionClientCore::new(
143            lighter_config.trader_id,
144            ClientId::from(name),
145            *LIGHTER_VENUE,
146            OmsType::Netting,
147            lighter_config.account_id,
148            AccountType::Margin,
149            None,
150            cache,
151        );
152
153        let client = LighterExecutionClient::new(core, lighter_config)?;
154        Ok(Box::new(client))
155    }
156
157    fn name(&self) -> &'static str {
158        LIGHTER
159    }
160
161    fn config_type(&self) -> &'static str {
162        "LighterExecClientConfig"
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use std::{cell::RefCell, rc::Rc};
169
170    use nautilus_common::{
171        cache::Cache,
172        clock::TestClock,
173        factories::{ClientConfig, DataClientFactory, ExecutionClientFactory},
174    };
175    use nautilus_model::identifiers::{AccountId, TraderId};
176    use rstest::rstest;
177
178    use super::*;
179
180    fn exec_config() -> LighterExecClientConfig {
181        LighterExecClientConfig::builder()
182            .trader_id(TraderId::from("TRADER-001"))
183            .account_id(AccountId::from("LIGHTER-001"))
184            .build()
185    }
186
187    #[rstest]
188    fn test_lighter_data_client_factory_creation() {
189        let factory = LighterDataClientFactory::new();
190        assert_eq!(factory.name(), LIGHTER);
191        assert_eq!(factory.config_type(), "LighterDataClientConfig");
192    }
193
194    #[rstest]
195    fn test_lighter_execution_client_factory_creation() {
196        let factory = LighterExecutionClientFactory::new();
197        assert_eq!(factory.name(), LIGHTER);
198        assert_eq!(factory.config_type(), "LighterExecClientConfig");
199    }
200
201    #[rstest]
202    fn test_lighter_exec_client_config_implements_client_config() {
203        let config = exec_config();
204        let boxed_config: Box<dyn ClientConfig> = Box::new(config);
205        let downcasted = boxed_config
206            .as_any()
207            .downcast_ref::<LighterExecClientConfig>();
208
209        assert!(downcasted.is_some());
210    }
211
212    #[rstest]
213    fn test_lighter_execution_client_factory_rejects_wrong_config_type() {
214        let factory = LighterExecutionClientFactory::new();
215        let wrong_config = LighterDataClientConfig::default();
216
217        let cache = Rc::new(RefCell::new(Cache::default()));
218
219        let result = factory.create("LIGHTER-TEST", &wrong_config, cache.into());
220        assert!(result.is_err());
221        assert!(
222            result
223                .err()
224                .unwrap()
225                .to_string()
226                .contains("Invalid config type")
227        );
228    }
229
230    #[rstest]
231    fn test_lighter_execution_client_factory_constructs_without_credentials() {
232        let factory = LighterExecutionClientFactory::new();
233        let config = exec_config();
234        let cache = Rc::new(RefCell::new(Cache::default()));
235
236        let client = factory
237            .create("LIGHTER-TEST", &config, cache.into())
238            .expect("expected client to construct without credentials");
239
240        assert!(!client.is_connected());
241    }
242
243    #[rstest]
244    fn test_lighter_data_client_factory_rejects_wrong_config_type() {
245        let factory = LighterDataClientFactory::new();
246        let wrong_config = exec_config();
247        let cache = Rc::new(RefCell::new(Cache::default()));
248        let clock = Rc::new(RefCell::new(TestClock::new()));
249
250        let result = factory.create("LIGHTER-TEST", &wrong_config, cache.into(), clock);
251        assert!(result.is_err());
252        assert!(
253            result
254                .err()
255                .unwrap()
256                .to_string()
257                .contains("Invalid config type")
258        );
259    }
260}