use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use serde::Deserialize;
use smol_str::SmolStr;
#[derive(Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum MarketEvent {
Snapshot(OrderbookEvent),
#[serde(rename = "depth_batch")]
DepthBatch(OrderbookEvent),
Trade(TradeEvent),
Unknown,
}
#[derive(Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
pub struct OrderbookEvent {
#[serde(with = "chrono::serde::ts_milliseconds")]
pub ts: DateTime<Utc>,
pub symbol: SmolStr,
pub bids: Vec<OrderbookLevel>,
pub asks: Vec<OrderbookLevel>,
}
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct OrderbookLevel {
pub price: Decimal,
pub qty: Decimal,
}
#[derive(Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
pub struct TradeEvent {
#[serde(with = "chrono::serde::ts_milliseconds")]
pub ts: DateTime<Utc>,
pub symbol: SmolStr,
pub side: Side,
pub price: Decimal,
pub qty: Decimal,
}
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[serde(rename_all = "lowercase")]
pub enum Side {
Buy,
Sell,
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::TimeZone;
fn parse(json: &str) -> MarketEvent {
serde_json::from_str(json).expect("valid market event json")
}
#[test]
fn deserializes_snapshot() {
let json = r#"{"ts":1000000000,"symbol":"BTC-USDT","type":"snapshot","bids":[[10000000,5],[9999900,8]],"asks":[[10000100,4]]}"#;
let event = parse(json);
let MarketEvent::Snapshot(book) = event else {
panic!("expected snapshot variant");
};
assert_eq!(book.ts, Utc.timestamp_millis_opt(1_000_000_000).unwrap());
assert_eq!(book.symbol.as_str(), "BTC-USDT");
assert_eq!(book.bids.len(), 2);
assert_eq!(book.bids[0].price, Decimal::from(10_000_000));
assert_eq!(book.bids[0].qty, Decimal::from(5));
assert_eq!(book.asks[0].price, Decimal::from(10_000_100));
assert_eq!(book.asks[0].qty, Decimal::from(4));
}
#[test]
fn deserializes_depth_batch() {
let json = r#"{"ts":1000000100,"symbol":"BTC-USDT","type":"depth_batch","bids":[[10000000,7]],"asks":[[10000100,3]]}"#;
let event = parse(json);
let MarketEvent::DepthBatch(book) = event else {
panic!("expected depth_batch variant");
};
assert_eq!(book.symbol.as_str(), "BTC-USDT");
assert_eq!(book.bids[0].qty, Decimal::from(7));
}
#[test]
fn deserializes_trade() {
let json = r#"{"ts":1000000200,"symbol":"BTC-USDT","type":"trade","side":"buy","price":10000100,"qty":1}"#;
let event = parse(json);
let MarketEvent::Trade(trade) = event else {
panic!("expected trade variant");
};
assert_eq!(trade.side, Side::Buy);
assert_eq!(trade.price, Decimal::from(10_000_100));
assert_eq!(trade.qty, Decimal::from(1));
}
#[test]
fn deserializes_all_fixture_lines() {
let fixture = include_str!("../data/input.ndjson");
let events: Vec<MarketEvent> = fixture
.lines()
.filter(|line| !line.is_empty())
.map(parse)
.collect();
assert_eq!(events.len(), 10);
assert!(matches!(events[0], MarketEvent::Snapshot(_)));
assert!(matches!(events[2], MarketEvent::Trade(_)));
}
}