Skip to main content

rustrade_data/exchange/hyperliquid/
mod.rs

1//! Hyperliquid market data connectors for perpetual futures and spot trading.
2//!
3//! Uses raw WebSocket connection to `wss://api.hyperliquid.xyz/ws` for market data streams.
4//! Reuses `hyperliquid_rust_sdk` types for deserialization.
5//!
6//! # Connectors
7//!
8//! - `Hyperliquid`: Perpetual futures market data
9//! - `HyperliquidSpot`: Spot trading market data
10//!
11//! # Supported Streams
12//! - [`PublicTrades`](crate::subscription::trade::PublicTrades): Real-time trade feed
13//! - [`OrderBooksL2`](crate::subscription::book::OrderBooksL2): L2 order book snapshots
14//!
15//! # Coins
16//!
17//! A subscription names its market by a coin, exactly as Hyperliquid spells it:
18//! - a perpetual by its asset (`BTC`, `kPEPE`), or a builder-deployed (HIP-3) one by its deployer
19//!   and asset (`xyz:TSLA`);
20//! - a spot pair by `@{index}` (`@107` is HYPE/USDC), except PURR/USDC, named `PURR/USDC`.
21//!
22//! A wrong coin gets no data, and the server then closes the connection, ending the other
23//! subscriptions on it as well. So read the coins with
24//! [`HyperliquidMeta`](crate::exchange::hyperliquid::HyperliquidMeta) and subscribe with a
25//! [`MarketInstrumentData`](crate::instrument::MarketInstrumentData) built from them:
26//!
27//! ```ignore
28//! use rustrade_data::{
29//!     exchange::hyperliquid::{HyperliquidMeta, HyperliquidSpot, Network},
30//!     instrument::MarketInstrumentData,
31//!     streams::Streams,
32//!     subscriber::WebSocketSubscriber,
33//!     subscription::{Subscription, trade::PublicTrades},
34//! };
35//!
36//! let meta = HyperliquidMeta::fetch(Network::Mainnet, &[]).await?;
37//! let hype = meta.spot_pair("HYPE", "USDC").ok_or("no such pair")?; // `@107`
38//!
39//! let streams = Streams::<PublicTrades>::builder()
40//!     .subscribe(
41//!         WebSocketSubscriber,
42//!         [Subscription {
43//!             exchange: HyperliquidSpot,
44//!             instrument: MarketInstrumentData::hyperliquid_spot("hype_usdc", hype),
45//!             kind: PublicTrades,
46//!         }],
47//!     )
48//!     .init()
49//!     .await?;
50//! ```
51//!
52//! A [`MarketInstrumentData`](crate::instrument::MarketInstrumentData) is subscribed by its
53//! `name_exchange` verbatim. A subscription built from a
54//! [`MarketDataInstrument`](rustrade_instrument::instrument::market_data::MarketDataInstrument)
55//! instead derives the coin from asset names, which lose their case, and works only for some
56//! markets. See
57//! [`market`](crate::exchange::hyperliquid::market).
58//!
59//! # Notes
60//! - Market data streams are unauthenticated (public data)
61//! - Execution client uses SDK's WebSocket separately for user data
62//! - Spot and perps use the same WebSocket endpoint and protocol
63
64use self::{
65    book::HyperliquidL2Book, channel::HyperliquidChannel, market::HyperliquidMarket,
66    subscription::HyperliquidSubResponse, trade::HyperliquidTrade,
67};
68use crate::{
69    ExchangeWsStream, NoInitialSnapshots,
70    exchange::{Connector, ExchangeSub, PingInterval, StreamSelector},
71    instrument::InstrumentData,
72    subscriber::{WebSocketSubscriber, validator::WebSocketSubValidator},
73    subscription::{book::OrderBooksL2, trade::PublicTrades},
74    transformer::stateless::StatelessTransformer,
75};
76use derive_more::Display;
77use rustrade_instrument::exchange::ExchangeId;
78use rustrade_integration::{
79    protocol::websocket::{WebSocketSerdeParser, WsMessage},
80    subscription::SubscriptionId,
81};
82use rustrade_macro::{DeExchange, SerExchange};
83use serde_json::json;
84use std::time::Duration;
85use url::Url;
86
87pub mod book;
88pub mod channel;
89pub mod historical;
90pub mod market;
91pub mod meta;
92pub mod subscription;
93pub mod trade;
94
95pub use meta::{HyperliquidMeta, HyperliquidMetaError};
96pub use rustrade_instrument::hyperliquid::Network;
97
98/// Hyperliquid mainnet WebSocket URL.
99pub const BASE_URL_HYPERLIQUID: &str = "wss://api.hyperliquid.xyz/ws";
100
101/// Ping interval for Hyperliquid WebSocket (50 seconds, matching SDK).
102const PING_INTERVAL_SECS: u64 = 50;
103
104/// Type alias for Hyperliquid WebSocket stream.
105pub type HyperliquidWsStream<Transformer> = ExchangeWsStream<WebSocketSerdeParser, Transformer>;
106
107/// Build WebSocket subscription messages for Hyperliquid channels.
108fn build_subscribe_messages(
109    exchange_subs: Vec<ExchangeSub<HyperliquidChannel, HyperliquidMarket>>,
110) -> Vec<WsMessage> {
111    exchange_subs
112        .into_iter()
113        .map(|ExchangeSub { channel, market }| {
114            WsMessage::text(
115                json!({
116                    "method": "subscribe",
117                    "subscription": channel.subscription_payload(market.as_ref())
118                })
119                .to_string(),
120            )
121        })
122        .collect()
123}
124
125/// Hyperliquid perpetual futures exchange connector.
126#[derive(
127    Copy,
128    Clone,
129    Eq,
130    PartialEq,
131    Ord,
132    PartialOrd,
133    Hash,
134    Debug,
135    Default,
136    Display,
137    DeExchange,
138    SerExchange,
139)]
140pub struct Hyperliquid;
141
142impl Connector for Hyperliquid {
143    const ID: ExchangeId = ExchangeId::HyperliquidPerp;
144    type Channel = HyperliquidChannel;
145    type Market = HyperliquidMarket;
146    type Subscriber = WebSocketSubscriber;
147    type SubValidator = WebSocketSubValidator;
148    type SubResponse = HyperliquidSubResponse;
149
150    fn url() -> Result<Url, url::ParseError> {
151        Url::parse(BASE_URL_HYPERLIQUID)
152    }
153
154    fn ping_interval() -> Option<PingInterval> {
155        Some(PingInterval {
156            interval: tokio::time::interval(Duration::from_secs(PING_INTERVAL_SECS)),
157            ping: || WsMessage::text(r#"{"method":"ping"}"#),
158        })
159    }
160
161    fn requests(exchange_subs: Vec<ExchangeSub<Self::Channel, Self::Market>>) -> Vec<WsMessage> {
162        build_subscribe_messages(exchange_subs)
163    }
164
165    fn acknowledged_subscription(response: &Self::SubResponse) -> Option<SubscriptionId> {
166        response.subscription_id()
167    }
168}
169
170impl<Instrument> StreamSelector<Instrument, PublicTrades> for Hyperliquid
171where
172    Instrument: InstrumentData,
173{
174    type SnapFetcher = NoInitialSnapshots;
175    type Stream = HyperliquidWsStream<
176        StatelessTransformer<Self, Instrument::Key, PublicTrades, HyperliquidTrade>,
177    >;
178}
179
180impl<Instrument> StreamSelector<Instrument, OrderBooksL2> for Hyperliquid
181where
182    Instrument: InstrumentData,
183{
184    type SnapFetcher = NoInitialSnapshots;
185    type Stream = HyperliquidWsStream<
186        StatelessTransformer<Self, Instrument::Key, OrderBooksL2, HyperliquidL2Book>,
187    >;
188}
189
190/// Hyperliquid spot trading exchange connector.
191///
192/// Uses the same WebSocket endpoint and protocol as perpetuals, but with a different
193/// `ExchangeId` for routing.
194///
195/// # Market Format
196///
197/// Spot markets are named `@{index}` (`"@107"` is HYPE/USDC), except PURR/USDC, named
198/// `"PURR/USDC"`. [`HyperliquidMeta::spot_pair`] finds a pair's coin from its tokens.
199///
200/// Perpetuals, subscribed with [`Hyperliquid`], are named by their asset (`"BTC"`, `"kPEPE"`).
201#[derive(
202    Copy,
203    Clone,
204    Eq,
205    PartialEq,
206    Ord,
207    PartialOrd,
208    Hash,
209    Debug,
210    Default,
211    Display,
212    DeExchange,
213    SerExchange,
214)]
215pub struct HyperliquidSpot;
216
217impl Connector for HyperliquidSpot {
218    const ID: ExchangeId = ExchangeId::HyperliquidSpot;
219    type Channel = HyperliquidChannel;
220    type Market = HyperliquidMarket;
221    type Subscriber = WebSocketSubscriber;
222    type SubValidator = WebSocketSubValidator;
223    type SubResponse = HyperliquidSubResponse;
224
225    fn url() -> Result<Url, url::ParseError> {
226        Url::parse(BASE_URL_HYPERLIQUID)
227    }
228
229    fn ping_interval() -> Option<PingInterval> {
230        Some(PingInterval {
231            interval: tokio::time::interval(Duration::from_secs(PING_INTERVAL_SECS)),
232            ping: || WsMessage::text(r#"{"method":"ping"}"#),
233        })
234    }
235
236    fn requests(exchange_subs: Vec<ExchangeSub<Self::Channel, Self::Market>>) -> Vec<WsMessage> {
237        build_subscribe_messages(exchange_subs)
238    }
239
240    fn acknowledged_subscription(response: &Self::SubResponse) -> Option<SubscriptionId> {
241        response.subscription_id()
242    }
243}
244
245impl<Instrument> StreamSelector<Instrument, PublicTrades> for HyperliquidSpot
246where
247    Instrument: InstrumentData,
248{
249    type SnapFetcher = NoInitialSnapshots;
250    type Stream = HyperliquidWsStream<
251        StatelessTransformer<Self, Instrument::Key, PublicTrades, HyperliquidTrade>,
252    >;
253}
254
255impl<Instrument> StreamSelector<Instrument, OrderBooksL2> for HyperliquidSpot
256where
257    Instrument: InstrumentData,
258{
259    type SnapFetcher = NoInitialSnapshots;
260    type Stream = HyperliquidWsStream<
261        StatelessTransformer<Self, Instrument::Key, OrderBooksL2, HyperliquidL2Book>,
262    >;
263}