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