nautilus_hyperliquid/common/
types.rs1use std::fmt::Display;
17
18use serde::{Deserialize, Serialize};
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
28pub struct HyperliquidAssetId(pub u32);
29
30const HIP_1_SPOT_BASE: u32 = 10_000;
31const HIP_3_BUILDER_PERP_BASE: u32 = 100_000;
32const HIP_4_OUTCOME_BASE: u32 = 100_000_000;
33
34impl HyperliquidAssetId {
35 pub fn perp(index: u32) -> Self {
37 Self(index)
38 }
39
40 pub fn spot(index: u32) -> Self {
42 Self(HIP_1_SPOT_BASE + index)
43 }
44
45 pub fn builder_perp(dex_index: u32, meta_index: u32) -> Self {
47 Self(HIP_3_BUILDER_PERP_BASE + dex_index * 10_000 + meta_index)
48 }
49
50 pub fn outcome(outcome: u32, side: u8) -> Self {
59 assert!(side <= 1, "outcome side must be 0 or 1, received {side}");
60 Self(HIP_4_OUTCOME_BASE + 10 * outcome + u32::from(side))
61 }
62
63 pub fn from_outcome_encoding(encoding: u32) -> Option<Self> {
65 let raw = HIP_4_OUTCOME_BASE.checked_add(encoding)?;
66 let asset_id = Self(raw);
67 asset_id.is_outcome().then_some(asset_id)
68 }
69
70 pub fn is_perp(self) -> bool {
72 self.0 < HIP_1_SPOT_BASE
73 }
74
75 pub fn is_spot(self) -> bool {
77 self.0 >= HIP_1_SPOT_BASE && self.0 < HIP_3_BUILDER_PERP_BASE
78 }
79
80 pub fn is_builder_perp(self) -> bool {
82 self.0 >= HIP_3_BUILDER_PERP_BASE && self.0 < HIP_4_OUTCOME_BASE
83 }
84
85 pub fn is_outcome(self) -> bool {
91 self.0 >= HIP_4_OUTCOME_BASE && (self.0 - HIP_4_OUTCOME_BASE) % 10 <= 1
92 }
93
94 pub fn base_index(self) -> u32 {
101 if self.is_outcome() {
102 self.0 - HIP_4_OUTCOME_BASE
103 } else if self.is_builder_perp() {
104 (self.0 - HIP_3_BUILDER_PERP_BASE) % 10_000
105 } else if self.is_spot() {
106 self.0 - HIP_1_SPOT_BASE
107 } else {
108 self.0
109 }
110 }
111
112 pub fn outcome_index(self) -> Option<u32> {
114 self.outcome_encoding().map(|encoding| encoding / 10)
115 }
116
117 pub fn outcome_side(self) -> Option<u8> {
119 self.outcome_encoding()
120 .map(|encoding| (encoding % 10) as u8)
121 }
122
123 pub fn outcome_encoding(self) -> Option<u32> {
125 self.is_outcome().then(|| self.0 - HIP_4_OUTCOME_BASE)
126 }
127
128 pub fn to_raw(self) -> u32 {
130 self.0
131 }
132}
133
134impl Display for HyperliquidAssetId {
135 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136 write!(f, "{}", self.0)
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use rstest::rstest;
143
144 use super::*;
145
146 #[rstest]
147 fn test_asset_id_perp() {
148 let asset_id = HyperliquidAssetId::perp(7);
149 assert_eq!(asset_id.to_raw(), 7);
150 assert!(asset_id.is_perp());
151 assert!(!asset_id.is_spot());
152 assert!(!asset_id.is_builder_perp());
153 assert!(!asset_id.is_outcome());
154 assert_eq!(asset_id.base_index(), 7);
155 }
156
157 #[rstest]
158 fn test_asset_id_spot() {
159 let asset_id = HyperliquidAssetId::spot(7);
160 assert_eq!(asset_id.to_raw(), 10_007);
161 assert!(!asset_id.is_perp());
162 assert!(asset_id.is_spot());
163 assert!(!asset_id.is_builder_perp());
164 assert!(!asset_id.is_outcome());
165 assert_eq!(asset_id.base_index(), 7);
166 }
167
168 #[rstest]
169 fn test_asset_id_builder_perp() {
170 let asset_id = HyperliquidAssetId::builder_perp(1, 7);
171 assert_eq!(asset_id.to_raw(), 110_007);
172 assert!(!asset_id.is_perp());
173 assert!(!asset_id.is_spot());
174 assert!(asset_id.is_builder_perp());
175 assert!(!asset_id.is_outcome());
176 assert_eq!(asset_id.base_index(), 7);
177 }
178
179 #[rstest]
180 fn test_asset_id_outcome() {
181 let asset_id = HyperliquidAssetId::outcome(1, 0);
182 assert_eq!(asset_id.to_raw(), 100_000_010);
183 assert!(!asset_id.is_perp());
184 assert!(!asset_id.is_spot());
185 assert!(!asset_id.is_builder_perp());
186 assert!(asset_id.is_outcome());
187 assert_eq!(asset_id.base_index(), 10);
188 assert_eq!(asset_id.outcome_encoding(), Some(10));
189 assert_eq!(asset_id.outcome_index(), Some(1));
190 assert_eq!(asset_id.outcome_side(), Some(0));
191 }
192
193 #[rstest]
194 fn test_asset_id_outcome_side_one() {
195 let asset_id = HyperliquidAssetId::outcome(3, 1);
196 assert_eq!(asset_id.to_raw(), 100_000_031);
197 assert!(asset_id.is_outcome());
198 assert_eq!(asset_id.outcome_encoding(), Some(31));
199 assert_eq!(asset_id.outcome_index(), Some(3));
200 assert_eq!(asset_id.outcome_side(), Some(1));
201 }
202
203 #[rstest]
204 fn test_asset_id_from_outcome_encoding() {
205 let asset_id = HyperliquidAssetId::from_outcome_encoding(10).unwrap();
206 assert_eq!(asset_id.to_raw(), 100_000_010);
207 assert_eq!(asset_id.outcome_index(), Some(1));
208 assert_eq!(asset_id.outcome_side(), Some(0));
209 }
210
211 #[rstest]
212 fn test_asset_id_from_outcome_encoding_rejects_invalid_side() {
213 assert_eq!(HyperliquidAssetId::from_outcome_encoding(12), None);
214 }
215
216 #[rstest]
217 fn test_asset_id_from_outcome_encoding_rejects_overflow() {
218 assert_eq!(HyperliquidAssetId::from_outcome_encoding(u32::MAX), None);
219 }
220
221 #[rstest]
222 #[should_panic(expected = "outcome side must be 0 or 1")]
223 fn test_asset_id_outcome_invalid_side() {
224 let _ = HyperliquidAssetId::outcome(0, 2);
225 }
226
227 #[rstest]
228 fn test_asset_id_outcome_accessors_non_outcome() {
229 let perp = HyperliquidAssetId::perp(7);
230 assert_eq!(perp.outcome_index(), None);
231 assert_eq!(perp.outcome_side(), None);
232
233 let spot = HyperliquidAssetId::spot(7);
234 assert_eq!(spot.outcome_index(), None);
235 assert_eq!(spot.outcome_side(), None);
236
237 let builder = HyperliquidAssetId::builder_perp(1, 7);
238 assert_eq!(builder.outcome_index(), None);
239 assert_eq!(builder.outcome_side(), None);
240 }
241
242 #[rstest]
243 fn test_asset_id_outcome_invalid_side_digit_not_outcome() {
244 for side_digit in 2..=9u32 {
249 let raw = HyperliquidAssetId(100_000_000 + side_digit);
250 assert!(!raw.is_outcome(), "side digit {side_digit} must reject");
251 assert_eq!(raw.outcome_index(), None);
252 assert_eq!(raw.outcome_side(), None);
253 }
254 }
255
256 #[rstest]
257 fn test_asset_id_ranges_mutually_exclusive() {
258 let high_builder = HyperliquidAssetId(99_999_999);
263 assert!(high_builder.is_builder_perp());
264 assert!(!high_builder.is_outcome());
265
266 let low_outcome = HyperliquidAssetId(100_000_000);
267 assert!(!low_outcome.is_builder_perp());
268 assert!(low_outcome.is_outcome());
269 }
270}