1use nautilus_network::websocket::TransportBackend;
19use serde::{Deserialize, Serialize};
20
21use crate::common::{
22 consts::{info_url, ws_url},
23 enums::HyperliquidEnvironment,
24};
25
26#[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 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
153#[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 pub private_key: Option<String>,
174 pub vault_address: Option<String>,
180 pub account_address: Option<String>,
187 pub base_url_ws: Option<String>,
189 pub base_url_http: Option<String>,
191 pub base_url_exchange: Option<String>,
193 pub proxy_url: Option<String>,
195 #[builder(default)]
197 pub environment: HyperliquidEnvironment,
198 #[builder(default = 60)]
200 pub http_timeout_secs: u64,
201 #[builder(default = 3)]
203 pub max_retries: u32,
204 #[builder(default = 100)]
206 pub retry_delay_initial_ms: u64,
207 #[builder(default = 5000)]
209 pub retry_delay_max_ms: u64,
210 #[builder(default = true)]
213 pub normalize_prices: bool,
214 #[builder(default = 50)]
218 pub market_order_slippage_bps: u32,
219 #[builder(default = true)]
221 pub include_builder_attribution: bool,
222 #[builder(default)]
225 pub transport_backend: TransportBackend,
226 #[builder(default = 10)]
228 pub ws_post_timeout_secs: u64,
229 #[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 #[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 #[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 #[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}