1use std::fmt::Debug;
19
20use nautilus_model::identifiers::AccountId;
21use nautilus_network::websocket::TransportBackend;
22use serde::{Deserialize, Serialize};
23
24use crate::common::{
25 consts::{info_url, ws_url},
26 enums::HyperliquidEnvironment,
27};
28
29#[derive(Clone, Serialize, Deserialize, bon::Builder)]
38#[serde(default, deny_unknown_fields)]
39#[cfg_attr(
40 feature = "python",
41 pyo3::pyclass(module = "nautilus_trader.adapters.hyperliquid", from_py_object)
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 pub private_key: Option<String>,
50 pub base_url_ws: Option<String>,
52 pub base_url_http: Option<String>,
54 pub proxy_url: Option<String>,
56 #[builder(default)]
58 pub environment: HyperliquidEnvironment,
59 #[builder(default = 60)]
61 pub http_timeout_secs: u64,
62 #[builder(default = 30)]
64 pub ws_timeout_secs: u64,
65 #[builder(default = 120)]
69 pub stale_stream_receive_timeout_secs: u64,
70 #[builder(default = 15)]
73 pub stream_health_check_interval_secs: u64,
74 #[builder(default = 60)]
76 pub stale_stream_warning_cooldown_secs: u64,
77 #[builder(default = false)]
80 pub stale_stream_recovery_enabled: bool,
81 #[builder(default = 120)]
84 pub stale_stream_recovery_cooldown_secs: u64,
85 #[builder(default = 3)]
88 pub stale_stream_max_targeted_resubscribes: u32,
89 #[builder(default = 60)]
91 pub update_instruments_interval_mins: u64,
92 #[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 #[must_use]
124 pub fn new() -> Self {
125 Self::default()
126 }
127
128 #[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 #[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 #[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
153impl Debug for HyperliquidDataClientConfig {
154 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155 formatter
156 .debug_struct(stringify!(HyperliquidDataClientConfig))
157 .field(
158 "private_key",
159 &self.private_key.as_ref().map(|_| "[REDACTED]"),
160 )
161 .field("base_url_ws", &self.base_url_ws)
162 .field("base_url_http", &self.base_url_http)
163 .field("proxy_url", &self.proxy_url)
164 .field("environment", &self.environment)
165 .field("http_timeout_secs", &self.http_timeout_secs)
166 .field("ws_timeout_secs", &self.ws_timeout_secs)
167 .field(
168 "stale_stream_receive_timeout_secs",
169 &self.stale_stream_receive_timeout_secs,
170 )
171 .field(
172 "stream_health_check_interval_secs",
173 &self.stream_health_check_interval_secs,
174 )
175 .field(
176 "stale_stream_warning_cooldown_secs",
177 &self.stale_stream_warning_cooldown_secs,
178 )
179 .field(
180 "stale_stream_recovery_enabled",
181 &self.stale_stream_recovery_enabled,
182 )
183 .field(
184 "stale_stream_recovery_cooldown_secs",
185 &self.stale_stream_recovery_cooldown_secs,
186 )
187 .field(
188 "stale_stream_max_targeted_resubscribes",
189 &self.stale_stream_max_targeted_resubscribes,
190 )
191 .field(
192 "update_instruments_interval_mins",
193 &self.update_instruments_interval_mins,
194 )
195 .field("transport_backend", &self.transport_backend)
196 .finish()
197 }
198}
199
200#[derive(Clone, Serialize, Deserialize, bon::Builder)]
202#[serde(default, deny_unknown_fields)]
203#[cfg_attr(
204 feature = "python",
205 pyo3::pyclass(module = "nautilus_trader.adapters.hyperliquid", from_py_object)
206)]
207#[cfg_attr(
208 feature = "python",
209 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.hyperliquid")
210)]
211pub struct HyperliquidExecutionClientConfig {
212 #[builder(default = AccountId::from("HYPERLIQUID-001"))]
214 pub account_id: AccountId,
215 pub private_key: Option<String>,
221 pub vault_address: Option<String>,
227 pub account_address: Option<String>,
234 pub base_url_ws: Option<String>,
236 pub base_url_http: Option<String>,
238 pub base_url_exchange: Option<String>,
240 pub proxy_url: Option<String>,
242 #[builder(default)]
244 pub environment: HyperliquidEnvironment,
245 #[builder(default = 60)]
247 pub http_timeout_secs: u64,
248 #[builder(default = 3)]
250 pub max_retries: u32,
251 #[builder(default = 100)]
253 pub retry_delay_initial_ms: u64,
254 #[builder(default = 5000)]
256 pub retry_delay_max_ms: u64,
257 #[builder(default = true)]
260 pub normalize_prices: bool,
261 #[builder(default = 50)]
265 pub market_order_slippage_bps: u32,
266 #[builder(default = true)]
268 pub include_builder_attribution: bool,
269 #[builder(default)]
272 pub transport_backend: TransportBackend,
273 #[builder(default = 10)]
275 pub ws_post_timeout_secs: u64,
276 #[builder(default = 0)]
281 pub outcome_settlement_poll_secs: u64,
282}
283
284#[cfg(feature = "python")]
285nautilus_core::impl_pyo3_config_getters!(HyperliquidExecutionClientConfig {
286 account_id: AccountId,
287 vault_address: Option<String>,
288 account_address: Option<String>,
289 environment: HyperliquidEnvironment,
290 base_url_ws: Option<String>,
291 base_url_http: Option<String>,
292 base_url_exchange: Option<String>,
293 http_timeout_secs: u64,
294 max_retries: u32,
295 retry_delay_initial_ms: u64,
296 retry_delay_max_ms: u64,
297 normalize_prices: bool,
298 market_order_slippage_bps: u32,
299 include_builder_attribution: bool,
300 ws_post_timeout_secs: u64,
301 transport_backend: TransportBackend,
302});
303
304impl Default for HyperliquidExecutionClientConfig {
305 fn default() -> Self {
306 Self::builder().build()
307 }
308}
309
310impl HyperliquidExecutionClientConfig {
311 #[must_use]
313 pub fn has_credentials(&self) -> bool {
314 self.private_key
315 .as_deref()
316 .is_some_and(|s| !s.trim().is_empty())
317 }
318
319 #[must_use]
321 pub fn ws_url(&self) -> String {
322 self.base_url_ws
323 .clone()
324 .unwrap_or_else(|| ws_url(self.environment).to_string())
325 }
326
327 #[must_use]
329 pub fn http_url(&self) -> String {
330 self.base_url_http
331 .clone()
332 .unwrap_or_else(|| info_url(self.environment).to_string())
333 }
334}
335
336impl Debug for HyperliquidExecutionClientConfig {
337 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
338 formatter
339 .debug_struct(stringify!(HyperliquidExecutionClientConfig))
340 .field("account_id", &self.account_id)
341 .field(
342 "private_key",
343 &self.private_key.as_ref().map(|_| "[REDACTED]"),
344 )
345 .field("vault_address", &self.vault_address)
346 .field("account_address", &self.account_address)
347 .field("base_url_ws", &self.base_url_ws)
348 .field("base_url_http", &self.base_url_http)
349 .field("base_url_exchange", &self.base_url_exchange)
350 .field("proxy_url", &self.proxy_url)
351 .field("environment", &self.environment)
352 .field("http_timeout_secs", &self.http_timeout_secs)
353 .field("max_retries", &self.max_retries)
354 .field("retry_delay_initial_ms", &self.retry_delay_initial_ms)
355 .field("retry_delay_max_ms", &self.retry_delay_max_ms)
356 .field("normalize_prices", &self.normalize_prices)
357 .field("market_order_slippage_bps", &self.market_order_slippage_bps)
358 .field(
359 "include_builder_attribution",
360 &self.include_builder_attribution,
361 )
362 .field("transport_backend", &self.transport_backend)
363 .field("ws_post_timeout_secs", &self.ws_post_timeout_secs)
364 .field(
365 "outcome_settlement_poll_secs",
366 &self.outcome_settlement_poll_secs,
367 )
368 .finish()
369 }
370}
371
372#[cfg(test)]
373mod tests {
374 use rstest::rstest;
375
376 use super::*;
377
378 #[rstest]
379 fn test_exec_config_default_account_address_is_none() {
380 let config = HyperliquidExecutionClientConfig::default();
381 assert!(config.account_address.is_none());
382 }
383
384 #[rstest]
385 fn test_exec_config_with_account_address() {
386 let config = HyperliquidExecutionClientConfig {
387 account_address: Some("0x1234".to_string()),
388 ..HyperliquidExecutionClientConfig::default()
389 };
390 assert_eq!(config.account_address.as_deref(), Some("0x1234"));
391 }
392
393 #[rstest]
394 fn test_data_config_toml_minimal() {
395 let config: HyperliquidDataClientConfig = toml::from_str(
396 r#"
397environment = "testnet"
398http_timeout_secs = 30
399update_instruments_interval_mins = 10
400transport_backend = "tungstenite"
401"#,
402 )
403 .unwrap();
404
405 assert_eq!(config.environment, HyperliquidEnvironment::Testnet);
406 assert_eq!(config.http_timeout_secs, 30);
407 assert_eq!(config.update_instruments_interval_mins, 10);
408 assert_eq!(config.transport_backend, TransportBackend::Tungstenite);
409 assert_eq!(config.stale_stream_receive_timeout_secs, 120);
410 assert_eq!(config.stream_health_check_interval_secs, 15);
411 assert_eq!(config.stale_stream_warning_cooldown_secs, 60);
412 assert!(!config.stale_stream_recovery_enabled);
413 assert_eq!(config.stale_stream_recovery_cooldown_secs, 120);
414 assert_eq!(config.stale_stream_max_targeted_resubscribes, 3);
415 }
416
417 #[rstest]
418 fn test_data_config_toml_stale_stream_settings() {
419 let config: HyperliquidDataClientConfig = toml::from_str(
420 "
421stale_stream_receive_timeout_secs = 30
422stream_health_check_interval_secs = 5
423stale_stream_warning_cooldown_secs = 20
424stale_stream_recovery_enabled = true
425stale_stream_recovery_cooldown_secs = 45
426stale_stream_max_targeted_resubscribes = 5
427",
428 )
429 .unwrap();
430
431 assert_eq!(config.stale_stream_receive_timeout_secs, 30);
432 assert_eq!(config.stream_health_check_interval_secs, 5);
433 assert_eq!(config.stale_stream_warning_cooldown_secs, 20);
434 assert!(config.stale_stream_recovery_enabled);
435 assert_eq!(config.stale_stream_recovery_cooldown_secs, 45);
436 assert_eq!(config.stale_stream_max_targeted_resubscribes, 5);
437 }
438
439 #[rstest]
440 fn test_exec_config_toml_empty_uses_defaults() {
441 let config: HyperliquidExecutionClientConfig = toml::from_str("").unwrap();
442 let expected = HyperliquidExecutionClientConfig::default();
443
444 assert_eq!(config.environment, expected.environment);
445 assert_eq!(config.http_timeout_secs, expected.http_timeout_secs);
446 assert_eq!(config.max_retries, expected.max_retries);
447 assert_eq!(config.normalize_prices, expected.normalize_prices);
448 assert_eq!(
449 config.market_order_slippage_bps,
450 expected.market_order_slippage_bps,
451 );
452 assert_eq!(
453 config.include_builder_attribution,
454 expected.include_builder_attribution,
455 );
456 assert_eq!(config.transport_backend, expected.transport_backend);
457 assert_eq!(config.ws_post_timeout_secs, expected.ws_post_timeout_secs);
458 assert_eq!(
459 config.outcome_settlement_poll_secs,
460 expected.outcome_settlement_poll_secs,
461 );
462 }
463
464 #[rstest]
465 fn test_exec_config_toml_include_builder_attribution_false() {
466 let config: HyperliquidExecutionClientConfig =
467 toml::from_str("include_builder_attribution = false").unwrap();
468
469 assert!(!config.include_builder_attribution);
470 }
471
472 #[rstest]
473 fn test_data_config_debug_redacts_private_key() {
474 let config = HyperliquidDataClientConfig {
475 private_key: Some(
476 "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
477 ),
478 ..HyperliquidDataClientConfig::default()
479 };
480 let debug = format!("{config:?}");
481
482 assert!(debug.contains("[REDACTED]"));
483 assert!(!debug.contains("0123456789abcdef"));
484 }
485
486 #[rstest]
487 fn test_exec_config_debug_redacts_private_key() {
488 let config = HyperliquidExecutionClientConfig {
489 private_key: Some(
490 "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
491 ),
492 ..HyperliquidExecutionClientConfig::default()
493 };
494 let debug = format!("{config:?}");
495
496 assert!(debug.contains("[REDACTED]"));
497 assert!(!debug.contains("0123456789abcdef"));
498 }
499}