use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use super::enums::{MarketDataType, Side};
use super::types::{CanonicalSymbol, InstrumentId, Price, Quantity, Sequence, Timestamp, VenueId};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MarketDataEnvelope {
pub venue: VenueId,
pub instrument: InstrumentId,
pub canonical_symbol: CanonicalSymbol,
pub data_type: MarketDataType,
pub received_at: Timestamp,
pub exchange_timestamp: Option<Timestamp>,
pub sequence: Sequence,
pub payload: MarketDataPayload,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum MarketDataPayload {
Trade(Trade),
Ticker(Ticker),
L2Update(L2Update),
FundingRate(FundingRate),
Liquidation(Liquidation),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Trade {
pub price: Price,
pub quantity: Quantity,
pub side: Side,
pub trade_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Ticker {
pub bid_price: Price,
pub bid_qty: Quantity,
pub ask_price: Price,
pub ask_qty: Quantity,
pub last_price: Price,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct L2Update {
pub bids: Vec<(Price, Quantity)>,
pub asks: Vec<(Price, Quantity)>,
pub is_snapshot: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FundingRate {
pub rate: Decimal,
pub predicted_rate: Option<Decimal>,
pub next_funding_at: Timestamp,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Liquidation {
pub side: Side,
pub price: Price,
pub quantity: Quantity,
}
#[cfg(test)]
mod tests {
use rust_decimal_macros::dec;
use super::*;
#[test]
fn test_market_data_envelope_serde_roundtrip() {
let envelope = MarketDataEnvelope {
venue: VenueId::try_new("binance").unwrap(),
instrument: InstrumentId::try_new("BTCUSDT").unwrap(),
canonical_symbol: CanonicalSymbol::try_new("BTC/USDT").unwrap(),
data_type: MarketDataType::Trade,
received_at: Timestamp::new(1_700_000_000_000),
exchange_timestamp: Some(Timestamp::new(1_699_999_999_999)),
sequence: Sequence::new(42),
payload: MarketDataPayload::Trade(Trade {
price: Price::try_new(dec!(50000.50)).unwrap(),
quantity: Quantity::try_new(dec!(1.5)).unwrap(),
side: Side::Buy,
trade_id: Some("12345".to_owned()),
}),
};
let json = serde_json::to_string(&envelope).unwrap();
let deserialized: MarketDataEnvelope = serde_json::from_str(&json).unwrap();
assert_eq!(envelope, deserialized);
}
#[test]
fn test_l2_update_snapshot() {
let update = L2Update {
bids: vec![
(
Price::try_new(dec!(100)).unwrap(),
Quantity::try_new(dec!(10)).unwrap(),
),
(
Price::try_new(dec!(99)).unwrap(),
Quantity::try_new(dec!(20)).unwrap(),
),
],
asks: vec![(
Price::try_new(dec!(101)).unwrap(),
Quantity::try_new(dec!(5)).unwrap(),
)],
is_snapshot: true,
};
assert!(update.is_snapshot);
assert_eq!(update.bids.len(), 2);
}
#[test]
fn test_funding_rate_without_prediction() {
let fr = FundingRate {
rate: dec!(0.0001),
predicted_rate: None,
next_funding_at: Timestamp::new(1_700_000_000_000),
};
assert!(fr.predicted_rate.is_none());
}
#[test]
fn test_payload_variants_serde() {
let payloads = vec![
MarketDataPayload::Trade(Trade {
price: Price::try_new(dec!(100)).unwrap(),
quantity: Quantity::try_new(dec!(1)).unwrap(),
side: Side::Buy,
trade_id: None,
}),
MarketDataPayload::Ticker(Ticker {
bid_price: Price::try_new(dec!(99)).unwrap(),
bid_qty: Quantity::try_new(dec!(10)).unwrap(),
ask_price: Price::try_new(dec!(101)).unwrap(),
ask_qty: Quantity::try_new(dec!(5)).unwrap(),
last_price: Price::try_new(dec!(100)).unwrap(),
}),
MarketDataPayload::Liquidation(Liquidation {
side: Side::Sell,
price: Price::try_new(dec!(50000)).unwrap(),
quantity: Quantity::try_new(dec!(0.5)).unwrap(),
}),
];
for payload in payloads {
let json = serde_json::to_string(&payload).unwrap();
let parsed: MarketDataPayload = serde_json::from_str(&json).unwrap();
assert_eq!(payload, parsed);
}
}
}