Skip to main content

nautilus_hyperliquid/
data_types.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Hyperliquid-specific custom data types.
17//!
18//! These types carry Hyperliquid domain data through the Nautilus data engine as
19//! [`CustomData`](nautilus_model::data::CustomData).
20
21use std::collections::HashMap;
22
23use nautilus_core::UnixNanos;
24use nautilus_model::{identifiers::InstrumentId, types::Price};
25use nautilus_persistence_macros::custom_data;
26use rust_decimal::Decimal;
27use serde::{Deserialize, Serialize};
28
29/// Hyperliquid all mid prices snapshot from the `allMids` WebSocket channel.
30#[cfg_attr(
31    feature = "arrow",
32    custom_data(pyo3, stub_module = "nautilus_trader.adapters.hyperliquid")
33)]
34#[cfg_attr(
35    not(feature = "arrow"),
36    custom_data(pyo3, no_arrow, stub_module = "nautilus_trader.adapters.hyperliquid")
37)]
38pub struct HyperliquidAllMids {
39    /// Mapping of instrument ID to mid price for all tradable coins.
40    #[custom_data_field(serde)]
41    pub mids: HashMap<InstrumentId, Price>,
42    /// UNIX timestamp (nanoseconds) when the data event occurred.
43    pub ts_event: UnixNanos,
44    /// UNIX timestamp (nanoseconds) when the instance was initialized.
45    pub ts_init: UnixNanos,
46}
47
48/// Hyperliquid open interest update from the `activeAssetCtx` WebSocket channel.
49///
50/// Hyperliquid does not provide a native event timestamp on this payload, so
51/// `ts_event` mirrors `ts_init` like the peer asset-context update types.
52#[cfg_attr(
53    feature = "arrow",
54    custom_data(pyo3, stub_module = "nautilus_trader.adapters.hyperliquid")
55)]
56#[cfg_attr(
57    not(feature = "arrow"),
58    custom_data(pyo3, no_arrow, stub_module = "nautilus_trader.adapters.hyperliquid")
59)]
60pub struct HyperliquidOpenInterest {
61    /// The instrument ID for this open interest update.
62    pub instrument_id: InstrumentId,
63    /// The current open interest for the perpetual instrument.
64    #[custom_data_field(serde)]
65    pub open_interest: Decimal,
66    /// UNIX timestamp (nanoseconds) when the data event occurred.
67    pub ts_event: UnixNanos,
68    /// UNIX timestamp (nanoseconds) when the instance was initialized.
69    pub ts_init: UnixNanos,
70}
71
72/// Impact prices reported by Hyperliquid for venue-side execution estimates.
73#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
74pub struct HyperliquidImpactPrices {
75    /// Impact bid price.
76    pub bid: Price,
77    /// Impact ask price.
78    pub ask: Price,
79}
80
81/// Normalized per-instrument entry within `allDexsAssetCtxs`.
82#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
83pub struct HyperliquidDexAssetCtx {
84    /// Hyperliquid dex identifier. The default perp dex uses the empty string.
85    pub dex: String,
86    /// Canonical Nautilus instrument ID.
87    pub instrument_id: InstrumentId,
88    /// Mark price.
89    pub mark_price: Price,
90    /// Oracle/index price.
91    pub oracle_price: Price,
92    /// Previous day price.
93    pub prev_day_price: Price,
94    /// Optional mid price.
95    pub mid_price: Option<Price>,
96    /// Optional impact prices.
97    pub impact_prices: Option<HyperliquidImpactPrices>,
98    /// Current funding rate.
99    pub funding_rate: Decimal,
100    /// Current open interest.
101    pub open_interest: Decimal,
102    /// Optional premium.
103    pub premium: Option<Decimal>,
104    /// 24h notional volume.
105    pub day_ntl_volume: Decimal,
106    /// 24h base volume.
107    pub day_base_volume: Decimal,
108}
109
110/// Hyperliquid normalized aggregate snapshot from the `allDexsAssetCtxs` WebSocket channel.
111///
112/// This feed is live-only and intentionally JSON-backed; it is not coupled to Arrow persistence.
113#[custom_data(pyo3, no_arrow, stub_module = "nautilus_trader.adapters.hyperliquid")]
114pub struct HyperliquidAllDexsAssetCtxs {
115    /// Normalized per-instrument entries across all perp dexes.
116    #[custom_data_field(serde)]
117    pub entries: Vec<HyperliquidDexAssetCtx>,
118    /// UNIX timestamp (nanoseconds) when the data event occurred.
119    pub ts_event: UnixNanos,
120    /// UNIX timestamp (nanoseconds) when the instance was initialized.
121    pub ts_init: UnixNanos,
122}
123
124/// Registers Hyperliquid custom data types.
125///
126/// Safe to call multiple times (idempotent via internal `Once` guards).
127pub fn register_hyperliquid_custom_data() {
128    #[cfg(feature = "arrow")]
129    {
130        nautilus_serialization::ensure_custom_data_registered::<HyperliquidAllMids>();
131        nautilus_serialization::ensure_custom_data_registered::<HyperliquidOpenInterest>();
132    }
133
134    #[cfg(not(feature = "arrow"))]
135    {
136        let _ = nautilus_model::data::ensure_custom_data_json_registered::<HyperliquidAllMids>();
137        let _ =
138            nautilus_model::data::ensure_custom_data_json_registered::<HyperliquidOpenInterest>();
139    }
140
141    let _ =
142        nautilus_model::data::ensure_custom_data_json_registered::<HyperliquidAllDexsAssetCtxs>();
143}
144
145#[cfg(test)]
146mod tests {
147    use rstest::rstest;
148
149    use super::*;
150
151    #[rstest]
152    fn test_register_hyperliquid_custom_data_is_idempotent() {
153        register_hyperliquid_custom_data();
154        register_hyperliquid_custom_data();
155    }
156
157    #[cfg(feature = "arrow")]
158    #[rstest]
159    fn test_hyperliquid_all_mids_arrow_schema() {
160        use arrow::datatypes::DataType;
161        use nautilus_serialization::arrow::ArrowSchemaProvider;
162
163        let schema = HyperliquidAllMids::get_schema(None);
164
165        assert_eq!(schema.fields().len(), 3);
166        assert_eq!(schema.field(0).name(), "mids");
167        assert_eq!(schema.field(0).data_type(), &DataType::Utf8);
168        assert_eq!(schema.field(1).name(), "ts_event");
169        assert_eq!(schema.field(1).data_type(), &DataType::UInt64);
170        assert_eq!(schema.field(2).name(), "ts_init");
171        assert_eq!(schema.field(2).data_type(), &DataType::UInt64);
172    }
173
174    #[cfg(feature = "arrow")]
175    #[rstest]
176    fn test_hyperliquid_open_interest_arrow_schema() {
177        use arrow::datatypes::DataType;
178        use nautilus_serialization::arrow::ArrowSchemaProvider;
179
180        let schema = HyperliquidOpenInterest::get_schema(None);
181
182        assert_eq!(schema.fields().len(), 4);
183        assert_eq!(schema.field(0).name(), "instrument_id");
184        assert!(matches!(
185            schema.field(0).data_type(),
186            DataType::Utf8 | DataType::Utf8View
187        ));
188        assert_eq!(schema.field(1).name(), "open_interest");
189        assert!(matches!(
190            schema.field(1).data_type(),
191            DataType::Utf8 | DataType::Utf8View
192        ));
193        assert_eq!(schema.field(2).name(), "ts_event");
194        assert_eq!(schema.field(2).data_type(), &DataType::UInt64);
195        assert_eq!(schema.field(3).name(), "ts_init");
196        assert_eq!(schema.field(3).data_type(), &DataType::UInt64);
197    }
198
199    #[cfg(feature = "arrow")]
200    #[rstest]
201    fn test_hyperliquid_open_interest_arrow_round_trip_preserves_decimal() {
202        use std::str::FromStr;
203
204        use nautilus_model::data::Data;
205        use nautilus_serialization::arrow::{DecodeDataFromRecordBatch, EncodeToRecordBatch};
206
207        let original = HyperliquidOpenInterest::new(
208            InstrumentId::from("BTC-USD-PERP.HYPERLIQUID"),
209            Decimal::from_str("123456.789012345678").unwrap(),
210            UnixNanos::from(1),
211            UnixNanos::from(2),
212        );
213        let metadata = EncodeToRecordBatch::metadata(&original);
214        let batch =
215            HyperliquidOpenInterest::encode_batch(&metadata, std::slice::from_ref(&original))
216                .unwrap();
217        let decoded = HyperliquidOpenInterest::decode_data_batch(&metadata, batch).unwrap();
218
219        assert_eq!(decoded.len(), 1);
220        match &decoded[0] {
221            Data::Custom(custom) => {
222                let open_interest = custom
223                    .data
224                    .as_any()
225                    .downcast_ref::<HyperliquidOpenInterest>()
226                    .expect("expected HyperliquidOpenInterest");
227                assert_eq!(open_interest.instrument_id, original.instrument_id);
228                assert_eq!(open_interest.open_interest, original.open_interest);
229                assert_eq!(open_interest.ts_event, original.ts_event);
230                assert_eq!(open_interest.ts_init, original.ts_init);
231            }
232            other => panic!("Expected Data::Custom, was {other:?}"),
233        }
234    }
235}