use rust_decimal::Decimal;
use serde::{Deserialize, Deserializer, Serialize};
use crate::Blockchain;
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Asset {
pub coingecko_id: Option<String>,
pub display_name: String,
pub symbol: String,
pub tokens: Vec<Token>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Ticker {
pub symbol: String,
pub first_price: Decimal,
pub last_price: Decimal,
pub price_change: Decimal,
pub price_change_percent: Decimal,
pub high: Decimal,
pub low: Decimal,
pub volume: Decimal,
pub trades: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TickerUpdate {
#[serde(rename = "e")]
pub event_type: String,
#[serde(rename = "E")]
pub event_time: i64,
#[serde(rename = "s")]
pub symbol: String,
#[serde(rename = "a")]
pub ask_price: Decimal,
#[serde(rename = "A")]
pub ask_quantity: Decimal,
#[serde(rename = "b")]
pub bid_price: Decimal,
#[serde(rename = "B")]
pub bid_quantity: Decimal,
#[serde(rename = "u")]
pub update_id: u64,
#[serde(rename = "T")]
pub timestamp: u64,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Market {
pub symbol: String,
pub base_symbol: String,
pub quote_symbol: String,
pub market_type: String,
pub filters: MarketFilters,
}
impl Market {
pub const fn price_decimal_places(&self) -> u32 {
self.filters.price.tick_size.scale()
}
pub const fn quantity_decimal_places(&self) -> u32 {
self.filters.quantity.step_size.scale()
}
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MarketFilters {
pub price: PriceFilter,
pub quantity: QuantityFilter,
pub leverage: Option<LeverageFilter>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PriceFilter {
pub min_price: Decimal,
pub max_price: Option<Decimal>,
pub tick_size: Decimal,
pub max_multiplier: Option<Decimal>,
pub min_multiplier: Option<Decimal>,
pub max_impact_multiplier: Option<Decimal>,
pub min_impact_multiplier: Option<Decimal>,
pub mean_mark_price_band: Option<PriceBandMarkPrice>,
pub mean_premium_band: Option<PriceBandPremium>,
pub borrow_entry_fee_max_multiplier: Option<Decimal>,
pub borrow_entry_fee_min_multiplier: Option<Decimal>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PriceBandMarkPrice {
pub max_multiplier: Decimal,
pub min_multiplier: Decimal,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PriceBandPremium {
pub index_price: Option<Decimal>,
pub max_premium_pct: Option<Decimal>,
pub min_premium_pct: Option<Decimal>,
pub tolerance_pct: Decimal,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct QuantityFilter {
pub min_quantity: Decimal,
pub max_quantity: Option<Decimal>,
pub step_size: Decimal,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LeverageFilter {
pub min_leverage: Decimal,
pub max_leverage: Decimal,
pub step_size: Decimal,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Token {
pub blockchain: Blockchain,
pub contract_address: String,
pub deposit_enabled: bool,
pub display_name: String,
pub minimum_deposit: Decimal,
pub withdraw_enabled: bool,
pub minimum_withdrawal: Decimal,
pub maximum_withdrawal: Option<Decimal>,
pub withdrawal_fee: Decimal,
}
#[derive(Debug, Serialize, Deserialize, strum::AsRefStr)]
pub enum OrderBookDepthLimit {
#[serde(rename = "5")]
#[strum(serialize = "5")]
Five,
#[serde(rename = "10")]
#[strum(serialize = "10")]
Ten,
#[serde(rename = "20")]
#[strum(serialize = "20")]
Twenty,
#[serde(rename = "50")]
#[strum(serialize = "50")]
Fifty,
#[serde(rename = "100")]
#[strum(serialize = "100")]
OneHundred,
#[serde(rename = "500")]
#[strum(serialize = "500")]
FiveHundred,
#[serde(rename = "1000")]
#[strum(serialize = "1000")]
OneThousand,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OrderBookDepth {
pub asks: Vec<(Decimal, Decimal)>,
pub bids: Vec<(Decimal, Decimal)>,
#[serde(deserialize_with = "deserialize_str_or_i64")]
pub last_update_id: i64,
pub timestamp: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OrderBookDepthUpdate {
#[serde(rename = "e")]
pub event_type: String,
#[serde(rename = "E")]
pub event_time: i64,
#[serde(rename = "s")]
pub symbol: String,
#[serde(rename = "T")]
pub timestamp: i64,
#[serde(rename = "U")]
pub first_update_id: i64,
#[serde(rename = "u")]
pub last_update_id: i64,
#[serde(rename = "a")]
pub asks: Vec<(Decimal, Decimal)>,
#[serde(rename = "b")]
pub bids: Vec<(Decimal, Decimal)>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Kline {
pub start: String,
pub open: Option<Decimal>,
pub high: Option<Decimal>,
pub low: Option<Decimal>,
pub close: Option<Decimal>,
pub end: Option<String>,
pub volume: Decimal,
pub trades: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KlineUpdate {
#[serde(rename = "e")]
pub event_type: String,
#[serde(rename = "E")]
pub event_time: i64,
#[serde(rename = "s")]
pub symbol: String,
#[serde(rename = "t")]
pub start_time: i64,
#[serde(rename = "T")]
pub end_time: i64,
#[serde(rename = "o")]
pub open_price: Decimal,
#[serde(rename = "c")]
pub close_price: Decimal,
#[serde(rename = "h")]
pub high_price: Decimal,
#[serde(rename = "l")]
pub low_price: Decimal,
#[serde(rename = "v")]
pub volume: Decimal,
#[serde(rename = "n")]
pub trades: u64,
#[serde(rename = "X")]
pub is_closed: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FundingRate {
pub symbol: String,
pub interval_end_timestamp: String,
pub funding_rate: Decimal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MarkPrice {
pub symbol: String,
pub funding_rate: Decimal,
pub index_price: Decimal,
pub mark_price: Decimal,
pub next_funding_timestamp: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MarkPriceUpdate {
#[serde(rename = "e")]
pub event_type: String,
#[serde(rename = "E")]
pub event_time: i64,
#[serde(rename = "s")]
pub symbol: String,
#[serde(rename = "p")]
pub mark_price: Decimal,
#[serde(rename = "f")]
pub funding_rate: Decimal,
#[serde(rename = "i")]
pub index_price: Decimal,
#[serde(rename = "n")]
pub funding_timestamp: u64,
#[serde(rename = "T")]
pub engine_timestamp: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Security {
pub asset: String,
pub name: String,
pub min_quantity: Decimal,
pub max_quantity: Option<Decimal>,
pub step_size: Decimal,
pub cusip: Option<String>,
}
impl TryFrom<u32> for OrderBookDepthLimit {
type Error = &'static str;
fn try_from(value: u32) -> Result<Self, Self::Error> {
match value {
5 => Ok(OrderBookDepthLimit::Five),
10 => Ok(OrderBookDepthLimit::Ten),
20 => Ok(OrderBookDepthLimit::Twenty),
50 => Ok(OrderBookDepthLimit::Fifty),
100 => Ok(OrderBookDepthLimit::OneHundred),
500 => Ok(OrderBookDepthLimit::FiveHundred),
1000 => Ok(OrderBookDepthLimit::OneThousand),
_ => Err("Invalid OrderBookDepthLimit value"),
}
}
}
fn deserialize_str_or_i64<'de, D>(deserializer: D) -> Result<i64, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::Visitor;
use std::fmt;
struct StringOrI64Visitor;
impl<'de> Visitor<'de> for StringOrI64Visitor {
type Value = i64;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a string or an integer")
}
fn visit_str<E>(self, value: &str) -> Result<i64, E>
where
E: serde::de::Error,
{
value.parse().map_err(serde::de::Error::custom)
}
fn visit_i64<E>(self, value: i64) -> Result<i64, E>
where
E: serde::de::Error,
{
Ok(value)
}
fn visit_u64<E>(self, value: u64) -> Result<i64, E>
where
E: serde::de::Error,
{
i64::try_from(value).map_err(|_| serde::de::Error::custom("value too large"))
}
}
deserializer.deserialize_any(StringOrI64Visitor)
}
#[cfg(test)]
mod test {
use super::*;
use rust_decimal_macros::dec;
fn get_test_market() -> Market {
Market {
symbol: "TEST_MARKET".to_string(),
base_symbol: "TEST".to_string(),
quote_symbol: "MARKET".to_string(),
market_type: "SPOT".to_string(),
filters: super::MarketFilters {
price: PriceFilter {
min_price: dec!(0.0001),
max_price: None,
tick_size: dec!(0.0001),
min_multiplier: Some(dec!(1.25)),
max_multiplier: Some(dec!(0.75)),
max_impact_multiplier: Some(dec!(1.05)),
min_impact_multiplier: Some(dec!(0.95)),
mean_mark_price_band: None,
mean_premium_band: None,
borrow_entry_fee_max_multiplier: None,
borrow_entry_fee_min_multiplier: None,
},
quantity: QuantityFilter {
min_quantity: dec!(0.01),
max_quantity: None,
step_size: dec!(0.01),
},
leverage: None,
},
}
}
#[test]
fn test_decimal_places_on_price_filters_4() {
let market = get_test_market();
assert_eq!(market.price_decimal_places(), 4);
}
#[test]
fn test_decimal_places_on_quantity_filters() {
let market = get_test_market();
assert_eq!(market.quantity_decimal_places(), 2);
}
#[test]
fn test_mark_price_update_parse() {
let data = r#"
{
"E": 1747291031914525,
"T": 1747291031910025,
"e": "markPrice",
"f": "-0.0000039641039274236048482914",
"i": "173.44031179",
"n": 1747296000000,
"p": "173.35998175",
"s": "SOL_USDC_PERP"
}
"#;
let mark_price_update: MarkPriceUpdate = serde_json::from_str(data).unwrap();
assert_eq!(mark_price_update.symbol, "SOL_USDC_PERP".to_string());
assert_eq!(
mark_price_update.funding_rate,
dec!(-0.0000039641039274236048482914)
);
assert_eq!(mark_price_update.mark_price, dec!(173.35998175));
}
#[test]
fn test_kline_update_parse() {
let data = r#"
{
"e": "kline",
"E": 1694687692980000,
"s": "SOL_USD",
"t": 123400000,
"T": 123460000,
"o": "18.75",
"c": "19.25",
"h": "19.80",
"l": "18.50",
"v": "32123",
"n": 93828,
"X": false
}
"#;
let kline_update: KlineUpdate = serde_json::from_str(data).unwrap();
assert_eq!(kline_update.symbol, "SOL_USD".to_string());
assert_eq!(kline_update.start_time, 123400000);
assert_eq!(kline_update.open_price, dec!(18.75));
}
#[test]
fn test_order_book_depth_last_update_id_as_string() {
let data = r#"
{
"asks": [["18.70", "0.000"]],
"bids": [["18.67", "0.832"]],
"lastUpdateId": "94978271",
"timestamp": 1694687965941000
}
"#;
let depth: OrderBookDepth = serde_json::from_str(data).unwrap();
assert_eq!(depth.last_update_id, 94978271);
}
#[test]
fn test_order_book_depth_last_update_id_as_i64() {
let data = r#"
{
"asks": [["18.70", "0.000"]],
"bids": [["18.67", "0.832"]],
"lastUpdateId": 94978271,
"timestamp": 1694687965941000
}
"#;
let depth: OrderBookDepth = serde_json::from_str(data).unwrap();
assert_eq!(depth.last_update_id, 94978271);
}
}