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
18#[cfg(test)]
19use nautilus_core::string::secret::REDACTED;
20use nautilus_core::string::secret::SecretString;
21use nautilus_model::identifiers::AccountId;
22use nautilus_network::websocket::TransportBackend;
23use serde::{Deserialize, Serialize};
24
25use crate::common::{
26    consts::{info_url, ws_url},
27    enums::HyperliquidEnvironment,
28};
29
30/// Configuration for the Hyperliquid data client.
31///
32/// The `stale_stream_*` options control the stream health monitor. With recovery
33/// enabled, a stale stream is warned about first, targeted-resubscribed once per
34/// recovery cooldown (preserving its original `l2Book` options), and escalated to
35/// a full WebSocket reconnect after `stale_stream_max_targeted_resubscribes`
36/// failed attempts; fresh data resets the ladder. See the Hyperliquid integration
37/// guide ("Stream health and recovery") for details.
38#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
39#[serde(default, deny_unknown_fields)]
40#[cfg_attr(
41    feature = "python",
42    pyo3::pyclass(module = "nautilus_trader.adapters.hyperliquid", from_py_object)
43)]
44#[cfg_attr(
45    feature = "python",
46    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.hyperliquid")
47)]
48pub struct HyperliquidDataClientConfig {
49    /// Optional private key for authenticated endpoints.
50    pub private_key: Option<SecretString>,
51    /// Override for the WebSocket URL.
52    pub base_url_ws: Option<String>,
53    /// Override for the HTTP info URL.
54    pub base_url_http: Option<String>,
55    /// Optional proxy URL for HTTP and WebSocket transports.
56    pub proxy_url: Option<SecretString>,
57    /// The target environment (mainnet or testnet).
58    #[builder(default)]
59    pub environment: HyperliquidEnvironment,
60    /// HTTP timeout in seconds.
61    #[builder(default = 60)]
62    pub http_timeout_secs: u64,
63    /// WebSocket timeout in seconds.
64    #[builder(default = 30)]
65    pub ws_timeout_secs: u64,
66    /// Receive-age threshold in seconds for warning about stale market-data streams.
67    /// Choose a value above the instrument's expected quiet period.
68    /// Set to 0 to disable the stream health monitor.
69    #[builder(default = 120)]
70    pub stale_stream_receive_timeout_secs: u64,
71    /// Interval in seconds for running market-data stream health checks.
72    /// Set to 0 to disable the stream health monitor.
73    #[builder(default = 15)]
74    pub stream_health_check_interval_secs: u64,
75    /// Cooldown in seconds between stale warnings for the same market-data stream.
76    #[builder(default = 60)]
77    pub stale_stream_warning_cooldown_secs: u64,
78    /// Enables automated stale-stream recovery. Off by default: the stream health
79    /// monitor warns only and never changes subscriptions.
80    #[builder(default = false)]
81    pub stale_stream_recovery_enabled: bool,
82    /// Cooldown in seconds between recovery actions for the same market-data stream.
83    /// Must be positive for recovery to run.
84    #[builder(default = 120)]
85    pub stale_stream_recovery_cooldown_secs: u64,
86    /// Targeted resubscribe attempts for a stale stream before escalating to a
87    /// full WebSocket reconnect.
88    #[builder(default = 3)]
89    pub stale_stream_max_targeted_resubscribes: u32,
90    /// Interval for refreshing instruments in minutes.
91    ///
92    /// Set to 0 to disable the periodic refresh.
93    #[builder(default = 60)]
94    pub update_instruments_interval_mins: u64,
95    /// WebSocket transport backend (`Sockudo` by default; `Tungstenite` when
96    /// the `transport-sockudo` feature is disabled).
97    #[builder(default)]
98    pub transport_backend: TransportBackend,
99}
100
101#[cfg(feature = "python")]
102nautilus_core::impl_pyo3_config_getters!(HyperliquidDataClientConfig {
103    environment: HyperliquidEnvironment,
104    base_url_ws: Option<String>,
105    base_url_http: Option<String>,
106    http_timeout_secs: u64,
107    ws_timeout_secs: u64,
108    update_instruments_interval_mins: u64,
109    transport_backend: TransportBackend,
110    stale_stream_receive_timeout_secs: u64,
111    stream_health_check_interval_secs: u64,
112    stale_stream_warning_cooldown_secs: u64,
113    stale_stream_recovery_enabled: bool,
114    stale_stream_recovery_cooldown_secs: u64,
115    stale_stream_max_targeted_resubscribes: u32,
116});
117
118impl Default for HyperliquidDataClientConfig {
119    fn default() -> Self {
120        Self::builder().build()
121    }
122}
123
124impl HyperliquidDataClientConfig {
125    /// Creates a new configuration with default settings.
126    #[must_use]
127    pub fn new() -> Self {
128        Self::default()
129    }
130
131    /// Returns `true` when private key is populated and non-empty.
132    #[must_use]
133    pub fn has_credentials(&self) -> bool {
134        self.private_key
135            .as_ref()
136            .map(SecretString::expose_secret)
137            .is_some_and(|s| !s.trim().is_empty())
138    }
139
140    /// Returns the WebSocket URL, respecting the environment and overrides.
141    #[must_use]
142    pub fn ws_url(&self) -> String {
143        self.base_url_ws
144            .clone()
145            .unwrap_or_else(|| ws_url(self.environment).to_string())
146    }
147
148    /// Returns the HTTP info URL, respecting the environment and overrides.
149    #[must_use]
150    pub fn http_url(&self) -> String {
151        self.base_url_http
152            .clone()
153            .unwrap_or_else(|| info_url(self.environment).to_string())
154    }
155}
156
157/// Configuration for the Hyperliquid execution client.
158#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
159#[serde(default, deny_unknown_fields)]
160#[cfg_attr(
161    feature = "python",
162    pyo3::pyclass(module = "nautilus_trader.adapters.hyperliquid", from_py_object)
163)]
164#[cfg_attr(
165    feature = "python",
166    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.hyperliquid")
167)]
168pub struct HyperliquidExecutionClientConfig {
169    /// Account identifier for the execution client.
170    #[builder(default = AccountId::from("HYPERLIQUID-001"))]
171    pub account_id: AccountId,
172    /// Private key for signing transactions.
173    ///
174    /// If not provided, falls back to environment variable:
175    /// - Mainnet: `HYPERLIQUID_PK`
176    /// - Testnet: `HYPERLIQUID_TESTNET_PK`
177    pub private_key: Option<SecretString>,
178    /// Optional vault address for vault operations.
179    ///
180    /// If not provided, falls back to environment variable:
181    /// - Mainnet: `HYPERLIQUID_VAULT`
182    /// - Testnet: `HYPERLIQUID_TESTNET_VAULT`
183    pub vault_address: Option<String>,
184    /// Optional main account address when using an agent wallet (API sub-key).
185    /// When set, used for balance queries, position reports, and WS subscriptions
186    /// instead of the address derived from the private key.
187    ///
188    /// If not provided and no explicit vault address is set, falls back to
189    /// the `HYPERLIQUID_ACCOUNT_ADDRESS` environment variable.
190    pub account_address: Option<String>,
191    /// Override for the WebSocket URL.
192    pub base_url_ws: Option<String>,
193    /// Override for the HTTP info URL.
194    pub base_url_http: Option<String>,
195    /// Override for the exchange API URL.
196    pub base_url_exchange: Option<String>,
197    /// Optional proxy URL for HTTP and WebSocket transports.
198    pub proxy_url: Option<SecretString>,
199    /// The target environment (mainnet or testnet).
200    #[builder(default)]
201    pub environment: HyperliquidEnvironment,
202    /// HTTP timeout in seconds.
203    #[builder(default = 60)]
204    pub http_timeout_secs: u64,
205    /// Maximum number of retry attempts for HTTP requests.
206    #[builder(default = 3)]
207    pub max_retries: u32,
208    /// Initial retry delay in milliseconds.
209    #[builder(default = 100)]
210    pub retry_delay_initial_ms: u64,
211    /// Maximum retry delay in milliseconds.
212    #[builder(default = 5000)]
213    pub retry_delay_max_ms: u64,
214    /// When true, normalize order prices to 5 significant figures
215    /// before submission (Hyperliquid requirement).
216    #[builder(default = true)]
217    pub normalize_prices: bool,
218    /// Slippage buffer in basis points applied to MARKET orders and
219    /// stop-to-limit trigger derivations. Can be overridden per-order via
220    /// `SubmitOrder.params["market_order_slippage_bps"]`.
221    #[builder(default = 50)]
222    pub market_order_slippage_bps: u32,
223    /// If true, attach Nautilus builder attribution to eligible mainnet orders.
224    #[builder(default = true)]
225    pub include_builder_attribution: bool,
226    /// WebSocket transport backend (`Sockudo` by default; `Tungstenite` when
227    /// the `transport-sockudo` feature is disabled).
228    #[builder(default)]
229    pub transport_backend: TransportBackend,
230    /// Timeout in seconds for WebSocket post trading requests.
231    #[builder(default = 10)]
232    pub ws_post_timeout_secs: u64,
233    /// Poll interval in seconds for `outcomeMeta` settlement detection.
234    /// Disabled by default; venue `Settlement` fills drive HIP-4 settlement
235    /// through the standard user-fills stream. Set to a non-zero value only
236    /// when the venue fill stream is unavailable.
237    #[builder(default = 0)]
238    pub outcome_settlement_poll_secs: u64,
239}
240
241#[cfg(feature = "python")]
242nautilus_core::impl_pyo3_config_getters!(HyperliquidExecutionClientConfig {
243    account_id: AccountId,
244    vault_address: Option<String>,
245    account_address: Option<String>,
246    environment: HyperliquidEnvironment,
247    base_url_ws: Option<String>,
248    base_url_http: Option<String>,
249    base_url_exchange: Option<String>,
250    http_timeout_secs: u64,
251    max_retries: u32,
252    retry_delay_initial_ms: u64,
253    retry_delay_max_ms: u64,
254    normalize_prices: bool,
255    market_order_slippage_bps: u32,
256    include_builder_attribution: bool,
257    ws_post_timeout_secs: u64,
258    transport_backend: TransportBackend,
259});
260
261impl Default for HyperliquidExecutionClientConfig {
262    fn default() -> Self {
263        Self::builder().build()
264    }
265}
266
267impl HyperliquidExecutionClientConfig {
268    /// Returns `true` when private key is populated and non-empty.
269    #[must_use]
270    pub fn has_credentials(&self) -> bool {
271        self.private_key
272            .as_ref()
273            .map(SecretString::expose_secret)
274            .is_some_and(|s| !s.trim().is_empty())
275    }
276
277    /// Returns the WebSocket URL, respecting the environment and overrides.
278    #[must_use]
279    pub fn ws_url(&self) -> String {
280        self.base_url_ws
281            .clone()
282            .unwrap_or_else(|| ws_url(self.environment).to_string())
283    }
284
285    /// Returns the HTTP info URL, respecting the environment and overrides.
286    #[must_use]
287    pub fn http_url(&self) -> String {
288        self.base_url_http
289            .clone()
290            .unwrap_or_else(|| info_url(self.environment).to_string())
291    }
292}
293
294#[cfg(test)]
295mod tests {
296    use rstest::rstest;
297
298    use super::*;
299
300    #[rstest]
301    fn test_exec_config_default_account_address_is_none() {
302        let config = HyperliquidExecutionClientConfig::default();
303        assert!(config.account_address.is_none());
304    }
305
306    #[rstest]
307    fn test_exec_config_with_account_address() {
308        let config = HyperliquidExecutionClientConfig {
309            account_address: Some("0x1234".to_string()),
310            ..HyperliquidExecutionClientConfig::default()
311        };
312        assert_eq!(config.account_address.as_deref(), Some("0x1234"));
313    }
314
315    #[rstest]
316    fn test_data_config_toml_minimal() {
317        let config: HyperliquidDataClientConfig = toml::from_str(
318            r#"
319environment = "testnet"
320http_timeout_secs = 30
321update_instruments_interval_mins = 10
322transport_backend = "tungstenite"
323"#,
324        )
325        .unwrap();
326
327        assert_eq!(config.environment, HyperliquidEnvironment::Testnet);
328        assert_eq!(config.http_timeout_secs, 30);
329        assert_eq!(config.update_instruments_interval_mins, 10);
330        assert_eq!(config.transport_backend, TransportBackend::Tungstenite);
331        assert_eq!(config.stale_stream_receive_timeout_secs, 120);
332        assert_eq!(config.stream_health_check_interval_secs, 15);
333        assert_eq!(config.stale_stream_warning_cooldown_secs, 60);
334        assert!(!config.stale_stream_recovery_enabled);
335        assert_eq!(config.stale_stream_recovery_cooldown_secs, 120);
336        assert_eq!(config.stale_stream_max_targeted_resubscribes, 3);
337    }
338
339    #[rstest]
340    fn test_data_config_toml_stale_stream_settings() {
341        let config: HyperliquidDataClientConfig = toml::from_str(
342            "
343stale_stream_receive_timeout_secs = 30
344stream_health_check_interval_secs = 5
345stale_stream_warning_cooldown_secs = 20
346stale_stream_recovery_enabled = true
347stale_stream_recovery_cooldown_secs = 45
348stale_stream_max_targeted_resubscribes = 5
349",
350        )
351        .unwrap();
352
353        assert_eq!(config.stale_stream_receive_timeout_secs, 30);
354        assert_eq!(config.stream_health_check_interval_secs, 5);
355        assert_eq!(config.stale_stream_warning_cooldown_secs, 20);
356        assert!(config.stale_stream_recovery_enabled);
357        assert_eq!(config.stale_stream_recovery_cooldown_secs, 45);
358        assert_eq!(config.stale_stream_max_targeted_resubscribes, 5);
359    }
360
361    #[rstest]
362    fn test_exec_config_toml_empty_uses_defaults() {
363        let config: HyperliquidExecutionClientConfig = toml::from_str("").unwrap();
364        let expected = HyperliquidExecutionClientConfig::default();
365
366        assert_eq!(config.environment, expected.environment);
367        assert_eq!(config.http_timeout_secs, expected.http_timeout_secs);
368        assert_eq!(config.max_retries, expected.max_retries);
369        assert_eq!(config.normalize_prices, expected.normalize_prices);
370        assert_eq!(
371            config.market_order_slippage_bps,
372            expected.market_order_slippage_bps,
373        );
374        assert_eq!(
375            config.include_builder_attribution,
376            expected.include_builder_attribution,
377        );
378        assert_eq!(config.transport_backend, expected.transport_backend);
379        assert_eq!(config.ws_post_timeout_secs, expected.ws_post_timeout_secs);
380        assert_eq!(
381            config.outcome_settlement_poll_secs,
382            expected.outcome_settlement_poll_secs,
383        );
384    }
385
386    #[rstest]
387    fn test_exec_config_toml_include_builder_attribution_false() {
388        let config: HyperliquidExecutionClientConfig =
389            toml::from_str("include_builder_attribution = false").unwrap();
390
391        assert!(!config.include_builder_attribution);
392    }
393
394    #[rstest]
395    fn test_data_config_debug_redacts_private_key() {
396        let config = HyperliquidDataClientConfig {
397            private_key: Some(
398                "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".into(),
399            ),
400            ..HyperliquidDataClientConfig::default()
401        };
402        let debug = format!("{config:?}");
403
404        assert!(debug.contains(REDACTED));
405        assert!(!debug.contains("0123456789abcdef"));
406    }
407
408    #[rstest]
409    fn test_exec_config_debug_redacts_private_key() {
410        let config = HyperliquidExecutionClientConfig {
411            private_key: Some(
412                "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".into(),
413            ),
414            ..HyperliquidExecutionClientConfig::default()
415        };
416        let debug = format!("{config:?}");
417
418        assert!(debug.contains(REDACTED));
419        assert!(!debug.contains("0123456789abcdef"));
420    }
421}