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