use std::{collections::BTreeMap, time::Instant};
use rust_decimal::Decimal;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct BookEntry {
pub price: Decimal,
pub quantity: Decimal,
}
impl BookEntry {
pub fn new(price: Decimal, quantity: Decimal) -> Self {
Self {
price,
quantity,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct BookData {
pub bids: BTreeMap<Decimal, Decimal>,
pub asks: BTreeMap<Decimal, Decimal>,
pub last_update: Option<Instant>,
}
impl BookData {
pub fn new() -> Self { Self::default() }
pub fn best_bid(&self) -> Option<(Decimal, Decimal)> {
self.bids.iter().next_back().map(|(&p, &q)| (p, q))
}
pub fn best_ask(&self) -> Option<(Decimal, Decimal)> {
self.asks.iter().next().map(|(&p, &q)| (p, q))
}
pub fn spread(&self) -> Option<Decimal> {
match (self.best_bid(), self.best_ask()) {
(Some((bid, _)), Some((ask, _))) => Some(ask - bid),
_ => None,
}
}
pub fn update_bid(&mut self, price: Decimal, qty: Decimal) {
if qty.is_zero() {
self.bids.remove(&price);
} else {
self.bids.insert(price, qty);
}
self.last_update = Some(Instant::now());
}
pub fn update_ask(&mut self, price: Decimal, qty: Decimal) {
if qty.is_zero() {
self.asks.remove(&price);
} else {
self.asks.insert(price, qty);
}
self.last_update = Some(Instant::now());
}
pub fn clear(&mut self) {
self.bids.clear();
self.asks.clear();
self.last_update = None;
}
pub fn mid_price(&self) -> Option<Decimal> {
match (self.best_bid(), self.best_ask()) {
(Some((bid, _)), Some((ask, _))) => Some((bid + ask) / Decimal::from(2)),
_ => None,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct Level1Snapshot {
pub instrument_id: u64,
pub best_bid: Decimal,
pub best_ask: Decimal,
pub best_bid_qty: Decimal,
pub best_ask_qty: Decimal,
pub last_price: Decimal,
pub last_qty: Decimal,
pub volume: Decimal,
pub timestamp: Option<i64>,
}
impl Level1Snapshot {
pub fn mid_price(&self) -> Decimal {
(self.best_bid + self.best_ask) / Decimal::from(2)
}
pub fn spread(&self) -> Decimal { self.best_ask - self.best_bid }
}
#[derive(Debug, Clone, Default)]
pub struct TradeData {
pub trade_id: u64,
pub instrument_id: u64,
pub price: Decimal,
pub quantity: Decimal,
pub side: u8,
pub timestamp: i64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_book_data_bid_ordering() {
let mut book = BookData::new();
book.update_bid(Decimal::from(100), Decimal::from(1));
book.update_bid(Decimal::from(102), Decimal::from(2));
book.update_bid(Decimal::from(99), Decimal::from(3));
let (price, qty) = book.best_bid().unwrap();
assert_eq!(price, Decimal::from(102));
assert_eq!(qty, Decimal::from(2));
let prices: Vec<_> = book.bids.iter().rev().map(|(&p, _)| p).collect();
assert_eq!(
prices,
vec![Decimal::from(102), Decimal::from(100), Decimal::from(99)]
);
}
#[test]
fn test_book_data_ask_ordering() {
let mut book = BookData::new();
book.update_ask(Decimal::from(103), Decimal::from(1));
book.update_ask(Decimal::from(101), Decimal::from(2));
book.update_ask(Decimal::from(105), Decimal::from(3));
let (price, qty) = book.best_ask().unwrap();
assert_eq!(price, Decimal::from(101));
assert_eq!(qty, Decimal::from(2));
let prices: Vec<_> = book.asks.iter().map(|(&p, _)| p).collect();
assert_eq!(
prices,
vec![Decimal::from(101), Decimal::from(103), Decimal::from(105)]
);
}
#[test]
fn test_book_data_update_removes_zero_qty() {
let mut book = BookData::new();
book.update_bid(Decimal::from(100), Decimal::from(10));
assert_eq!(book.bids.len(), 1);
book.update_bid(Decimal::from(100), Decimal::ZERO);
assert!(book.bids.is_empty());
book.update_ask(Decimal::from(101), Decimal::from(5));
assert_eq!(book.asks.len(), 1);
book.update_ask(Decimal::from(101), Decimal::ZERO);
assert!(book.asks.is_empty());
}
#[test]
fn test_book_data_best_bid_ask() {
let mut book = BookData::new();
assert!(book.best_bid().is_none());
assert!(book.best_ask().is_none());
assert!(book.spread().is_none());
book.update_bid(Decimal::from(100), Decimal::from(10));
book.update_ask(Decimal::from(102), Decimal::from(5));
let (bid_price, bid_qty) = book.best_bid().unwrap();
assert_eq!(bid_price, Decimal::from(100));
assert_eq!(bid_qty, Decimal::from(10));
let (ask_price, ask_qty) = book.best_ask().unwrap();
assert_eq!(ask_price, Decimal::from(102));
assert_eq!(ask_qty, Decimal::from(5));
assert_eq!(book.spread(), Some(Decimal::from(2)));
assert_eq!(book.mid_price(), Some(Decimal::from(101)));
}
#[test]
fn test_book_data_clear() {
let mut book = BookData::new();
book.update_bid(Decimal::from(100), Decimal::from(10));
book.update_ask(Decimal::from(101), Decimal::from(5));
assert!(!book.bids.is_empty());
assert!(!book.asks.is_empty());
book.clear();
assert!(book.bids.is_empty());
assert!(book.asks.is_empty());
assert!(book.last_update.is_none());
}
#[test]
fn test_book_entry() {
let entry = BookEntry::new(Decimal::from(100), Decimal::from(10));
assert_eq!(entry.price, Decimal::from(100));
assert_eq!(entry.quantity, Decimal::from(10));
}
#[test]
fn test_level1_snapshot() {
let snapshot = Level1Snapshot {
instrument_id: 1,
best_bid: Decimal::from(100),
best_ask: Decimal::from(102),
best_bid_qty: Decimal::from(10),
best_ask_qty: Decimal::from(5),
last_price: Decimal::from(101),
last_qty: Decimal::from(1),
volume: Decimal::from(1000),
timestamp: Some(1234567890),
};
assert_eq!(snapshot.mid_price(), Decimal::from(101));
assert_eq!(snapshot.spread(), Decimal::from(2));
}
}