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 perpetual subscription can name as a market.
35///
36/// Implemented for every instrument type this crate subscribes with: [`MarketDataInstrument`],
37/// [`Keyed`] over it, and [`MarketInstrumentData`]. One blanket `Identifier` impl covers them all,
38/// which is also what lets
39/// [`DynamicStreams`](crate::streams::builder::dynamic::DynamicStreams) route to this integration
40/// through a single bound on the instrument type.
41pub trait HyperliquidInstrument {
42    /// The perpetual market this instrument subscribes to.
43    fn hyperliquid_market(&self) -> HyperliquidMarket;
44}
45
46impl<Instrument, Kind> Identifier<HyperliquidMarket> for Subscription<Hyperliquid, Instrument, Kind>
47where
48    Instrument: HyperliquidInstrument,
49{
50    fn id(&self) -> HyperliquidMarket {
51        self.instrument.hyperliquid_market()
52    }
53}
54
55impl HyperliquidInstrument for MarketDataInstrument {
56    fn hyperliquid_market(&self) -> HyperliquidMarket {
57        hyperliquid_market(&self.base)
58    }
59}
60
61impl<InstrumentKey> HyperliquidInstrument for Keyed<InstrumentKey, MarketDataInstrument> {
62    fn hyperliquid_market(&self) -> HyperliquidMarket {
63        self.value.hyperliquid_market()
64    }
65}
66
67/// The perpetual coin a [`MarketDataInstrument`] names: its base asset, upper-cased. See the
68/// [module docs](self) for the coins this gets wrong.
69fn hyperliquid_market(base: &AssetNameInternal) -> HyperliquidMarket {
70    HyperliquidMarket(base.name().to_uppercase_smolstr())
71}
72
73impl<InstrumentKey> HyperliquidInstrument for MarketInstrumentData<InstrumentKey> {
74    fn hyperliquid_market(&self) -> HyperliquidMarket {
75        HyperliquidMarket(self.name_exchange.name().clone())
76    }
77}
78
79impl AsRef<str> for HyperliquidMarket {
80    fn as_ref(&self) -> &str {
81        &self.0
82    }
83}
84
85// HyperliquidSpot implementations
86
87impl<Kind> Identifier<HyperliquidMarket>
88    for Subscription<HyperliquidSpot, MarketDataInstrument, Kind>
89{
90    fn id(&self) -> HyperliquidMarket {
91        hyperliquid_spot_market(&self.instrument.base, &self.instrument.quote)
92    }
93}
94
95impl<InstrumentKey, Kind> Identifier<HyperliquidMarket>
96    for Subscription<HyperliquidSpot, Keyed<InstrumentKey, MarketDataInstrument>, Kind>
97{
98    fn id(&self) -> HyperliquidMarket {
99        hyperliquid_spot_market(&self.instrument.value.base, &self.instrument.value.quote)
100    }
101}
102
103/// The spot coin a [`MarketDataInstrument`] names: its base asset if that is an `@{index}`
104/// coin, otherwise `BASE/QUOTE` upper-cased, which names only PURR/USDC. See the
105/// [module docs](self).
106fn hyperliquid_spot_market(
107    base: &AssetNameInternal,
108    quote: &AssetNameInternal,
109) -> HyperliquidMarket {
110    let base_name = base.name();
111    if base_name.starts_with('@') {
112        HyperliquidMarket(SmolStr::new(base_name))
113    } else {
114        HyperliquidMarket(format_smolstr!(
115            "{}/{}",
116            base_name.to_uppercase_smolstr(),
117            quote.name().to_uppercase_smolstr()
118        ))
119    }
120}
121
122impl<InstrumentKey, Kind> Identifier<HyperliquidMarket>
123    for Subscription<HyperliquidSpot, MarketInstrumentData<InstrumentKey>, Kind>
124{
125    fn id(&self) -> HyperliquidMarket {
126        HyperliquidMarket(self.instrument.name_exchange.name().clone())
127    }
128}
129
130#[cfg(test)]
131// Test code: panics on bad input are acceptable
132#[allow(clippy::unwrap_used)]
133mod tests {
134    use super::*;
135    use crate::subscription::trade::PublicTrades;
136    use rustrade_instrument::{
137        hyperliquid::{Perps, SpotPairs},
138        instrument::market_data::kind::MarketDataInstrumentKind,
139    };
140
141    fn market<Exchange, Instrument>(exchange: Exchange, instrument: Instrument) -> String
142    where
143        Subscription<Exchange, Instrument, PublicTrades>: Identifier<HyperliquidMarket>,
144    {
145        Subscription::new(exchange, instrument, PublicTrades)
146            .id()
147            .0
148            .to_string()
149    }
150
151    #[test]
152    fn a_market_instrument_data_subscribes_by_the_venue_spelling() {
153        let perps: Perps =
154            serde_json::from_str(r#"{"universe": [{"name": "kPEPE"}, {"name": "xyz:TSLA"}]}"#)
155                .unwrap();
156        let spot_pairs: SpotPairs = serde_json::from_str(
157            r#"{
158                "universe": [{"name": "@107", "tokens": [150, 0], "index": 107}],
159                "tokens": [{"name": "USDC", "szDecimals": 8, "index": 0}, {"name": "HYPE", "szDecimals": 2, "index": 150}]
160            }"#,
161        )
162        .unwrap();
163
164        for (name, coin) in [("KPEPE", "kPEPE"), ("XYZ:TSLA", "xyz:TSLA")] {
165            let perp = MarketInstrumentData::hyperliquid_perp(0, perps.get(name).unwrap());
166            assert_eq!(perp.kind, MarketDataInstrumentKind::Perpetual);
167            assert_eq!(market(Hyperliquid, perp), coin);
168        }
169        let hype =
170            MarketInstrumentData::hyperliquid_spot(0, spot_pairs.find("hype", "usdc").unwrap());
171        assert_eq!(hype.kind, MarketDataInstrumentKind::Spot);
172        assert_eq!(market(HyperliquidSpot, hype), "@107");
173    }
174
175    #[test]
176    fn a_market_data_instrument_loses_the_venue_case() {
177        // Documented limitation: asset names are stored lower-cased.
178        let perp =
179            |base| MarketDataInstrument::new(base, "usdc", MarketDataInstrumentKind::Perpetual);
180        assert_eq!(market(Hyperliquid, perp("btc")), "BTC");
181        assert_eq!(market(Hyperliquid, perp("kPEPE")), "KPEPE");
182
183        let spot = |base| MarketDataInstrument::new(base, "usdc", MarketDataInstrumentKind::Spot);
184        assert_eq!(market(HyperliquidSpot, spot("purr")), "PURR/USDC");
185        assert_eq!(market(HyperliquidSpot, spot("@107")), "@107");
186    }
187}