use crate::{
checksum::{compute_checksum_with_precision, DEFAULT_PRICE_PRECISION, DEFAULT_QTY_PRECISION},
storage::TreeBook,
};
use kraken_types::{BookData, Level};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum OrderbookState {
#[default]
Uninitialized,
AwaitingSnapshot,
Synced,
Desynchronized,
}
#[derive(Debug, Clone)]
pub struct ChecksumMismatch {
pub symbol: String,
pub expected: u32,
pub computed: u32,
}
impl std::fmt::Display for ChecksumMismatch {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Checksum mismatch for {}: expected {}, computed {}",
self.symbol, self.expected, self.computed
)
}
}
impl std::error::Error for ChecksumMismatch {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ApplyResult {
Snapshot,
Update,
Ignored,
}
pub struct Orderbook {
symbol: String,
storage: TreeBook,
last_checksum: u32,
state: OrderbookState,
depth: u32,
price_precision: u8,
qty_precision: u8,
}
impl Orderbook {
pub fn new(symbol: impl Into<String>) -> Self {
Self {
symbol: symbol.into(),
storage: TreeBook::new(),
last_checksum: 0,
state: OrderbookState::Uninitialized,
depth: 10, price_precision: DEFAULT_PRICE_PRECISION,
qty_precision: DEFAULT_QTY_PRECISION,
}
}
pub fn with_depth(symbol: impl Into<String>, depth: u32) -> Self {
Self {
symbol: symbol.into(),
storage: TreeBook::new(),
last_checksum: 0,
state: OrderbookState::Uninitialized,
depth,
price_precision: DEFAULT_PRICE_PRECISION,
qty_precision: DEFAULT_QTY_PRECISION,
}
}
pub fn set_precision(&mut self, price_precision: u8, qty_precision: u8) {
self.price_precision = price_precision;
self.qty_precision = qty_precision;
}
pub fn price_precision(&self) -> u8 {
self.price_precision
}
pub fn qty_precision(&self) -> u8 {
self.qty_precision
}
pub fn symbol(&self) -> &str {
&self.symbol
}
pub fn state(&self) -> OrderbookState {
self.state
}
pub fn is_synced(&self) -> bool {
self.state == OrderbookState::Synced
}
pub fn last_checksum(&self) -> u32 {
self.last_checksum
}
pub fn depth(&self) -> u32 {
self.depth
}
pub fn best_bid(&self) -> Option<&Level> {
self.storage.best_bid()
}
pub fn best_ask(&self) -> Option<&Level> {
self.storage.best_ask()
}
pub fn spread(&self) -> Option<Decimal> {
match (self.best_ask(), self.best_bid()) {
(Some(ask), Some(bid)) => Some(ask.price - bid.price),
_ => None,
}
}
pub fn mid_price(&self) -> Option<Decimal> {
match (self.best_ask(), self.best_bid()) {
(Some(ask), Some(bid)) => Some((ask.price + bid.price) / Decimal::TWO),
_ => None,
}
}
pub fn bids_vec(&self) -> Vec<Level> {
self.storage.bids_vec()
}
pub fn asks_vec(&self) -> Vec<Level> {
self.storage.asks_vec()
}
pub fn top_bids(&self, n: usize) -> Vec<Level> {
self.storage.top_bids(n)
}
pub fn top_asks(&self, n: usize) -> Vec<Level> {
self.storage.top_asks(n)
}
pub fn bid_count(&self) -> usize {
self.storage.bid_count()
}
pub fn ask_count(&self) -> usize {
self.storage.ask_count()
}
pub fn set_awaiting_snapshot(&mut self) {
self.state = OrderbookState::AwaitingSnapshot;
}
pub fn apply_book_data(
&mut self,
data: &BookData,
is_snapshot: bool,
) -> Result<ApplyResult, ChecksumMismatch> {
if is_snapshot {
self.apply_snapshot_data(data)
} else {
self.apply_delta_data(data)
}
}
fn apply_snapshot_data(&mut self, data: &BookData) -> Result<ApplyResult, ChecksumMismatch> {
self.storage.clear();
for level in &data.bids {
self.storage.insert_bid(level.price, level.qty);
}
for level in &data.asks {
self.storage.insert_ask(level.price, level.qty);
}
self.storage.truncate(self.depth as usize);
self.validate_checksum(data.checksum)?;
self.state = OrderbookState::Synced;
Ok(ApplyResult::Snapshot)
}
fn apply_delta_data(&mut self, data: &BookData) -> Result<ApplyResult, ChecksumMismatch> {
if self.state != OrderbookState::Synced {
return Ok(ApplyResult::Ignored);
}
for level in &data.bids {
if level.qty.is_zero() {
self.storage.remove_bid(&level.price);
} else {
self.storage.insert_bid(level.price, level.qty);
}
}
for level in &data.asks {
if level.qty.is_zero() {
self.storage.remove_ask(&level.price);
} else {
self.storage.insert_ask(level.price, level.qty);
}
}
self.storage.truncate(self.depth as usize);
self.validate_checksum(data.checksum)?;
Ok(ApplyResult::Update)
}
fn validate_checksum(&mut self, expected: u32) -> Result<(), ChecksumMismatch> {
let bids = self.storage.bids_vec();
let asks = self.storage.asks_vec();
let computed = compute_checksum_with_precision(
&bids,
&asks,
self.price_precision,
self.qty_precision,
);
if computed != expected {
self.state = OrderbookState::Desynchronized;
return Err(ChecksumMismatch {
symbol: self.symbol.clone(),
expected,
computed,
});
}
self.last_checksum = expected;
Ok(())
}
pub fn reset(&mut self) {
self.storage.clear();
self.last_checksum = 0;
self.state = OrderbookState::Uninitialized;
}
pub fn snapshot(&self) -> OrderbookSnapshot {
OrderbookSnapshot {
symbol: self.symbol.clone(),
bids: self.bids_vec(),
asks: self.asks_vec(),
checksum: self.last_checksum,
state: self.state,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrderbookSnapshot {
pub symbol: String,
pub bids: Vec<Level>,
pub asks: Vec<Level>,
pub checksum: u32,
#[serde(skip)]
pub state: OrderbookState,
}
impl Default for OrderbookSnapshot {
fn default() -> Self {
Self {
symbol: String::new(),
bids: Vec::new(),
asks: Vec::new(),
checksum: 0,
state: OrderbookState::Uninitialized,
}
}
}
impl OrderbookSnapshot {
pub fn best_bid_price(&self) -> Option<Decimal> {
self.bids.first().map(|l| l.price)
}
pub fn best_ask_price(&self) -> Option<Decimal> {
self.asks.first().map(|l| l.price)
}
pub fn spread(&self) -> Option<Decimal> {
match (self.best_ask_price(), self.best_bid_price()) {
(Some(ask), Some(bid)) => Some(ask - bid),
_ => None,
}
}
pub fn mid_price(&self) -> Option<Decimal> {
match (self.best_ask_price(), self.best_bid_price()) {
(Some(ask), Some(bid)) => Some((ask + bid) / Decimal::TWO),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::checksum::compute_checksum;
use rust_decimal_macros::dec;
fn make_book_data(bids: Vec<(f64, f64)>, asks: Vec<(f64, f64)>) -> BookData {
let bids: Vec<Level> = bids
.into_iter()
.map(|(p, q)| Level::from_f64(p, q))
.collect();
let asks: Vec<Level> = asks
.into_iter()
.map(|(p, q)| Level::from_f64(p, q))
.collect();
let checksum = compute_checksum(&bids, &asks);
BookData {
symbol: "BTC/USD".to_string(),
bids,
asks,
checksum,
timestamp: None,
}
}
#[test]
fn test_orderbook_snapshot() {
let mut book = Orderbook::new("BTC/USD");
assert_eq!(book.state(), OrderbookState::Uninitialized);
let data = make_book_data(vec![(100.0, 1.0), (99.0, 2.0)], vec![(101.0, 1.0), (102.0, 2.0)]);
book.apply_book_data(&data, true).unwrap();
assert_eq!(book.state(), OrderbookState::Synced);
assert!(book.is_synced());
}
#[test]
fn test_orderbook_delta() {
let mut book = Orderbook::new("BTC/USD");
let snapshot = make_book_data(vec![(100.0, 1.0)], vec![(101.0, 1.0)]);
book.apply_book_data(&snapshot, true).unwrap();
let delta = make_book_data(vec![(100.0, 2.0)], vec![(101.0, 2.0)]);
book.apply_book_data(&delta, false).unwrap();
assert_eq!(book.best_bid().unwrap().qty, dec!(2));
assert_eq!(book.best_ask().unwrap().qty, dec!(2));
}
#[test]
fn test_spread_and_mid() {
let mut book = Orderbook::new("BTC/USD");
let data = make_book_data(vec![(100.0, 1.0)], vec![(102.0, 1.0)]);
book.apply_book_data(&data, true).unwrap();
assert_eq!(book.spread(), Some(dec!(2)));
assert_eq!(book.mid_price(), Some(dec!(101)));
}
#[test]
fn test_checksum_mismatch() {
let mut book = Orderbook::new("BTC/USD");
let mut data = make_book_data(vec![(100.0, 1.0)], vec![(101.0, 1.0)]);
data.checksum = 12345;
let result = book.apply_book_data(&data, true);
assert!(result.is_err());
assert_eq!(book.state(), OrderbookState::Desynchronized);
}
#[test]
fn test_reset() {
let mut book = Orderbook::new("BTC/USD");
let data = make_book_data(vec![(100.0, 1.0)], vec![(101.0, 1.0)]);
book.apply_book_data(&data, true).unwrap();
book.reset();
assert_eq!(book.state(), OrderbookState::Uninitialized);
assert_eq!(book.bid_count(), 0);
assert_eq!(book.ask_count(), 0);
}
}