Skip to main content

hypercall/price_oracle/
hyperliquid_types.rs

1//! Hyperliquid API types for the metaAndAssetCtxs endpoint.
2//!
3//! This module defines the request and response types for fetching oracle prices
4//! from Hyperliquid's info API.
5
6use serde::{Deserialize, Serialize};
7
8/// Request body for the metaAndAssetCtxs endpoint.
9///
10/// POST to https://api.hyperliquid.xyz/info
11#[derive(Debug, Clone, Serialize)]
12pub struct MetaAndAssetCtxsRequest {
13    #[serde(rename = "type")]
14    pub request_type: String,
15}
16
17impl MetaAndAssetCtxsRequest {
18    pub fn new() -> Self {
19        Self {
20            request_type: "metaAndAssetCtxs".to_string(),
21        }
22    }
23}
24
25impl Default for MetaAndAssetCtxsRequest {
26    fn default() -> Self {
27        Self::new()
28    }
29}
30
31/// Response from metaAndAssetCtxs endpoint.
32///
33/// The API returns a tuple: `[Meta, Vec<AssetCtx>]`
34/// where the indices of AssetCtx correspond to the indices in Meta.universe
35#[derive(Debug, Clone)]
36pub struct MetaAndAssetCtxsResponse {
37    pub meta: Meta,
38    pub asset_ctxs: Vec<AssetCtx>,
39}
40
41impl<'de> Deserialize<'de> for MetaAndAssetCtxsResponse {
42    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
43    where
44        D: serde::Deserializer<'de>,
45    {
46        // Response format: [Meta, [AssetCtx, AssetCtx, ...]]
47        let tuple: (Meta, Vec<AssetCtx>) = Deserialize::deserialize(deserializer)?;
48        Ok(MetaAndAssetCtxsResponse {
49            meta: tuple.0,
50            asset_ctxs: tuple.1,
51        })
52    }
53}
54
55/// Metadata about the Hyperliquid exchange.
56#[derive(Debug, Clone, Deserialize)]
57pub struct Meta {
58    pub universe: Vec<UniverseAsset>,
59}
60
61/// Asset definition in the Hyperliquid universe.
62#[derive(Debug, Clone, Deserialize)]
63#[serde(rename_all = "camelCase")]
64pub struct UniverseAsset {
65    /// Asset name (e.g., "BTC", "ETH")
66    pub name: String,
67    /// Size decimals for the asset
68    pub sz_decimals: u32,
69    /// Maximum leverage allowed
70    #[serde(default)]
71    pub max_leverage: u32,
72    /// Whether only isolated margin is allowed
73    #[serde(default)]
74    pub only_isolated: Option<bool>,
75}
76
77/// Asset context containing pricing information.
78///
79/// This is the main structure containing oracle prices.
80#[derive(Debug, Clone, Deserialize)]
81#[serde(rename_all = "camelCase")]
82pub struct AssetCtx {
83    /// Funding rate (as string, needs parsing)
84    pub funding: String,
85    /// Open interest (as string, needs parsing)
86    pub open_interest: String,
87    /// Previous day's price
88    #[serde(default)]
89    pub prev_day_px: String,
90    /// Day's notional volume
91    #[serde(default)]
92    pub day_ntl_vlm: String,
93    /// Premium (mark - oracle)
94    #[serde(default)]
95    pub premium: Option<String>,
96    /// Oracle/index price - THIS IS WHAT WE WANT
97    /// This is the spot price from external exchanges, not influenced by HL funding
98    pub oracle_px: String,
99    /// Mark price (oracle + premium adjustment)
100    pub mark_px: String,
101    /// Mid price from orderbook
102    #[serde(default)]
103    pub mid_px: Option<String>,
104    /// Impact prices for slippage calculation
105    #[serde(default)]
106    pub impact_pxs: Option<Vec<String>>,
107}
108
109impl AssetCtx {
110    /// Parse oracle price as f64.
111    ///
112    /// Returns None if the price string cannot be parsed.
113    pub fn oracle_price(&self) -> Option<f64> {
114        self.oracle_px.parse::<f64>().ok()
115    }
116
117    /// Parse previous day's price as f64.
118    pub fn prev_day_price(&self) -> Option<f64> {
119        self.prev_day_px
120            .parse::<f64>()
121            .ok()
122            .filter(|v| v.is_finite() && *v > 0.0)
123    }
124
125    /// Parse mark price as f64.
126    ///
127    /// Returns None if the price string cannot be parsed.
128    pub fn mark_price(&self) -> Option<f64> {
129        self.mark_px.parse::<f64>().ok()
130    }
131
132    /// Parse funding rate as f64.
133    pub fn funding_rate(&self) -> Option<f64> {
134        self.funding.parse::<f64>().ok()
135    }
136
137    /// Parse open interest as f64.
138    pub fn open_interest_value(&self) -> Option<f64> {
139        self.open_interest.parse::<f64>().ok()
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn test_deserialize_meta_and_asset_ctxs() {
149        let json = r#"[
150            {
151                "universe": [
152                    {"name": "BTC", "szDecimals": 5, "maxLeverage": 50},
153                    {"name": "ETH", "szDecimals": 4, "maxLeverage": 50}
154                ]
155            },
156            [
157                {
158                    "funding": "0.0001",
159                    "openInterest": "1000000",
160                    "prevDayPx": "42000.0",
161                    "dayNtlVlm": "500000000",
162                    "premium": "0.001",
163                    "oraclePx": "42500.5",
164                    "markPx": "42550.0"
165                },
166                {
167                    "funding": "0.00005",
168                    "openInterest": "500000",
169                    "prevDayPx": "2200.0",
170                    "dayNtlVlm": "100000000",
171                    "premium": "0.0005",
172                    "oraclePx": "2250.25",
173                    "markPx": "2251.0"
174                }
175            ]
176        ]"#;
177
178        let response: MetaAndAssetCtxsResponse = sonic_rs::from_str(json).unwrap();
179
180        assert_eq!(response.meta.universe.len(), 2);
181        assert_eq!(response.meta.universe[0].name, "BTC");
182        assert_eq!(response.meta.universe[1].name, "ETH");
183
184        assert_eq!(response.asset_ctxs.len(), 2);
185        assert_eq!(response.asset_ctxs[0].oracle_price(), Some(42500.5));
186        assert_eq!(response.asset_ctxs[1].oracle_price(), Some(2250.25));
187    }
188
189    #[test]
190    fn test_asset_ctx_parsing() {
191        let ctx = AssetCtx {
192            funding: "0.0001".to_string(),
193            open_interest: "1000000.5".to_string(),
194            prev_day_px: "42000.0".to_string(),
195            day_ntl_vlm: "500000000".to_string(),
196            premium: Some("0.001".to_string()),
197            oracle_px: "42500.5".to_string(),
198            mark_px: "42550.0".to_string(),
199            mid_px: None,
200            impact_pxs: None,
201        };
202
203        assert_eq!(ctx.oracle_price(), Some(42500.5));
204        assert_eq!(ctx.mark_price(), Some(42550.0));
205        assert_eq!(ctx.funding_rate(), Some(0.0001));
206        assert_eq!(ctx.open_interest_value(), Some(1000000.5));
207    }
208}