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