use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct HlOrderbook {
pub coin: String,
pub bids: Vec<(Decimal, Decimal)>,
pub asks: Vec<(Decimal, Decimal)>,
pub timestamp: u64,
}
impl HlOrderbook {
pub fn new(
coin: String,
bids: Vec<(Decimal, Decimal)>,
asks: Vec<(Decimal, Decimal)>,
timestamp: u64,
) -> Self {
Self {
coin,
bids,
asks,
timestamp,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct HlAssetInfo {
pub coin: String,
pub asset_id: u32,
pub min_size: Decimal,
pub sz_decimals: u32,
pub px_decimals: u32,
}
impl HlAssetInfo {
pub fn new(
coin: String,
asset_id: u32,
min_size: Decimal,
sz_decimals: u32,
px_decimals: u32,
) -> Self {
Self {
coin,
asset_id,
min_size,
sz_decimals,
px_decimals,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct HlFundingRate {
pub coin: String,
pub funding_rate: Decimal,
pub next_funding_time: u64,
}
impl HlFundingRate {
pub fn new(coin: String, funding_rate: Decimal, next_funding_time: u64) -> Self {
Self {
coin,
funding_rate,
next_funding_time,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct HlSpotAssetInfo {
pub name: String,
pub index: u32,
pub sz_decimals: u32,
pub wei_decimals: u32,
}
impl HlSpotAssetInfo {
pub fn new(name: String, index: u32, sz_decimals: u32, wei_decimals: u32) -> Self {
Self {
name,
index,
sz_decimals,
wei_decimals,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct HlSpotMeta {
pub tokens: Vec<HlSpotAssetInfo>,
}
impl HlSpotMeta {
pub fn new(tokens: Vec<HlSpotAssetInfo>) -> Self {
Self { tokens }
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct AssetContext {
pub funding: Decimal,
pub open_interest: Decimal,
pub mark_px: Decimal,
}
impl AssetContext {
pub fn new(funding: Decimal, open_interest: Decimal, mark_px: Decimal) -> Self {
Self {
funding,
open_interest,
mark_px,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct SpotAssetContext {
pub mark_px: Decimal,
pub mid_px: Decimal,
}
impl SpotAssetContext {
pub fn new(mark_px: Decimal, mid_px: Decimal) -> Self {
Self { mark_px, mid_px }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum TradeSide {
#[serde(rename = "B")]
Buy,
#[serde(rename = "A")]
Sell,
}
impl std::fmt::Display for TradeSide {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TradeSide::Buy => write!(f, "B"),
TradeSide::Sell => write!(f, "A"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct HlTrade {
pub coin: String,
pub side: TradeSide,
pub px: Decimal,
pub sz: Decimal,
pub time: u64,
}
impl HlTrade {
pub fn new(coin: String, side: TradeSide, px: Decimal, sz: Decimal, time: u64) -> Self {
Self {
coin,
side,
px,
sz,
time,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct HlSpotBalance {
pub coin: String,
pub token: u32,
pub hold: Decimal,
pub total: Decimal,
}
impl HlSpotBalance {
pub fn new(coin: String, token: u32, hold: Decimal, total: Decimal) -> Self {
Self {
coin,
token,
hold,
total,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct HlPerpDexStatus {
pub name: String,
pub is_active: bool,
pub num_assets: u32,
pub total_oi: Decimal,
}
impl HlPerpDexStatus {
pub fn new(name: String, is_active: bool, num_assets: u32, total_oi: Decimal) -> Self {
Self {
name,
is_active,
num_assets,
total_oi,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[test]
fn orderbook_serde_roundtrip() {
let ob = HlOrderbook {
coin: "BTC".into(),
bids: vec![
(
Decimal::from_str("50000.0").unwrap(),
Decimal::from_str("1.5").unwrap(),
),
(
Decimal::from_str("49999.0").unwrap(),
Decimal::from_str("2.0").unwrap(),
),
],
asks: vec![
(
Decimal::from_str("50001.0").unwrap(),
Decimal::from_str("0.5").unwrap(),
),
(
Decimal::from_str("50002.0").unwrap(),
Decimal::from_str("3.0").unwrap(),
),
],
timestamp: 1700000000000,
};
let json = serde_json::to_string(&ob).unwrap();
let parsed: HlOrderbook = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.coin, "BTC");
assert_eq!(parsed.bids.len(), 2);
assert_eq!(parsed.asks.len(), 2);
assert_eq!(parsed.bids[0].0, Decimal::from_str("50000.0").unwrap());
assert_eq!(parsed.bids[0].1, Decimal::from_str("1.5").unwrap());
assert_eq!(parsed.timestamp, 1700000000000);
}
#[test]
fn orderbook_empty_levels_roundtrip() {
let ob = HlOrderbook {
coin: "SOL".into(),
bids: vec![],
asks: vec![],
timestamp: 0,
};
let json = serde_json::to_string(&ob).unwrap();
let parsed: HlOrderbook = serde_json::from_str(&json).unwrap();
assert!(parsed.bids.is_empty());
assert!(parsed.asks.is_empty());
}
#[test]
fn asset_info_serde_roundtrip() {
let info = HlAssetInfo {
coin: "BTC".into(),
asset_id: 0,
min_size: Decimal::from_str("0.001").unwrap(),
sz_decimals: 5,
px_decimals: 1,
};
let json = serde_json::to_string(&info).unwrap();
let parsed: HlAssetInfo = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.coin, "BTC");
assert_eq!(parsed.asset_id, 0);
assert_eq!(parsed.min_size, Decimal::from_str("0.001").unwrap());
assert_eq!(parsed.sz_decimals, 5);
assert_eq!(parsed.px_decimals, 1);
}
#[test]
fn asset_info_camel_case_keys() {
let info = HlAssetInfo {
coin: "X".into(),
asset_id: 0,
min_size: Decimal::ZERO,
sz_decimals: 0,
px_decimals: 0,
};
let json = serde_json::to_string(&info).unwrap();
assert!(json.contains("assetId"));
assert!(json.contains("minSize"));
assert!(json.contains("szDecimals"));
assert!(json.contains("pxDecimals"));
}
#[test]
fn funding_rate_serde_roundtrip() {
let fr = HlFundingRate {
coin: "ETH".into(),
funding_rate: Decimal::from_str("0.0001").unwrap(),
next_funding_time: 1700003600000,
};
let json = serde_json::to_string(&fr).unwrap();
let parsed: HlFundingRate = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.coin, "ETH");
assert_eq!(parsed.funding_rate, Decimal::from_str("0.0001").unwrap());
assert_eq!(parsed.next_funding_time, 1700003600000);
}
#[test]
fn funding_rate_camel_case_keys() {
let fr = HlFundingRate {
coin: "X".into(),
funding_rate: Decimal::ZERO,
next_funding_time: 0,
};
let json = serde_json::to_string(&fr).unwrap();
assert!(json.contains("fundingRate"));
assert!(json.contains("nextFundingTime"));
}
#[test]
fn spot_asset_info_serde_roundtrip() {
let info = HlSpotAssetInfo {
name: "PURR".into(),
index: 1,
sz_decimals: 0,
wei_decimals: 18,
};
let json = serde_json::to_string(&info).unwrap();
let parsed: HlSpotAssetInfo = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.name, "PURR");
assert_eq!(parsed.index, 1);
assert_eq!(parsed.sz_decimals, 0);
assert_eq!(parsed.wei_decimals, 18);
}
#[test]
fn spot_asset_info_camel_case_keys() {
let info = HlSpotAssetInfo {
name: "X".into(),
index: 0,
sz_decimals: 0,
wei_decimals: 0,
};
let json = serde_json::to_string(&info).unwrap();
assert!(json.contains("szDecimals"));
assert!(json.contains("weiDecimals"));
}
#[test]
fn spot_meta_serde_roundtrip() {
let meta = HlSpotMeta {
tokens: vec![HlSpotAssetInfo::new("PURR".into(), 1, 0, 18)],
};
let json = serde_json::to_string(&meta).unwrap();
let parsed: HlSpotMeta = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.tokens.len(), 1);
assert_eq!(parsed.tokens[0].name, "PURR");
}
#[test]
fn spot_balance_serde_roundtrip() {
let bal = HlSpotBalance {
coin: "PURR".into(),
token: 1,
hold: Decimal::ZERO,
total: Decimal::from_str("1000.0").unwrap(),
};
let json = serde_json::to_string(&bal).unwrap();
let parsed: HlSpotBalance = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.coin, "PURR");
assert_eq!(parsed.token, 1);
assert_eq!(parsed.hold, Decimal::ZERO);
assert_eq!(parsed.total, Decimal::from_str("1000.0").unwrap());
}
#[test]
fn spot_balance_camel_case_deserialize() {
let json = r#"{"coin":"PURR","token":1,"hold":"0","total":"500.0"}"#;
let parsed: HlSpotBalance = serde_json::from_str(json).unwrap();
assert_eq!(parsed.coin, "PURR");
assert_eq!(parsed.total, Decimal::from_str("500.0").unwrap());
}
#[test]
fn asset_context_serde_roundtrip() {
let ctx = AssetContext {
funding: Decimal::from_str("0.0001").unwrap(),
open_interest: Decimal::from_str("50000.0").unwrap(),
mark_px: Decimal::from_str("94000.0").unwrap(),
};
let json = serde_json::to_string(&ctx).unwrap();
let parsed: AssetContext = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, ctx);
}
#[test]
fn asset_context_camel_case_keys() {
let ctx = AssetContext::new(Decimal::ZERO, Decimal::ZERO, Decimal::ZERO);
let json = serde_json::to_string(&ctx).unwrap();
assert!(json.contains("openInterest"));
assert!(json.contains("markPx"));
}
#[test]
fn asset_context_camel_case_deserialize() {
let json = r#"{"funding":"0.0001","openInterest":"50000.0","markPx":"94000.0"}"#;
let parsed: AssetContext = serde_json::from_str(json).unwrap();
assert_eq!(parsed.funding, Decimal::from_str("0.0001").unwrap());
assert_eq!(parsed.open_interest, Decimal::from_str("50000.0").unwrap());
assert_eq!(parsed.mark_px, Decimal::from_str("94000.0").unwrap());
}
#[test]
fn spot_asset_context_serde_roundtrip() {
let ctx = SpotAssetContext {
mark_px: Decimal::from_str("1.05").unwrap(),
mid_px: Decimal::from_str("1.04").unwrap(),
};
let json = serde_json::to_string(&ctx).unwrap();
let parsed: SpotAssetContext = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, ctx);
}
#[test]
fn spot_asset_context_camel_case_keys() {
let ctx = SpotAssetContext::new(Decimal::ZERO, Decimal::ZERO);
let json = serde_json::to_string(&ctx).unwrap();
assert!(json.contains("markPx"));
assert!(json.contains("midPx"));
}
#[test]
fn spot_asset_context_camel_case_deserialize() {
let json = r#"{"markPx":"1.05","midPx":"1.04"}"#;
let parsed: SpotAssetContext = serde_json::from_str(json).unwrap();
assert_eq!(parsed.mark_px, Decimal::from_str("1.05").unwrap());
assert_eq!(parsed.mid_px, Decimal::from_str("1.04").unwrap());
}
#[test]
fn perp_dex_status_serde_roundtrip() {
let status = HlPerpDexStatus {
name: "HyperBTC".into(),
is_active: true,
num_assets: 5,
total_oi: Decimal::from_str("1000000.0").unwrap(),
};
let json = serde_json::to_string(&status).unwrap();
let parsed: HlPerpDexStatus = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.name, "HyperBTC");
assert!(parsed.is_active);
assert_eq!(parsed.num_assets, 5);
assert_eq!(parsed.total_oi, Decimal::from_str("1000000.0").unwrap());
}
#[test]
fn perp_dex_status_camel_case_keys() {
let status = HlPerpDexStatus {
name: "X".into(),
is_active: false,
num_assets: 0,
total_oi: Decimal::ZERO,
};
let json = serde_json::to_string(&status).unwrap();
assert!(json.contains("isActive"));
assert!(json.contains("numAssets"));
assert!(json.contains("totalOi"));
}
#[test]
fn perp_dex_status_camel_case_deserialize() {
let json = r#"{"name":"TestDex","isActive":true,"numAssets":3,"totalOi":"500000.0"}"#;
let parsed: HlPerpDexStatus = serde_json::from_str(json).unwrap();
assert_eq!(parsed.name, "TestDex");
assert!(parsed.is_active);
assert_eq!(parsed.num_assets, 3);
assert_eq!(parsed.total_oi, Decimal::from_str("500000.0").unwrap());
}
}