1use std::collections::HashMap;
22
23use nautilus_core::UnixNanos;
24use nautilus_model::{
25 enums::{AggressorSide, OrderSide},
26 identifiers::InstrumentId,
27 types::{Price, Quantity},
28};
29use nautilus_persistence_macros::custom_data;
30use rust_decimal::Decimal;
31use serde::{Deserialize, Serialize};
32
33use crate::common::enums::HyperliquidTwapStatus;
34
35#[cfg_attr(
37 feature = "arrow",
38 custom_data(pyo3, stub_module = "nautilus_trader.adapters.hyperliquid")
39)]
40#[cfg_attr(
41 not(feature = "arrow"),
42 custom_data(pyo3, no_arrow, stub_module = "nautilus_trader.adapters.hyperliquid")
43)]
44pub struct HyperliquidAllMids {
45 #[custom_data_field(serde)]
47 pub mids: HashMap<InstrumentId, Price>,
48 pub ts_event: UnixNanos,
50 pub ts_init: UnixNanos,
52}
53
54#[cfg_attr(
59 feature = "arrow",
60 custom_data(pyo3, stub_module = "nautilus_trader.adapters.hyperliquid")
61)]
62#[cfg_attr(
63 not(feature = "arrow"),
64 custom_data(pyo3, no_arrow, stub_module = "nautilus_trader.adapters.hyperliquid")
65)]
66pub struct HyperliquidOpenInterest {
67 pub instrument_id: InstrumentId,
69 #[custom_data_field(serde)]
71 pub open_interest: Decimal,
72 pub ts_event: UnixNanos,
74 pub ts_init: UnixNanos,
76}
77
78#[cfg_attr(
84 feature = "arrow",
85 custom_data(pyo3, stub_module = "nautilus_trader.adapters.hyperliquid")
86)]
87#[cfg_attr(
88 not(feature = "arrow"),
89 custom_data(pyo3, no_arrow, stub_module = "nautilus_trader.adapters.hyperliquid")
90)]
91pub struct HyperliquidPublicTrade {
92 pub instrument_id: InstrumentId,
94 #[custom_data_field(serde)]
96 pub price: Price,
97 #[custom_data_field(serde)]
99 pub size: Quantity,
100 #[custom_data_field(serde)]
102 pub aggressor_side: AggressorSide,
103 pub trade_id: String,
105 pub buyer: String,
107 pub seller: String,
109 pub hash: String,
111 pub ts_event: UnixNanos,
113 pub ts_init: UnixNanos,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
119pub struct HyperliquidImpactPrices {
120 pub bid: Price,
122 pub ask: Price,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
128pub struct HyperliquidDexAssetCtx {
129 pub dex: String,
131 pub instrument_id: InstrumentId,
133 pub mark_price: Price,
135 pub oracle_price: Price,
137 pub prev_day_price: Price,
139 pub mid_price: Option<Price>,
141 pub impact_prices: Option<HyperliquidImpactPrices>,
143 pub funding_rate: Decimal,
145 pub open_interest: Decimal,
147 pub premium: Option<Decimal>,
149 pub day_ntl_volume: Decimal,
151 pub day_base_volume: Decimal,
153}
154
155#[custom_data(pyo3, no_arrow, stub_module = "nautilus_trader.adapters.hyperliquid")]
159pub struct HyperliquidAllDexsAssetCtxs {
160 #[custom_data_field(serde)]
162 pub entries: Vec<HyperliquidDexAssetCtx>,
163 pub ts_event: UnixNanos,
165 pub ts_init: UnixNanos,
167}
168
169#[cfg_attr(
173 feature = "arrow",
174 custom_data(pyo3, stub_module = "nautilus_trader.adapters.hyperliquid")
175)]
176#[cfg_attr(
177 not(feature = "arrow"),
178 custom_data(pyo3, no_arrow, stub_module = "nautilus_trader.adapters.hyperliquid")
179)]
180pub struct HyperliquidTwapHistory {
181 pub user: String,
183 #[custom_data_field(serde)]
185 pub twap_id: Option<u64>,
186 pub coin: String,
188 #[custom_data_field(serde)]
190 pub instrument_id: Option<InstrumentId>,
191 #[custom_data_field(serde)]
193 pub side: OrderSide,
194 #[custom_data_field(serde)]
196 pub size: Decimal,
197 #[custom_data_field(serde)]
199 pub executed_size: Decimal,
200 #[custom_data_field(serde)]
202 pub executed_notional: Decimal,
203 pub minutes: u32,
205 pub reduce_only: bool,
207 pub randomize: bool,
209 #[custom_data_field(serde)]
211 pub status: HyperliquidTwapStatus,
212 pub status_description: String,
214 pub state_timestamp: UnixNanos,
216 pub is_snapshot: bool,
218 pub ts_event: UnixNanos,
220 pub ts_init: UnixNanos,
222}
223
224#[cfg_attr(
228 feature = "arrow",
229 custom_data(pyo3, stub_module = "nautilus_trader.adapters.hyperliquid")
230)]
231#[cfg_attr(
232 not(feature = "arrow"),
233 custom_data(pyo3, no_arrow, stub_module = "nautilus_trader.adapters.hyperliquid")
234)]
235pub struct HyperliquidTwapSliceFill {
236 pub user: String,
238 pub twap_id: u64,
240 pub coin: String,
242 #[custom_data_field(serde)]
244 pub instrument_id: Option<InstrumentId>,
245 #[custom_data_field(serde)]
247 pub price: Decimal,
248 #[custom_data_field(serde)]
250 pub size: Decimal,
251 #[custom_data_field(serde)]
253 pub side: OrderSide,
254 pub hash: String,
256 pub oid: u64,
258 pub tid: u64,
260 pub crossed: bool,
262 #[custom_data_field(serde)]
264 pub fee: Decimal,
265 pub fee_token: String,
267 pub dir: String,
269 #[custom_data_field(serde)]
271 pub closed_pnl: Decimal,
272 pub is_snapshot: bool,
274 pub ts_event: UnixNanos,
276 pub ts_init: UnixNanos,
278}
279
280pub fn register_hyperliquid_custom_data() {
284 #[cfg(feature = "arrow")]
285 {
286 nautilus_serialization::ensure_custom_data_registered::<HyperliquidAllMids>();
287 nautilus_serialization::ensure_custom_data_registered::<HyperliquidOpenInterest>();
288 nautilus_serialization::ensure_custom_data_registered::<HyperliquidPublicTrade>();
289 nautilus_serialization::ensure_custom_data_registered::<HyperliquidTwapHistory>();
290 nautilus_serialization::ensure_custom_data_registered::<HyperliquidTwapSliceFill>();
291 }
292
293 #[cfg(not(feature = "arrow"))]
294 {
295 let _ = nautilus_model::data::ensure_custom_data_json_registered::<HyperliquidAllMids>();
296 let _ =
297 nautilus_model::data::ensure_custom_data_json_registered::<HyperliquidOpenInterest>();
298 let _ =
299 nautilus_model::data::ensure_custom_data_json_registered::<HyperliquidPublicTrade>();
300 let _ =
301 nautilus_model::data::ensure_custom_data_json_registered::<HyperliquidTwapHistory>();
302 let _ =
303 nautilus_model::data::ensure_custom_data_json_registered::<HyperliquidTwapSliceFill>();
304 }
305
306 let _ =
307 nautilus_model::data::ensure_custom_data_json_registered::<HyperliquidAllDexsAssetCtxs>();
308}
309
310#[cfg(test)]
311mod tests {
312 use rstest::rstest;
313
314 use super::*;
315
316 #[rstest]
317 fn test_register_hyperliquid_custom_data_is_idempotent() {
318 register_hyperliquid_custom_data();
319 register_hyperliquid_custom_data();
320 }
321
322 #[cfg(feature = "arrow")]
323 #[rstest]
324 fn test_hyperliquid_all_mids_arrow_schema() {
325 use arrow::datatypes::DataType;
326 use nautilus_serialization::arrow::ArrowSchemaProvider;
327
328 let schema = HyperliquidAllMids::get_schema(None);
329
330 assert_eq!(schema.fields().len(), 3);
331 assert_eq!(schema.field(0).name(), "mids");
332 assert_eq!(schema.field(0).data_type(), &DataType::Utf8);
333 assert_eq!(schema.field(1).name(), "ts_event");
334 assert_eq!(schema.field(1).data_type(), &DataType::UInt64);
335 assert_eq!(schema.field(2).name(), "ts_init");
336 assert_eq!(schema.field(2).data_type(), &DataType::UInt64);
337 }
338
339 #[cfg(feature = "arrow")]
340 #[rstest]
341 fn test_hyperliquid_open_interest_arrow_schema() {
342 use arrow::datatypes::DataType;
343 use nautilus_serialization::arrow::ArrowSchemaProvider;
344
345 let schema = HyperliquidOpenInterest::get_schema(None);
346
347 assert_eq!(schema.fields().len(), 4);
348 assert_eq!(schema.field(0).name(), "instrument_id");
349 assert!(matches!(
350 schema.field(0).data_type(),
351 DataType::Utf8 | DataType::Utf8View
352 ));
353 assert_eq!(schema.field(1).name(), "open_interest");
354 assert!(matches!(
355 schema.field(1).data_type(),
356 DataType::Utf8 | DataType::Utf8View
357 ));
358 assert_eq!(schema.field(2).name(), "ts_event");
359 assert_eq!(schema.field(2).data_type(), &DataType::UInt64);
360 assert_eq!(schema.field(3).name(), "ts_init");
361 assert_eq!(schema.field(3).data_type(), &DataType::UInt64);
362 }
363
364 #[cfg(feature = "arrow")]
365 #[rstest]
366 fn test_hyperliquid_open_interest_arrow_round_trip_preserves_decimal() {
367 use std::str::FromStr;
368
369 use nautilus_model::data::Data;
370 use nautilus_serialization::arrow::{DecodeDataFromRecordBatch, EncodeToRecordBatch};
371
372 let original = HyperliquidOpenInterest::new(
373 InstrumentId::from("BTC-USD-PERP.HYPERLIQUID"),
374 Decimal::from_str("123456.789012345678").unwrap(),
375 UnixNanos::from(1),
376 UnixNanos::from(2),
377 );
378 let metadata = EncodeToRecordBatch::metadata(&original);
379 let batch =
380 HyperliquidOpenInterest::encode_batch(&metadata, std::slice::from_ref(&original))
381 .unwrap();
382 let decoded = HyperliquidOpenInterest::decode_data_batch(&metadata, batch).unwrap();
383
384 assert_eq!(decoded.len(), 1);
385 match &decoded[0] {
386 Data::Custom(custom) => {
387 let open_interest = custom
388 .data
389 .as_any()
390 .downcast_ref::<HyperliquidOpenInterest>()
391 .expect("expected HyperliquidOpenInterest");
392 assert_eq!(open_interest.instrument_id, original.instrument_id);
393 assert_eq!(open_interest.open_interest, original.open_interest);
394 assert_eq!(open_interest.ts_event, original.ts_event);
395 assert_eq!(open_interest.ts_init, original.ts_init);
396 }
397 other => panic!("Expected Data::Custom, was {other:?}"),
398 }
399 }
400
401 #[cfg(feature = "arrow")]
402 #[rstest]
403 fn test_hyperliquid_public_trade_arrow_round_trip_preserves_counterparties() {
404 use nautilus_model::{
405 data::Data,
406 enums::AggressorSide,
407 types::{Price, Quantity},
408 };
409 use nautilus_serialization::arrow::{DecodeDataFromRecordBatch, EncodeToRecordBatch};
410
411 let original = HyperliquidPublicTrade::new(
412 InstrumentId::from("BTC-USD-PERP.HYPERLIQUID"),
413 Price::from("100000.50"),
414 Quantity::from("0.123"),
415 AggressorSide::Buy,
416 "123456".to_string(),
417 "0xbuyer".to_string(),
418 "0xseller".to_string(),
419 "0xhash".to_string(),
420 UnixNanos::from(1),
421 UnixNanos::from(2),
422 );
423 let metadata = EncodeToRecordBatch::metadata(&original);
424 let batch =
425 HyperliquidPublicTrade::encode_batch(&metadata, std::slice::from_ref(&original))
426 .unwrap();
427 let decoded = HyperliquidPublicTrade::decode_data_batch(&metadata, batch).unwrap();
428
429 let Data::Custom(custom) = &decoded[0] else {
430 panic!("Expected Data::Custom");
431 };
432 let trade = custom
433 .data
434 .as_any()
435 .downcast_ref::<HyperliquidPublicTrade>()
436 .expect("expected HyperliquidPublicTrade");
437 assert_eq!(trade.buyer, original.buyer);
438 assert_eq!(trade.seller, original.seller);
439 assert_eq!(trade.hash, original.hash);
440 assert_eq!(trade.price, original.price);
441 assert_eq!(trade.size, original.size);
442 }
443
444 #[cfg(feature = "arrow")]
445 #[rstest]
446 fn test_hyperliquid_twap_history_arrow_schema() {
447 use arrow::datatypes::DataType;
448 use nautilus_serialization::arrow::ArrowSchemaProvider;
449
450 let schema = HyperliquidTwapHistory::get_schema(None);
451 let names: Vec<_> = schema.fields().iter().map(|f| f.name().as_str()).collect();
452
453 assert!(names.contains(&"user"));
455 assert!(names.contains(&"twap_id"));
456 assert!(names.contains(&"instrument_id"));
457 assert!(names.contains(&"size"));
458 assert!(names.contains(&"executed_notional"));
459 assert!(names.contains(&"status"));
460 assert!(names.contains(&"is_snapshot"));
461 assert!(names.contains(&"ts_event"));
462 assert!(names.contains(&"ts_init"));
463
464 for name in ["twap_id", "size", "status"] {
465 assert!(matches!(
466 schema.field_with_name(name).unwrap().data_type(),
467 DataType::Utf8 | DataType::Utf8View
468 ));
469 }
470 assert_eq!(
471 schema.field_with_name("ts_init").unwrap().data_type(),
472 &DataType::UInt64
473 );
474 }
475
476 #[cfg(feature = "arrow")]
477 #[rstest]
478 fn test_hyperliquid_twap_history_arrow_round_trip_preserves_fields() {
479 use std::str::FromStr;
480
481 use nautilus_model::{data::Data, enums::OrderSide};
482 use nautilus_serialization::arrow::{DecodeDataFromRecordBatch, EncodeToRecordBatch};
483
484 let with_options = HyperliquidTwapHistory::new(
485 "0xuser".to_string(),
486 Some(7),
487 "BTC".to_string(),
488 Some(InstrumentId::from("BTC-USD-PERP.HYPERLIQUID")),
489 OrderSide::Buy,
490 Decimal::from_str("1.25").unwrap(),
491 Decimal::from_str("0.5").unwrap(),
492 Decimal::from_str("50000.123456789").unwrap(),
493 15,
494 false,
495 true,
496 HyperliquidTwapStatus::Finished,
497 "finished".to_string(),
498 UnixNanos::from(10),
499 true,
500 UnixNanos::from(20),
501 UnixNanos::from(30),
502 );
503 let without_options = HyperliquidTwapHistory::new(
504 "0xuser2".to_string(),
505 None,
506 "UNK".to_string(),
507 None,
508 OrderSide::Sell,
509 Decimal::from_str("2").unwrap(),
510 Decimal::ZERO,
511 Decimal::ZERO,
512 60,
513 true,
514 false,
515 HyperliquidTwapStatus::Activated,
516 "activated".to_string(),
517 UnixNanos::from(40),
518 false,
519 UnixNanos::from(50),
520 UnixNanos::from(60),
521 );
522
523 for original in [&with_options, &without_options] {
524 let metadata = EncodeToRecordBatch::metadata(original);
525 let batch =
526 HyperliquidTwapHistory::encode_batch(&metadata, std::slice::from_ref(original))
527 .unwrap();
528 let decoded = HyperliquidTwapHistory::decode_data_batch(&metadata, batch).unwrap();
529
530 let Data::Custom(custom) = &decoded[0] else {
531 panic!("Expected Data::Custom");
532 };
533 let history = custom
534 .data
535 .as_any()
536 .downcast_ref::<HyperliquidTwapHistory>()
537 .expect("expected HyperliquidTwapHistory");
538 assert_eq!(history, original);
539 }
540 }
541
542 #[cfg(feature = "arrow")]
543 #[rstest]
544 fn test_hyperliquid_twap_slice_fill_arrow_round_trip_preserves_decimals() {
545 use std::str::FromStr;
546
547 use nautilus_model::{data::Data, enums::OrderSide};
548 use nautilus_serialization::arrow::{DecodeDataFromRecordBatch, EncodeToRecordBatch};
549
550 let original = HyperliquidTwapSliceFill::new(
551 "0xslice".to_string(),
552 99,
553 "ETH".to_string(),
554 Some(InstrumentId::from("ETH-USD-PERP.HYPERLIQUID")),
555 Decimal::from_str("3456.789012345678").unwrap(),
556 Decimal::from_str("0.001").unwrap(),
557 OrderSide::Buy,
558 "0xhash".to_string(),
559 111,
560 222,
561 true,
562 Decimal::from_str("-0.0001").unwrap(),
563 "USDC".to_string(),
564 "Open Long".to_string(),
565 Decimal::from_str("1.23").unwrap(),
566 false,
567 UnixNanos::from(1),
568 UnixNanos::from(2),
569 );
570 let metadata = EncodeToRecordBatch::metadata(&original);
571 let batch =
572 HyperliquidTwapSliceFill::encode_batch(&metadata, std::slice::from_ref(&original))
573 .unwrap();
574 let decoded = HyperliquidTwapSliceFill::decode_data_batch(&metadata, batch).unwrap();
575
576 let Data::Custom(custom) = &decoded[0] else {
577 panic!("Expected Data::Custom");
578 };
579 let fill = custom
580 .data
581 .as_any()
582 .downcast_ref::<HyperliquidTwapSliceFill>()
583 .expect("expected HyperliquidTwapSliceFill");
584 assert_eq!(fill, &original);
585 assert_eq!(fill.price, original.price);
586 assert_eq!(fill.fee, original.fee);
587 assert_eq!(fill.closed_pnl, original.closed_pnl);
588 }
589}