Skip to main content

rustrade_data/exchange/hyperliquid/
market.rs

1//! The coin a Hyperliquid subscription names its market by.
2//!
3//! # Which instrument to subscribe with
4//!
5//! A [`MarketInstrumentData`] is subscribed by its `name_exchange` verbatim, so it works for every
6//! market. Build it with [`MarketInstrumentData::hyperliquid_perp`] or
7//! [`MarketInstrumentData::hyperliquid_spot`] from a
8//! [`HyperliquidMeta`](super::HyperliquidMeta) lookup.
9//!
10//! A [`MarketDataInstrument`] (or one [`Keyed`] over it) derives the coin from its asset names,
11//! which are stored lower-cased, so the venue's case is lost before the coin is built:
12//! - A perpetual's coin is the base asset upper-cased. That is right for most perpetuals (`BTC`),
13//!   but wrong for a mixed-case one (`kPEPE` becomes `KPEPE`) and for a builder-deployed (HIP-3)
14//!   one, whose deployer is lower-case (`xyz:TSLA` becomes `XYZ:TSLA`).
15//! - A spot pair's coin is `BASE/QUOTE` upper-cased, which names only PURR/USDC. Every other pair
16//!   is named `@{index}`; pass that as the base asset (`@107`) to subscribe to it.
17//!
18//! A wrong coin gets no data, and Hyperliquid then closes the connection, ending the other
19//! subscriptions on it too.
20
21use super::{Hyperliquid, HyperliquidSpot};
22use crate::{Identifier, instrument::MarketInstrumentData, subscription::Subscription};
23use rustrade_instrument::{
24    Keyed, asset::name::AssetNameInternal, instrument::market_data::MarketDataInstrument,
25};
26use serde::{Deserialize, Serialize};
27use smol_str::{SmolStr, StrExt, format_smolstr};
28
29/// The coin a Hyperliquid subscription names its market by (`BTC`, `kPEPE`, `xyz:TSLA`, `@107`,
30/// `PURR/USDC`).
31#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)]
32pub struct HyperliquidMarket(pub SmolStr);
33
34/// An instrument representation a Hyperliquid subscription can name as a market, perpetual or
35/// spot.
36///
37/// Implemented for every instrument type this crate subscribes with: [`MarketDataInstrument`],
38/// [`Keyed`] over it, and [`MarketInstrumentData`]. One blanket `Identifier` impl per connector
39/// covers them all, which is also what lets
40/// [`DynamicStreams`](crate::streams::builder::dynamic::DynamicStreams) route to both connectors
41/// through a single bound on the instrument type.
42pub trait HyperliquidInstrument {
43    /// The perpetual market this instrument subscribes to with [`Hyperliquid`].
44    fn hyperliquid_perp_market(&self) -> HyperliquidMarket;
45
46    /// The spot market this instrument subscribes to with [`HyperliquidSpot`].
47    fn hyperliquid_spot_market(&self) -> HyperliquidMarket;
48}
49
50impl<Instrument, Kind> Identifier<HyperliquidMarket> for Subscription<Hyperliquid, Instrument, Kind>
51where
52    Instrument: HyperliquidInstrument,
53{
54    fn id(&self) -> HyperliquidMarket {
55        self.instrument.hyperliquid_perp_market()
56    }
57}
58
59impl<Instrument, Kind> Identifier<HyperliquidMarket>
60    for Subscription<HyperliquidSpot, Instrument, Kind>
61where
62    Instrument: HyperliquidInstrument,
63{
64    fn id(&self) -> HyperliquidMarket {
65        self.instrument.hyperliquid_spot_market()
66    }
67}
68
69impl HyperliquidInstrument for MarketDataInstrument {
70    fn hyperliquid_perp_market(&self) -> HyperliquidMarket {
71        hyperliquid_perp_market(&self.base)
72    }
73
74    fn hyperliquid_spot_market(&self) -> HyperliquidMarket {
75        hyperliquid_spot_market(&self.base, &self.quote)
76    }
77}
78
79impl<InstrumentKey> HyperliquidInstrument for Keyed<InstrumentKey, MarketDataInstrument> {
80    fn hyperliquid_perp_market(&self) -> HyperliquidMarket {
81        self.value.hyperliquid_perp_market()
82    }
83
84    fn hyperliquid_spot_market(&self) -> HyperliquidMarket {
85        self.value.hyperliquid_spot_market()
86    }
87}
88
89/// The perpetual coin a [`MarketDataInstrument`] names: its base asset, upper-cased. See the
90/// [module docs](self) for the coins this gets wrong.
91fn hyperliquid_perp_market(base: &AssetNameInternal) -> HyperliquidMarket {
92    HyperliquidMarket(base.name().to_uppercase_smolstr())
93}
94
95/// The spot coin a [`MarketDataInstrument`] names: its base asset if that is an `@{index}`
96/// coin, otherwise `BASE/QUOTE` upper-cased, which names only PURR/USDC. See the
97/// [module docs](self).
98fn hyperliquid_spot_market(
99    base: &AssetNameInternal,
100    quote: &AssetNameInternal,
101) -> HyperliquidMarket {
102    let base_name = base.name();
103    if base_name.starts_with('@') {
104        HyperliquidMarket(SmolStr::new(base_name))
105    } else {
106        HyperliquidMarket(format_smolstr!(
107            "{}/{}",
108            base_name.to_uppercase_smolstr(),
109            quote.name().to_uppercase_smolstr()
110        ))
111    }
112}
113
114/// Built from a [`HyperliquidMeta`](super::HyperliquidMeta) lookup, so `name_exchange` is
115/// already the venue's coin, whichever connector subscribes with it.
116impl<InstrumentKey> HyperliquidInstrument for MarketInstrumentData<InstrumentKey> {
117    fn hyperliquid_perp_market(&self) -> HyperliquidMarket {
118        HyperliquidMarket(self.name_exchange.name().clone())
119    }
120
121    fn hyperliquid_spot_market(&self) -> HyperliquidMarket {
122        HyperliquidMarket(self.name_exchange.name().clone())
123    }
124}
125
126impl AsRef<str> for HyperliquidMarket {
127    fn as_ref(&self) -> &str {
128        &self.0
129    }
130}
131
132#[cfg(test)]
133// Test code: panics on bad input are acceptable
134#[allow(clippy::unwrap_used)]
135mod tests {
136    use super::*;
137    use crate::subscription::trade::PublicTrades;
138    use rustrade_instrument::{
139        hyperliquid::{Perps, SpotPairs},
140        instrument::market_data::kind::MarketDataInstrumentKind,
141    };
142
143    fn market<Exchange, Instrument>(exchange: Exchange, instrument: Instrument) -> String
144    where
145        Subscription<Exchange, Instrument, PublicTrades>: Identifier<HyperliquidMarket>,
146    {
147        Subscription::new(exchange, instrument, PublicTrades)
148            .id()
149            .0
150            .to_string()
151    }
152
153    #[test]
154    fn a_market_instrument_data_subscribes_by_the_venue_spelling() {
155        let perps: Perps =
156            serde_json::from_str(r#"{"universe": [{"name": "kPEPE", "szDecimals": 0}, {"name": "xyz:TSLA", "szDecimals": 3}]}"#)
157                .unwrap();
158        let spot_pairs: SpotPairs = serde_json::from_str(
159            r#"{
160                "universe": [{"name": "@107", "tokens": [150, 0], "index": 107}],
161                "tokens": [{"name": "USDC", "szDecimals": 8, "index": 0}, {"name": "HYPE", "szDecimals": 2, "index": 150}]
162            }"#,
163        )
164        .unwrap();
165
166        for (name, coin) in [("KPEPE", "kPEPE"), ("XYZ:TSLA", "xyz:TSLA")] {
167            let perp = MarketInstrumentData::hyperliquid_perp(0, perps.get(name).unwrap());
168            assert_eq!(perp.kind, MarketDataInstrumentKind::Perpetual);
169            assert_eq!(market(Hyperliquid, perp), coin);
170        }
171        let hype =
172            MarketInstrumentData::hyperliquid_spot(0, spot_pairs.find("hype", "usdc").unwrap());
173        assert_eq!(hype.kind, MarketDataInstrumentKind::Spot);
174        assert_eq!(market(HyperliquidSpot, hype), "@107");
175    }
176
177    #[test]
178    fn a_market_data_instrument_loses_the_venue_case() {
179        // Documented limitation: asset names are stored lower-cased.
180        let perp =
181            |base| MarketDataInstrument::new(base, "usdc", MarketDataInstrumentKind::Perpetual);
182        assert_eq!(market(Hyperliquid, perp("btc")), "BTC");
183        assert_eq!(market(Hyperliquid, perp("kPEPE")), "KPEPE");
184
185        let spot = |base| MarketDataInstrument::new(base, "usdc", MarketDataInstrumentKind::Spot);
186        assert_eq!(market(HyperliquidSpot, spot("purr")), "PURR/USDC");
187        assert_eq!(market(HyperliquidSpot, spot("@107")), "@107");
188    }
189
190    #[test]
191    fn a_keyed_instrument_subscribes_as_its_value_does() {
192        let perp = MarketDataInstrument::new("btc", "usdc", MarketDataInstrumentKind::Perpetual);
193        assert_eq!(market(Hyperliquid, Keyed::new(0, perp)), "BTC");
194
195        let spot = |base| MarketDataInstrument::new(base, "usdc", MarketDataInstrumentKind::Spot);
196        assert_eq!(
197            market(HyperliquidSpot, Keyed::new(0, spot("purr"))),
198            "PURR/USDC"
199        );
200        assert_eq!(market(HyperliquidSpot, Keyed::new(0, spot("@107"))), "@107");
201    }
202}