1use std::collections::HashMap;
22
23use nautilus_core::UnixNanos;
24use nautilus_model::{
25 enums::AggressorSide,
26 identifiers::InstrumentId,
27 types::{Price, Quantity},
28};
29use nautilus_persistence_macros::custom_data;
30use rust_decimal::Decimal;
31use serde::{Deserialize, Serialize};
32
33#[cfg_attr(
35 feature = "arrow",
36 custom_data(pyo3, stub_module = "nautilus_trader.adapters.hyperliquid")
37)]
38#[cfg_attr(
39 not(feature = "arrow"),
40 custom_data(pyo3, no_arrow, stub_module = "nautilus_trader.adapters.hyperliquid")
41)]
42pub struct HyperliquidAllMids {
43 #[custom_data_field(serde)]
45 pub mids: HashMap<InstrumentId, Price>,
46 pub ts_event: UnixNanos,
48 pub ts_init: UnixNanos,
50}
51
52#[cfg_attr(
57 feature = "arrow",
58 custom_data(pyo3, stub_module = "nautilus_trader.adapters.hyperliquid")
59)]
60#[cfg_attr(
61 not(feature = "arrow"),
62 custom_data(pyo3, no_arrow, stub_module = "nautilus_trader.adapters.hyperliquid")
63)]
64pub struct HyperliquidOpenInterest {
65 pub instrument_id: InstrumentId,
67 #[custom_data_field(serde)]
69 pub open_interest: Decimal,
70 pub ts_event: UnixNanos,
72 pub ts_init: UnixNanos,
74}
75
76#[cfg_attr(
82 feature = "arrow",
83 custom_data(pyo3, stub_module = "nautilus_trader.adapters.hyperliquid")
84)]
85#[cfg_attr(
86 not(feature = "arrow"),
87 custom_data(pyo3, no_arrow, stub_module = "nautilus_trader.adapters.hyperliquid")
88)]
89pub struct HyperliquidPublicTrade {
90 pub instrument_id: InstrumentId,
92 #[custom_data_field(serde)]
94 pub price: Price,
95 #[custom_data_field(serde)]
97 pub size: Quantity,
98 #[custom_data_field(serde)]
100 pub aggressor_side: AggressorSide,
101 pub trade_id: String,
103 pub buyer: String,
105 pub seller: String,
107 pub hash: String,
109 pub ts_event: UnixNanos,
111 pub ts_init: UnixNanos,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
117pub struct HyperliquidImpactPrices {
118 pub bid: Price,
120 pub ask: Price,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
126pub struct HyperliquidDexAssetCtx {
127 pub dex: String,
129 pub instrument_id: InstrumentId,
131 pub mark_price: Price,
133 pub oracle_price: Price,
135 pub prev_day_price: Price,
137 pub mid_price: Option<Price>,
139 pub impact_prices: Option<HyperliquidImpactPrices>,
141 pub funding_rate: Decimal,
143 pub open_interest: Decimal,
145 pub premium: Option<Decimal>,
147 pub day_ntl_volume: Decimal,
149 pub day_base_volume: Decimal,
151}
152
153#[custom_data(pyo3, no_arrow, stub_module = "nautilus_trader.adapters.hyperliquid")]
157pub struct HyperliquidAllDexsAssetCtxs {
158 #[custom_data_field(serde)]
160 pub entries: Vec<HyperliquidDexAssetCtx>,
161 pub ts_event: UnixNanos,
163 pub ts_init: UnixNanos,
165}
166
167pub fn register_hyperliquid_custom_data() {
171 #[cfg(feature = "arrow")]
172 {
173 nautilus_serialization::ensure_custom_data_registered::<HyperliquidAllMids>();
174 nautilus_serialization::ensure_custom_data_registered::<HyperliquidOpenInterest>();
175 nautilus_serialization::ensure_custom_data_registered::<HyperliquidPublicTrade>();
176 }
177
178 #[cfg(not(feature = "arrow"))]
179 {
180 let _ = nautilus_model::data::ensure_custom_data_json_registered::<HyperliquidAllMids>();
181 let _ =
182 nautilus_model::data::ensure_custom_data_json_registered::<HyperliquidOpenInterest>();
183 let _ =
184 nautilus_model::data::ensure_custom_data_json_registered::<HyperliquidPublicTrade>();
185 }
186
187 let _ =
188 nautilus_model::data::ensure_custom_data_json_registered::<HyperliquidAllDexsAssetCtxs>();
189}
190
191#[cfg(test)]
192mod tests {
193 use rstest::rstest;
194
195 use super::*;
196
197 #[rstest]
198 fn test_register_hyperliquid_custom_data_is_idempotent() {
199 register_hyperliquid_custom_data();
200 register_hyperliquid_custom_data();
201 }
202
203 #[cfg(feature = "arrow")]
204 #[rstest]
205 fn test_hyperliquid_all_mids_arrow_schema() {
206 use arrow::datatypes::DataType;
207 use nautilus_serialization::arrow::ArrowSchemaProvider;
208
209 let schema = HyperliquidAllMids::get_schema(None);
210
211 assert_eq!(schema.fields().len(), 3);
212 assert_eq!(schema.field(0).name(), "mids");
213 assert_eq!(schema.field(0).data_type(), &DataType::Utf8);
214 assert_eq!(schema.field(1).name(), "ts_event");
215 assert_eq!(schema.field(1).data_type(), &DataType::UInt64);
216 assert_eq!(schema.field(2).name(), "ts_init");
217 assert_eq!(schema.field(2).data_type(), &DataType::UInt64);
218 }
219
220 #[cfg(feature = "arrow")]
221 #[rstest]
222 fn test_hyperliquid_open_interest_arrow_schema() {
223 use arrow::datatypes::DataType;
224 use nautilus_serialization::arrow::ArrowSchemaProvider;
225
226 let schema = HyperliquidOpenInterest::get_schema(None);
227
228 assert_eq!(schema.fields().len(), 4);
229 assert_eq!(schema.field(0).name(), "instrument_id");
230 assert!(matches!(
231 schema.field(0).data_type(),
232 DataType::Utf8 | DataType::Utf8View
233 ));
234 assert_eq!(schema.field(1).name(), "open_interest");
235 assert!(matches!(
236 schema.field(1).data_type(),
237 DataType::Utf8 | DataType::Utf8View
238 ));
239 assert_eq!(schema.field(2).name(), "ts_event");
240 assert_eq!(schema.field(2).data_type(), &DataType::UInt64);
241 assert_eq!(schema.field(3).name(), "ts_init");
242 assert_eq!(schema.field(3).data_type(), &DataType::UInt64);
243 }
244
245 #[cfg(feature = "arrow")]
246 #[rstest]
247 fn test_hyperliquid_open_interest_arrow_round_trip_preserves_decimal() {
248 use std::str::FromStr;
249
250 use nautilus_model::data::Data;
251 use nautilus_serialization::arrow::{DecodeDataFromRecordBatch, EncodeToRecordBatch};
252
253 let original = HyperliquidOpenInterest::new(
254 InstrumentId::from("BTC-USD-PERP.HYPERLIQUID"),
255 Decimal::from_str("123456.789012345678").unwrap(),
256 UnixNanos::from(1),
257 UnixNanos::from(2),
258 );
259 let metadata = EncodeToRecordBatch::metadata(&original);
260 let batch =
261 HyperliquidOpenInterest::encode_batch(&metadata, std::slice::from_ref(&original))
262 .unwrap();
263 let decoded = HyperliquidOpenInterest::decode_data_batch(&metadata, batch).unwrap();
264
265 assert_eq!(decoded.len(), 1);
266 match &decoded[0] {
267 Data::Custom(custom) => {
268 let open_interest = custom
269 .data
270 .as_any()
271 .downcast_ref::<HyperliquidOpenInterest>()
272 .expect("expected HyperliquidOpenInterest");
273 assert_eq!(open_interest.instrument_id, original.instrument_id);
274 assert_eq!(open_interest.open_interest, original.open_interest);
275 assert_eq!(open_interest.ts_event, original.ts_event);
276 assert_eq!(open_interest.ts_init, original.ts_init);
277 }
278 other => panic!("Expected Data::Custom, was {other:?}"),
279 }
280 }
281
282 #[cfg(feature = "arrow")]
283 #[rstest]
284 fn test_hyperliquid_public_trade_arrow_round_trip_preserves_counterparties() {
285 use nautilus_model::{
286 data::Data,
287 enums::AggressorSide,
288 types::{Price, Quantity},
289 };
290 use nautilus_serialization::arrow::{DecodeDataFromRecordBatch, EncodeToRecordBatch};
291
292 let original = HyperliquidPublicTrade::new(
293 InstrumentId::from("BTC-USD-PERP.HYPERLIQUID"),
294 Price::from("100000.50"),
295 Quantity::from("0.123"),
296 AggressorSide::Buyer,
297 "123456".to_string(),
298 "0xbuyer".to_string(),
299 "0xseller".to_string(),
300 "0xhash".to_string(),
301 UnixNanos::from(1),
302 UnixNanos::from(2),
303 );
304 let metadata = EncodeToRecordBatch::metadata(&original);
305 let batch =
306 HyperliquidPublicTrade::encode_batch(&metadata, std::slice::from_ref(&original))
307 .unwrap();
308 let decoded = HyperliquidPublicTrade::decode_data_batch(&metadata, batch).unwrap();
309
310 let Data::Custom(custom) = &decoded[0] else {
311 panic!("Expected Data::Custom");
312 };
313 let trade = custom
314 .data
315 .as_any()
316 .downcast_ref::<HyperliquidPublicTrade>()
317 .expect("expected HyperliquidPublicTrade");
318 assert_eq!(trade.buyer, original.buyer);
319 assert_eq!(trade.seller, original.seller);
320 assert_eq!(trade.hash, original.hash);
321 assert_eq!(trade.price, original.price);
322 assert_eq!(trade.size, original.size);
323 }
324}