Skip to main content

nautilus_hyperliquid/
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 Hyperliquid 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::{AccountId, ClientId, TraderId},
30};
31
32use crate::{
33    common::consts::{HYPERLIQUID, HYPERLIQUID_VENUE},
34    config::{HyperliquidDataClientConfig, HyperliquidExecClientConfig},
35    data::HyperliquidDataClient,
36    execution::HyperliquidExecutionClient,
37};
38
39impl ClientConfig for HyperliquidDataClientConfig {
40    fn as_any(&self) -> &dyn Any {
41        self
42    }
43}
44
45impl ClientConfig for HyperliquidExecClientConfig {
46    fn as_any(&self) -> &dyn Any {
47        self
48    }
49}
50
51/// Factory for creating Hyperliquid data clients.
52#[derive(Debug, Clone)]
53#[cfg_attr(
54    feature = "python",
55    pyo3::pyclass(
56        module = "nautilus_trader.core.nautilus_pyo3.hyperliquid",
57        from_py_object
58    )
59)]
60#[cfg_attr(
61    feature = "python",
62    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.hyperliquid")
63)]
64pub struct HyperliquidDataClientFactory;
65
66impl HyperliquidDataClientFactory {
67    /// Creates a new [`HyperliquidDataClientFactory`] instance.
68    #[must_use]
69    pub const fn new() -> Self {
70        Self
71    }
72}
73
74impl Default for HyperliquidDataClientFactory {
75    fn default() -> Self {
76        Self::new()
77    }
78}
79
80impl DataClientFactory for HyperliquidDataClientFactory {
81    fn create(
82        &self,
83        name: &str,
84        config: &dyn ClientConfig,
85        _cache: CacheView,
86        _clock: Rc<RefCell<dyn Clock>>,
87    ) -> anyhow::Result<Box<dyn DataClient>> {
88        let hyperliquid_config = config
89            .as_any()
90            .downcast_ref::<HyperliquidDataClientConfig>()
91            .ok_or_else(|| {
92                anyhow::anyhow!(
93                    "Invalid config type for HyperliquidDataClientFactory. Expected HyperliquidDataClientConfig, was {config:?}",
94                )
95            })?
96            .clone();
97
98        let client_id = ClientId::from(name);
99        let client = HyperliquidDataClient::new(client_id, hyperliquid_config)?;
100        Ok(Box::new(client))
101    }
102
103    fn name(&self) -> &'static str {
104        HYPERLIQUID
105    }
106
107    fn config_type(&self) -> &'static str {
108        "HyperliquidDataClientConfig"
109    }
110}
111
112/// Configuration for creating Hyperliquid execution clients via factory.
113///
114/// This wraps [`HyperliquidExecClientConfig`] with the additional trader and account
115/// identifiers required by the [`ExecutionClientCore`].
116#[derive(Clone, Debug)]
117#[cfg_attr(
118    feature = "python",
119    pyo3::pyclass(
120        module = "nautilus_trader.core.nautilus_pyo3.hyperliquid",
121        from_py_object
122    )
123)]
124#[cfg_attr(
125    feature = "python",
126    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.hyperliquid")
127)]
128pub struct HyperliquidExecFactoryConfig {
129    /// The trader ID for the execution client.
130    pub trader_id: TraderId,
131    /// The account ID for the execution client.
132    pub account_id: AccountId,
133    /// The underlying execution client configuration.
134    pub config: HyperliquidExecClientConfig,
135}
136
137#[cfg(feature = "python")]
138nautilus_core::impl_pyo3_config_getters!(HyperliquidExecFactoryConfig {
139    trader_id: TraderId,
140    account_id: AccountId,
141    config: HyperliquidExecClientConfig,
142});
143
144impl ClientConfig for HyperliquidExecFactoryConfig {
145    fn as_any(&self) -> &dyn Any {
146        self
147    }
148}
149
150/// Factory for creating Hyperliquid execution clients.
151#[derive(Debug, Clone)]
152#[cfg_attr(
153    feature = "python",
154    pyo3::pyclass(
155        module = "nautilus_trader.core.nautilus_pyo3.hyperliquid",
156        from_py_object
157    )
158)]
159#[cfg_attr(
160    feature = "python",
161    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.hyperliquid")
162)]
163pub struct HyperliquidExecutionClientFactory;
164
165impl HyperliquidExecutionClientFactory {
166    /// Creates a new [`HyperliquidExecutionClientFactory`] instance.
167    #[must_use]
168    pub const fn new() -> Self {
169        Self
170    }
171}
172
173impl Default for HyperliquidExecutionClientFactory {
174    fn default() -> Self {
175        Self::new()
176    }
177}
178
179impl ExecutionClientFactory for HyperliquidExecutionClientFactory {
180    fn create(
181        &self,
182        name: &str,
183        config: &dyn ClientConfig,
184        cache: CacheView,
185    ) -> anyhow::Result<Box<dyn ExecutionClient>> {
186        let factory_config = config
187            .as_any()
188            .downcast_ref::<HyperliquidExecFactoryConfig>()
189            .ok_or_else(|| {
190                anyhow::anyhow!(
191                    "Invalid config type for HyperliquidExecutionClientFactory. Expected HyperliquidExecFactoryConfig, was {config:?}",
192                )
193            })?
194            .clone();
195
196        // Hyperliquid uses netting for perpetual futures
197        let oms_type = OmsType::Netting;
198
199        // Hyperliquid is always margin (perpetual futures)
200        let account_type = AccountType::Margin;
201
202        let core = ExecutionClientCore::new(
203            factory_config.trader_id,
204            ClientId::from(name),
205            *HYPERLIQUID_VENUE,
206            oms_type,
207            factory_config.account_id,
208            account_type,
209            None,
210            cache,
211        );
212
213        let client = HyperliquidExecutionClient::new(core, factory_config.config)?;
214        Ok(Box::new(client))
215    }
216
217    fn name(&self) -> &'static str {
218        HYPERLIQUID
219    }
220
221    fn config_type(&self) -> &'static str {
222        "HyperliquidExecFactoryConfig"
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use std::{cell::RefCell, rc::Rc};
229
230    use nautilus_common::{
231        cache::Cache,
232        clock::TestClock,
233        factories::{ClientConfig, DataClientFactory, ExecutionClientFactory},
234    };
235    use nautilus_model::identifiers::{AccountId, TraderId};
236    use rstest::rstest;
237
238    use super::*;
239    use crate::config::{HyperliquidDataClientConfig, HyperliquidExecClientConfig};
240
241    #[rstest]
242    fn test_hyperliquid_data_client_factory_creation() {
243        let factory = HyperliquidDataClientFactory::new();
244        assert_eq!(factory.name(), HYPERLIQUID);
245        assert_eq!(factory.config_type(), "HyperliquidDataClientConfig");
246    }
247
248    #[rstest]
249    fn test_hyperliquid_data_client_factory_default() {
250        let factory = HyperliquidDataClientFactory;
251        assert_eq!(factory.name(), HYPERLIQUID);
252    }
253
254    #[rstest]
255    fn test_hyperliquid_execution_client_factory_creation() {
256        let factory = HyperliquidExecutionClientFactory::new();
257        assert_eq!(factory.name(), HYPERLIQUID);
258        assert_eq!(factory.config_type(), "HyperliquidExecFactoryConfig");
259    }
260
261    #[rstest]
262    fn test_hyperliquid_execution_client_factory_default() {
263        let factory = HyperliquidExecutionClientFactory;
264        assert_eq!(factory.name(), HYPERLIQUID);
265    }
266
267    #[rstest]
268    fn test_hyperliquid_data_client_config_implements_client_config() {
269        let config = HyperliquidDataClientConfig::default();
270        let boxed_config: Box<dyn ClientConfig> = Box::new(config);
271        let downcasted = boxed_config
272            .as_any()
273            .downcast_ref::<HyperliquidDataClientConfig>();
274
275        assert!(downcasted.is_some());
276    }
277
278    #[rstest]
279    fn test_hyperliquid_exec_factory_config_implements_client_config() {
280        let config = HyperliquidExecFactoryConfig {
281            trader_id: TraderId::from("TRADER-001"),
282            account_id: AccountId::from("HYPERLIQUID-001"),
283            config: HyperliquidExecClientConfig::builder()
284                .private_key("test_private_key".to_string())
285                .build(),
286        };
287
288        let boxed_config: Box<dyn ClientConfig> = Box::new(config);
289        let downcasted = boxed_config
290            .as_any()
291            .downcast_ref::<HyperliquidExecFactoryConfig>();
292
293        assert!(downcasted.is_some());
294    }
295
296    #[rstest]
297    fn test_hyperliquid_data_client_factory_rejects_wrong_config_type() {
298        let factory = HyperliquidDataClientFactory::new();
299        let wrong_config = HyperliquidExecFactoryConfig {
300            trader_id: TraderId::from("TRADER-001"),
301            account_id: AccountId::from("HYPERLIQUID-001"),
302            config: HyperliquidExecClientConfig::builder()
303                .private_key("test_private_key".to_string())
304                .build(),
305        };
306
307        let cache = Rc::new(RefCell::new(Cache::default()));
308        let clock = Rc::new(RefCell::new(TestClock::new()));
309
310        let result = factory.create("HYPERLIQUID-TEST", &wrong_config, cache.into(), clock);
311        assert!(result.is_err());
312        assert!(
313            result
314                .err()
315                .unwrap()
316                .to_string()
317                .contains("Invalid config type")
318        );
319    }
320
321    #[rstest]
322    fn test_hyperliquid_execution_client_factory_rejects_wrong_config_type() {
323        let factory = HyperliquidExecutionClientFactory::new();
324        let wrong_config = HyperliquidDataClientConfig::default();
325
326        let cache = Rc::new(RefCell::new(Cache::default()));
327
328        let result = factory.create("HYPERLIQUID-TEST", &wrong_config, cache.into());
329        assert!(result.is_err());
330        assert!(
331            result
332                .err()
333                .unwrap()
334                .to_string()
335                .contains("Invalid config type")
336        );
337    }
338}