1use rust_decimal::Decimal;
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
6#[non_exhaustive]
7pub struct HlOrderbook {
8 pub coin: String,
10 pub bids: Vec<(Decimal, Decimal)>,
12 pub asks: Vec<(Decimal, Decimal)>,
14 pub timestamp: u64,
16}
17
18impl HlOrderbook {
19 pub fn new(
21 coin: String,
22 bids: Vec<(Decimal, Decimal)>,
23 asks: Vec<(Decimal, Decimal)>,
24 timestamp: u64,
25 ) -> Self {
26 Self {
27 coin,
28 bids,
29 asks,
30 timestamp,
31 }
32 }
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
37#[serde(rename_all = "camelCase")]
38#[non_exhaustive]
39pub struct HlAssetInfo {
40 pub coin: String,
42 pub asset_id: u32,
44 pub min_size: Decimal,
46 pub sz_decimals: u32,
48 pub px_decimals: u32,
50}
51
52impl HlAssetInfo {
53 pub fn new(
55 coin: String,
56 asset_id: u32,
57 min_size: Decimal,
58 sz_decimals: u32,
59 px_decimals: u32,
60 ) -> Self {
61 Self {
62 coin,
63 asset_id,
64 min_size,
65 sz_decimals,
66 px_decimals,
67 }
68 }
69}
70
71#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
73#[serde(rename_all = "camelCase")]
74#[non_exhaustive]
75pub struct HlFundingRate {
76 pub coin: String,
78 pub funding_rate: Decimal,
80 pub next_funding_time: u64,
82}
83
84impl HlFundingRate {
85 pub fn new(coin: String, funding_rate: Decimal, next_funding_time: u64) -> Self {
87 Self {
88 coin,
89 funding_rate,
90 next_funding_time,
91 }
92 }
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
97#[serde(rename_all = "camelCase")]
98#[non_exhaustive]
99pub struct HlSpotAssetInfo {
100 pub name: String,
102 pub index: u32,
104 pub sz_decimals: u32,
106 pub wei_decimals: u32,
108}
109
110impl HlSpotAssetInfo {
111 pub fn new(name: String, index: u32, sz_decimals: u32, wei_decimals: u32) -> Self {
113 Self {
114 name,
115 index,
116 sz_decimals,
117 wei_decimals,
118 }
119 }
120}
121
122#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
124#[non_exhaustive]
125pub struct HlSpotMeta {
126 pub tokens: Vec<HlSpotAssetInfo>,
128}
129
130impl HlSpotMeta {
131 pub fn new(tokens: Vec<HlSpotAssetInfo>) -> Self {
133 Self { tokens }
134 }
135}
136
137#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
139#[serde(rename_all = "camelCase")]
140#[non_exhaustive]
141pub struct AssetContext {
142 pub funding: Decimal,
144 pub open_interest: Decimal,
146 pub mark_px: Decimal,
148}
149
150impl AssetContext {
151 pub fn new(funding: Decimal, open_interest: Decimal, mark_px: Decimal) -> Self {
153 Self {
154 funding,
155 open_interest,
156 mark_px,
157 }
158 }
159}
160
161#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
163#[serde(rename_all = "camelCase")]
164#[non_exhaustive]
165pub struct SpotAssetContext {
166 pub mark_px: Decimal,
168 pub mid_px: Decimal,
170}
171
172impl SpotAssetContext {
173 pub fn new(mark_px: Decimal, mid_px: Decimal) -> Self {
175 Self { mark_px, mid_px }
176 }
177}
178
179#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
181pub enum TradeSide {
182 #[serde(rename = "B")]
184 Buy,
185 #[serde(rename = "A")]
187 Sell,
188}
189
190impl std::fmt::Display for TradeSide {
191 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
192 match self {
193 TradeSide::Buy => write!(f, "B"),
194 TradeSide::Sell => write!(f, "A"),
195 }
196 }
197}
198
199#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
201#[serde(rename_all = "camelCase")]
202#[non_exhaustive]
203pub struct HlTrade {
204 pub coin: String,
206 pub side: TradeSide,
208 pub px: Decimal,
210 pub sz: Decimal,
212 pub time: u64,
214}
215
216impl HlTrade {
217 pub fn new(coin: String, side: TradeSide, px: Decimal, sz: Decimal, time: u64) -> Self {
219 Self {
220 coin,
221 side,
222 px,
223 sz,
224 time,
225 }
226 }
227}
228
229#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
231#[serde(rename_all = "camelCase")]
232#[non_exhaustive]
233pub struct HlSpotBalance {
234 pub coin: String,
236 pub token: u32,
238 pub hold: Decimal,
240 pub total: Decimal,
242}
243
244impl HlSpotBalance {
245 pub fn new(coin: String, token: u32, hold: Decimal, total: Decimal) -> Self {
247 Self {
248 coin,
249 token,
250 hold,
251 total,
252 }
253 }
254}
255
256#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
258#[serde(rename_all = "camelCase")]
259#[non_exhaustive]
260pub struct HlPerpDexStatus {
261 pub name: String,
263 pub is_active: bool,
265 pub num_assets: u32,
267 pub total_oi: Decimal,
269}
270
271impl HlPerpDexStatus {
272 pub fn new(name: String, is_active: bool, num_assets: u32, total_oi: Decimal) -> Self {
274 Self {
275 name,
276 is_active,
277 num_assets,
278 total_oi,
279 }
280 }
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286 use std::str::FromStr;
287
288 #[test]
289 fn orderbook_serde_roundtrip() {
290 let ob = HlOrderbook {
291 coin: "BTC".into(),
292 bids: vec![
293 (
294 Decimal::from_str("50000.0").unwrap(),
295 Decimal::from_str("1.5").unwrap(),
296 ),
297 (
298 Decimal::from_str("49999.0").unwrap(),
299 Decimal::from_str("2.0").unwrap(),
300 ),
301 ],
302 asks: vec![
303 (
304 Decimal::from_str("50001.0").unwrap(),
305 Decimal::from_str("0.5").unwrap(),
306 ),
307 (
308 Decimal::from_str("50002.0").unwrap(),
309 Decimal::from_str("3.0").unwrap(),
310 ),
311 ],
312 timestamp: 1700000000000,
313 };
314 let json = serde_json::to_string(&ob).unwrap();
315 let parsed: HlOrderbook = serde_json::from_str(&json).unwrap();
316 assert_eq!(parsed.coin, "BTC");
317 assert_eq!(parsed.bids.len(), 2);
318 assert_eq!(parsed.asks.len(), 2);
319 assert_eq!(parsed.bids[0].0, Decimal::from_str("50000.0").unwrap());
320 assert_eq!(parsed.bids[0].1, Decimal::from_str("1.5").unwrap());
321 assert_eq!(parsed.timestamp, 1700000000000);
322 }
323
324 #[test]
325 fn orderbook_empty_levels_roundtrip() {
326 let ob = HlOrderbook {
327 coin: "SOL".into(),
328 bids: vec![],
329 asks: vec![],
330 timestamp: 0,
331 };
332 let json = serde_json::to_string(&ob).unwrap();
333 let parsed: HlOrderbook = serde_json::from_str(&json).unwrap();
334 assert!(parsed.bids.is_empty());
335 assert!(parsed.asks.is_empty());
336 }
337
338 #[test]
339 fn asset_info_serde_roundtrip() {
340 let info = HlAssetInfo {
341 coin: "BTC".into(),
342 asset_id: 0,
343 min_size: Decimal::from_str("0.001").unwrap(),
344 sz_decimals: 5,
345 px_decimals: 1,
346 };
347 let json = serde_json::to_string(&info).unwrap();
348 let parsed: HlAssetInfo = serde_json::from_str(&json).unwrap();
349 assert_eq!(parsed.coin, "BTC");
350 assert_eq!(parsed.asset_id, 0);
351 assert_eq!(parsed.min_size, Decimal::from_str("0.001").unwrap());
352 assert_eq!(parsed.sz_decimals, 5);
353 assert_eq!(parsed.px_decimals, 1);
354 }
355
356 #[test]
357 fn asset_info_camel_case_keys() {
358 let info = HlAssetInfo {
359 coin: "X".into(),
360 asset_id: 0,
361 min_size: Decimal::ZERO,
362 sz_decimals: 0,
363 px_decimals: 0,
364 };
365 let json = serde_json::to_string(&info).unwrap();
366 assert!(json.contains("assetId"));
367 assert!(json.contains("minSize"));
368 assert!(json.contains("szDecimals"));
369 assert!(json.contains("pxDecimals"));
370 }
371
372 #[test]
373 fn funding_rate_serde_roundtrip() {
374 let fr = HlFundingRate {
375 coin: "ETH".into(),
376 funding_rate: Decimal::from_str("0.0001").unwrap(),
377 next_funding_time: 1700003600000,
378 };
379 let json = serde_json::to_string(&fr).unwrap();
380 let parsed: HlFundingRate = serde_json::from_str(&json).unwrap();
381 assert_eq!(parsed.coin, "ETH");
382 assert_eq!(parsed.funding_rate, Decimal::from_str("0.0001").unwrap());
383 assert_eq!(parsed.next_funding_time, 1700003600000);
384 }
385
386 #[test]
387 fn funding_rate_camel_case_keys() {
388 let fr = HlFundingRate {
389 coin: "X".into(),
390 funding_rate: Decimal::ZERO,
391 next_funding_time: 0,
392 };
393 let json = serde_json::to_string(&fr).unwrap();
394 assert!(json.contains("fundingRate"));
395 assert!(json.contains("nextFundingTime"));
396 }
397
398 #[test]
399 fn spot_asset_info_serde_roundtrip() {
400 let info = HlSpotAssetInfo {
401 name: "PURR".into(),
402 index: 1,
403 sz_decimals: 0,
404 wei_decimals: 18,
405 };
406 let json = serde_json::to_string(&info).unwrap();
407 let parsed: HlSpotAssetInfo = serde_json::from_str(&json).unwrap();
408 assert_eq!(parsed.name, "PURR");
409 assert_eq!(parsed.index, 1);
410 assert_eq!(parsed.sz_decimals, 0);
411 assert_eq!(parsed.wei_decimals, 18);
412 }
413
414 #[test]
415 fn spot_asset_info_camel_case_keys() {
416 let info = HlSpotAssetInfo {
417 name: "X".into(),
418 index: 0,
419 sz_decimals: 0,
420 wei_decimals: 0,
421 };
422 let json = serde_json::to_string(&info).unwrap();
423 assert!(json.contains("szDecimals"));
424 assert!(json.contains("weiDecimals"));
425 }
426
427 #[test]
428 fn spot_meta_serde_roundtrip() {
429 let meta = HlSpotMeta {
430 tokens: vec![HlSpotAssetInfo::new("PURR".into(), 1, 0, 18)],
431 };
432 let json = serde_json::to_string(&meta).unwrap();
433 let parsed: HlSpotMeta = serde_json::from_str(&json).unwrap();
434 assert_eq!(parsed.tokens.len(), 1);
435 assert_eq!(parsed.tokens[0].name, "PURR");
436 }
437
438 #[test]
439 fn spot_balance_serde_roundtrip() {
440 let bal = HlSpotBalance {
441 coin: "PURR".into(),
442 token: 1,
443 hold: Decimal::ZERO,
444 total: Decimal::from_str("1000.0").unwrap(),
445 };
446 let json = serde_json::to_string(&bal).unwrap();
447 let parsed: HlSpotBalance = serde_json::from_str(&json).unwrap();
448 assert_eq!(parsed.coin, "PURR");
449 assert_eq!(parsed.token, 1);
450 assert_eq!(parsed.hold, Decimal::ZERO);
451 assert_eq!(parsed.total, Decimal::from_str("1000.0").unwrap());
452 }
453
454 #[test]
455 fn spot_balance_camel_case_deserialize() {
456 let json = r#"{"coin":"PURR","token":1,"hold":"0","total":"500.0"}"#;
457 let parsed: HlSpotBalance = serde_json::from_str(json).unwrap();
458 assert_eq!(parsed.coin, "PURR");
459 assert_eq!(parsed.total, Decimal::from_str("500.0").unwrap());
460 }
461
462 #[test]
463 fn asset_context_serde_roundtrip() {
464 let ctx = AssetContext {
465 funding: Decimal::from_str("0.0001").unwrap(),
466 open_interest: Decimal::from_str("50000.0").unwrap(),
467 mark_px: Decimal::from_str("94000.0").unwrap(),
468 };
469 let json = serde_json::to_string(&ctx).unwrap();
470 let parsed: AssetContext = serde_json::from_str(&json).unwrap();
471 assert_eq!(parsed, ctx);
472 }
473
474 #[test]
475 fn asset_context_camel_case_keys() {
476 let ctx = AssetContext::new(Decimal::ZERO, Decimal::ZERO, Decimal::ZERO);
477 let json = serde_json::to_string(&ctx).unwrap();
478 assert!(json.contains("openInterest"));
479 assert!(json.contains("markPx"));
480 }
481
482 #[test]
483 fn asset_context_camel_case_deserialize() {
484 let json = r#"{"funding":"0.0001","openInterest":"50000.0","markPx":"94000.0"}"#;
485 let parsed: AssetContext = serde_json::from_str(json).unwrap();
486 assert_eq!(parsed.funding, Decimal::from_str("0.0001").unwrap());
487 assert_eq!(parsed.open_interest, Decimal::from_str("50000.0").unwrap());
488 assert_eq!(parsed.mark_px, Decimal::from_str("94000.0").unwrap());
489 }
490
491 #[test]
492 fn spot_asset_context_serde_roundtrip() {
493 let ctx = SpotAssetContext {
494 mark_px: Decimal::from_str("1.05").unwrap(),
495 mid_px: Decimal::from_str("1.04").unwrap(),
496 };
497 let json = serde_json::to_string(&ctx).unwrap();
498 let parsed: SpotAssetContext = serde_json::from_str(&json).unwrap();
499 assert_eq!(parsed, ctx);
500 }
501
502 #[test]
503 fn spot_asset_context_camel_case_keys() {
504 let ctx = SpotAssetContext::new(Decimal::ZERO, Decimal::ZERO);
505 let json = serde_json::to_string(&ctx).unwrap();
506 assert!(json.contains("markPx"));
507 assert!(json.contains("midPx"));
508 }
509
510 #[test]
511 fn spot_asset_context_camel_case_deserialize() {
512 let json = r#"{"markPx":"1.05","midPx":"1.04"}"#;
513 let parsed: SpotAssetContext = serde_json::from_str(json).unwrap();
514 assert_eq!(parsed.mark_px, Decimal::from_str("1.05").unwrap());
515 assert_eq!(parsed.mid_px, Decimal::from_str("1.04").unwrap());
516 }
517
518 #[test]
519 fn perp_dex_status_serde_roundtrip() {
520 let status = HlPerpDexStatus {
521 name: "HyperBTC".into(),
522 is_active: true,
523 num_assets: 5,
524 total_oi: Decimal::from_str("1000000.0").unwrap(),
525 };
526 let json = serde_json::to_string(&status).unwrap();
527 let parsed: HlPerpDexStatus = serde_json::from_str(&json).unwrap();
528 assert_eq!(parsed.name, "HyperBTC");
529 assert!(parsed.is_active);
530 assert_eq!(parsed.num_assets, 5);
531 assert_eq!(parsed.total_oi, Decimal::from_str("1000000.0").unwrap());
532 }
533
534 #[test]
535 fn perp_dex_status_camel_case_keys() {
536 let status = HlPerpDexStatus {
537 name: "X".into(),
538 is_active: false,
539 num_assets: 0,
540 total_oi: Decimal::ZERO,
541 };
542 let json = serde_json::to_string(&status).unwrap();
543 assert!(json.contains("isActive"));
544 assert!(json.contains("numAssets"));
545 assert!(json.contains("totalOi"));
546 }
547
548 #[test]
549 fn perp_dex_status_camel_case_deserialize() {
550 let json = r#"{"name":"TestDex","isActive":true,"numAssets":3,"totalOi":"500000.0"}"#;
551 let parsed: HlPerpDexStatus = serde_json::from_str(json).unwrap();
552 assert_eq!(parsed.name, "TestDex");
553 assert!(parsed.is_active);
554 assert_eq!(parsed.num_assets, 3);
555 assert_eq!(parsed.total_oi, Decimal::from_str("500000.0").unwrap());
556 }
557}