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