Skip to main content

nautilus_hyperliquid/
config.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//! Configuration structures for the Hyperliquid adapter.
17
18use nautilus_network::websocket::TransportBackend;
19use serde::{Deserialize, Serialize};
20
21use crate::common::{
22    consts::{info_url, ws_url},
23    enums::HyperliquidEnvironment,
24};
25
26/// Configuration for the Hyperliquid data client.
27#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
28#[serde(default, deny_unknown_fields)]
29#[cfg_attr(
30    feature = "python",
31    pyo3::pyclass(
32        module = "nautilus_trader.core.nautilus_pyo3.hyperliquid",
33        from_py_object
34    )
35)]
36#[cfg_attr(
37    feature = "python",
38    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.hyperliquid")
39)]
40pub struct HyperliquidDataClientConfig {
41    /// Optional private key for authenticated endpoints.
42    pub private_key: Option<String>,
43    /// Override for the WebSocket URL.
44    pub base_url_ws: Option<String>,
45    /// Override for the HTTP info URL.
46    pub base_url_http: Option<String>,
47    /// Optional proxy URL for HTTP and WebSocket transports.
48    pub proxy_url: Option<String>,
49    /// The target environment (mainnet or testnet).
50    #[builder(default)]
51    pub environment: HyperliquidEnvironment,
52    /// HTTP timeout in seconds.
53    #[builder(default = 60)]
54    pub http_timeout_secs: u64,
55    /// WebSocket timeout in seconds.
56    #[builder(default = 30)]
57    pub ws_timeout_secs: u64,
58    /// Interval for refreshing instruments in minutes.
59    #[builder(default = 60)]
60    pub update_instruments_interval_mins: u64,
61    /// WebSocket transport backend (defaults to `Tungstenite`).
62    #[builder(default)]
63    pub transport_backend: TransportBackend,
64}
65
66impl Default for HyperliquidDataClientConfig {
67    fn default() -> Self {
68        Self::builder().build()
69    }
70}
71
72impl HyperliquidDataClientConfig {
73    /// Creates a new configuration with default settings.
74    #[must_use]
75    pub fn new() -> Self {
76        Self::default()
77    }
78
79    /// Returns `true` when private key is populated and non-empty.
80    #[must_use]
81    pub fn has_credentials(&self) -> bool {
82        self.private_key
83            .as_deref()
84            .is_some_and(|s| !s.trim().is_empty())
85    }
86
87    /// Returns the WebSocket URL, respecting the environment and overrides.
88    #[must_use]
89    pub fn ws_url(&self) -> String {
90        self.base_url_ws
91            .clone()
92            .unwrap_or_else(|| ws_url(self.environment).to_string())
93    }
94
95    /// Returns the HTTP info URL, respecting the environment and overrides.
96    #[must_use]
97    pub fn http_url(&self) -> String {
98        self.base_url_http
99            .clone()
100            .unwrap_or_else(|| info_url(self.environment).to_string())
101    }
102}
103
104/// Configuration for the Hyperliquid execution client.
105#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
106#[serde(default, deny_unknown_fields)]
107#[cfg_attr(
108    feature = "python",
109    pyo3::pyclass(
110        module = "nautilus_trader.core.nautilus_pyo3.hyperliquid",
111        from_py_object
112    )
113)]
114#[cfg_attr(
115    feature = "python",
116    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.hyperliquid")
117)]
118pub struct HyperliquidExecClientConfig {
119    /// Private key for signing transactions.
120    ///
121    /// If not provided, falls back to environment variable:
122    /// - Mainnet: `HYPERLIQUID_PK`
123    /// - Testnet: `HYPERLIQUID_TESTNET_PK`
124    pub private_key: Option<String>,
125    /// Optional vault address for vault operations.
126    ///
127    /// If not provided, falls back to environment variable:
128    /// - Mainnet: `HYPERLIQUID_VAULT`
129    /// - Testnet: `HYPERLIQUID_TESTNET_VAULT`
130    pub vault_address: Option<String>,
131    /// Optional main account address when using an agent wallet (API sub-key).
132    /// When set, used for balance queries, position reports, and WS subscriptions
133    /// instead of the address derived from the private key.
134    ///
135    /// If not provided and no explicit vault address is set, falls back to
136    /// the `HYPERLIQUID_ACCOUNT_ADDRESS` environment variable.
137    pub account_address: Option<String>,
138    /// Override for the WebSocket URL.
139    pub base_url_ws: Option<String>,
140    /// Override for the HTTP info URL.
141    pub base_url_http: Option<String>,
142    /// Override for the exchange API URL.
143    pub base_url_exchange: Option<String>,
144    /// Optional proxy URL for HTTP and WebSocket transports.
145    pub proxy_url: Option<String>,
146    /// The target environment (mainnet or testnet).
147    #[builder(default)]
148    pub environment: HyperliquidEnvironment,
149    /// HTTP timeout in seconds.
150    #[builder(default = 60)]
151    pub http_timeout_secs: u64,
152    /// Maximum number of retry attempts for HTTP requests.
153    #[builder(default = 3)]
154    pub max_retries: u32,
155    /// Initial retry delay in milliseconds.
156    #[builder(default = 100)]
157    pub retry_delay_initial_ms: u64,
158    /// Maximum retry delay in milliseconds.
159    #[builder(default = 5000)]
160    pub retry_delay_max_ms: u64,
161    /// When true, normalize order prices to 5 significant figures
162    /// before submission (Hyperliquid requirement).
163    #[builder(default = true)]
164    pub normalize_prices: bool,
165    /// Slippage buffer in basis points applied to MARKET orders and
166    /// stop-to-limit trigger derivations. Can be overridden per-order via
167    /// `SubmitOrder.params["market_order_slippage_bps"]`.
168    #[builder(default = 50)]
169    pub market_order_slippage_bps: u32,
170    /// WebSocket transport backend (defaults to `Tungstenite`).
171    #[builder(default)]
172    pub transport_backend: TransportBackend,
173    /// Timeout in seconds for WebSocket post trading requests.
174    #[builder(default = 10)]
175    pub ws_post_timeout_secs: u64,
176    /// Poll interval in seconds for `outcomeMeta` settlement detection.
177    /// Disabled by default; venue `Settlement` fills drive HIP-4 settlement
178    /// through the standard user-fills stream. Set to a non-zero value only
179    /// when the venue fill stream is unavailable.
180    #[builder(default = 0)]
181    pub outcome_settlement_poll_secs: u64,
182}
183
184impl Default for HyperliquidExecClientConfig {
185    fn default() -> Self {
186        Self::builder().build()
187    }
188}
189
190impl HyperliquidExecClientConfig {
191    /// Returns `true` when private key is populated and non-empty.
192    #[must_use]
193    pub fn has_credentials(&self) -> bool {
194        self.private_key
195            .as_deref()
196            .is_some_and(|s| !s.trim().is_empty())
197    }
198
199    /// Returns the WebSocket URL, respecting the environment and overrides.
200    #[must_use]
201    pub fn ws_url(&self) -> String {
202        self.base_url_ws
203            .clone()
204            .unwrap_or_else(|| ws_url(self.environment).to_string())
205    }
206
207    /// Returns the HTTP info URL, respecting the environment and overrides.
208    #[must_use]
209    pub fn http_url(&self) -> String {
210        self.base_url_http
211            .clone()
212            .unwrap_or_else(|| info_url(self.environment).to_string())
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use rstest::rstest;
219
220    use super::*;
221
222    #[rstest]
223    fn test_exec_config_default_account_address_is_none() {
224        let config = HyperliquidExecClientConfig::default();
225        assert!(config.account_address.is_none());
226    }
227
228    #[rstest]
229    fn test_exec_config_with_account_address() {
230        let config = HyperliquidExecClientConfig {
231            account_address: Some("0x1234".to_string()),
232            ..HyperliquidExecClientConfig::default()
233        };
234        assert_eq!(config.account_address.as_deref(), Some("0x1234"));
235    }
236
237    #[rstest]
238    fn test_data_config_toml_minimal() {
239        let config: HyperliquidDataClientConfig = toml::from_str(
240            r#"
241environment = "testnet"
242http_timeout_secs = 30
243update_instruments_interval_mins = 10
244transport_backend = "tungstenite"
245"#,
246        )
247        .unwrap();
248
249        assert_eq!(config.environment, HyperliquidEnvironment::Testnet);
250        assert_eq!(config.http_timeout_secs, 30);
251        assert_eq!(config.update_instruments_interval_mins, 10);
252        assert_eq!(config.transport_backend, TransportBackend::Tungstenite);
253    }
254
255    #[rstest]
256    fn test_exec_config_toml_empty_uses_defaults() {
257        let config: HyperliquidExecClientConfig = toml::from_str("").unwrap();
258        let expected = HyperliquidExecClientConfig::default();
259
260        assert_eq!(config.environment, expected.environment);
261        assert_eq!(config.http_timeout_secs, expected.http_timeout_secs);
262        assert_eq!(config.max_retries, expected.max_retries);
263        assert_eq!(config.normalize_prices, expected.normalize_prices);
264        assert_eq!(
265            config.market_order_slippage_bps,
266            expected.market_order_slippage_bps,
267        );
268        assert_eq!(config.transport_backend, expected.transport_backend);
269        assert_eq!(config.ws_post_timeout_secs, expected.ws_post_timeout_secs);
270        assert_eq!(
271            config.outcome_settlement_poll_secs,
272            expected.outcome_settlement_poll_secs,
273        );
274    }
275}