use bitflags::bitflags;
use pricelevel::PriceLevelSnapshot;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use tracing::trace;
use super::error::OrderBookError;
use super::fees::FeeSchedule;
use super::stp::STPMode;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrderBookSnapshot {
pub symbol: String,
pub timestamp: u64,
pub bids: Vec<PriceLevelSnapshot>,
pub asks: Vec<PriceLevelSnapshot>,
}
impl OrderBookSnapshot {
pub fn refresh_aggregates(&mut self) {
for level in &mut self.bids {
let _ = level.refresh_aggregates();
}
for level in &mut self.asks {
let _ = level.refresh_aggregates();
}
}
pub fn best_bid(&self) -> Option<(u128, u64)> {
let bids = self
.bids
.iter()
.map(|level| (level.price(), level.visible_quantity()))
.max_by_key(|&(price, _)| price);
trace!("best_bid: {:?}", bids);
bids
}
pub fn best_ask(&self) -> Option<(u128, u64)> {
let ask = self
.asks
.iter()
.map(|level| (level.price(), level.visible_quantity()))
.min_by_key(|&(price, _)| price);
trace!("best_ask: {:?}", ask);
ask
}
pub fn mid_price(&self) -> Option<f64> {
let mid_price = match (self.best_bid(), self.best_ask()) {
(Some((bid_price, _)), Some((ask_price, _))) => {
Some((bid_price as f64 + ask_price as f64) / 2.0)
}
_ => None,
};
trace!("mid_price: {:?}", mid_price);
mid_price
}
pub fn spread(&self) -> Option<u128> {
let spread = match (self.best_bid(), self.best_ask()) {
(Some((bid_price, _)), Some((ask_price, _))) => {
Some(ask_price.saturating_sub(bid_price))
}
_ => None,
};
trace!("spread: {:?}", spread);
spread
}
pub fn total_bid_volume(&self) -> u64 {
let volume = self
.bids
.iter()
.map(|level| level.total_quantity().unwrap_or(0))
.sum();
trace!("total_bid_volume: {:?}", volume);
volume
}
pub fn total_ask_volume(&self) -> u64 {
let volume = self
.asks
.iter()
.map(|level| level.total_quantity().unwrap_or(0))
.sum();
trace!("total_ask_volume: {:?}", volume);
volume
}
pub fn total_bid_value(&self) -> u128 {
let value = self
.bids
.iter()
.map(|level| {
level
.price()
.saturating_mul(level.total_quantity().unwrap_or(0) as u128)
})
.sum();
trace!("total_bid_value: {:?}", value);
value
}
pub fn total_ask_value(&self) -> u128 {
let value = self
.asks
.iter()
.map(|level| {
level
.price()
.saturating_mul(level.total_quantity().unwrap_or(0) as u128)
})
.sum();
trace!("total_ask_value: {:?}", value);
value
}
}
pub const ORDERBOOK_SNAPSHOT_FORMAT_VERSION: u32 = 1;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrderBookSnapshotPackage {
pub version: u32,
pub snapshot: OrderBookSnapshot,
pub checksum: String,
#[serde(default)]
pub fee_schedule: Option<FeeSchedule>,
#[serde(default)]
pub stp_mode: STPMode,
#[serde(default)]
pub tick_size: Option<u128>,
#[serde(default)]
pub lot_size: Option<u64>,
#[serde(default)]
pub min_order_size: Option<u64>,
#[serde(default)]
pub max_order_size: Option<u64>,
}
impl OrderBookSnapshotPackage {
pub fn new(mut snapshot: OrderBookSnapshot) -> Result<Self, OrderBookError> {
snapshot.refresh_aggregates();
let checksum = Self::compute_checksum(&snapshot)?;
Ok(Self {
version: ORDERBOOK_SNAPSHOT_FORMAT_VERSION,
snapshot,
checksum,
fee_schedule: None,
stp_mode: STPMode::None,
tick_size: None,
lot_size: None,
min_order_size: None,
max_order_size: None,
})
}
pub fn to_json(&self) -> Result<String, OrderBookError> {
serde_json::to_string(self).map_err(|error| OrderBookError::SerializationError {
message: error.to_string(),
})
}
pub fn from_json(data: &str) -> Result<Self, OrderBookError> {
serde_json::from_str(data).map_err(|error| OrderBookError::DeserializationError {
message: error.to_string(),
})
}
pub fn validate(&self) -> Result<(), OrderBookError> {
if self.version != ORDERBOOK_SNAPSHOT_FORMAT_VERSION {
return Err(OrderBookError::InvalidOperation {
message: format!(
"Unsupported snapshot version: {} (expected {})",
self.version, ORDERBOOK_SNAPSHOT_FORMAT_VERSION
),
});
}
let computed = Self::compute_checksum(&self.snapshot)?;
if computed != self.checksum {
return Err(OrderBookError::ChecksumMismatch {
expected: self.checksum.clone(),
actual: computed,
});
}
Ok(())
}
pub fn into_snapshot(self) -> Result<OrderBookSnapshot, OrderBookError> {
self.validate()?;
Ok(self.snapshot)
}
fn compute_checksum(snapshot: &OrderBookSnapshot) -> Result<String, OrderBookError> {
let payload =
serde_json::to_vec(snapshot).map_err(|error| OrderBookError::SerializationError {
message: error.to_string(),
})?;
let mut hasher = Sha256::new();
hasher.update(payload);
let checksum_bytes = hasher.finalize();
let mut out = String::with_capacity(checksum_bytes.len() * 2);
for byte in checksum_bytes.iter() {
use std::fmt::Write;
write!(&mut out, "{byte:02x}").map_err(|error| OrderBookError::SerializationError {
message: error.to_string(),
})?;
}
Ok(out)
}
}
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct MetricFlags: u32 {
const MID_PRICE = 1 << 0;
const SPREAD = 1 << 1;
const DEPTH = 1 << 2;
const VWAP = 1 << 3;
const IMBALANCE = 1 << 4;
const ALL = Self::MID_PRICE.bits() | Self::SPREAD.bits()
| Self::DEPTH.bits() | Self::VWAP.bits() | Self::IMBALANCE.bits();
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EnrichedSnapshot {
pub symbol: String,
pub timestamp: u64,
pub bids: Vec<PriceLevelSnapshot>,
pub asks: Vec<PriceLevelSnapshot>,
pub mid_price: Option<f64>,
pub spread_bps: Option<f64>,
pub bid_depth_total: u64,
pub ask_depth_total: u64,
pub order_book_imbalance: f64,
pub vwap_bid: Option<f64>,
pub vwap_ask: Option<f64>,
}
impl EnrichedSnapshot {
pub fn new(
symbol: String,
timestamp: u64,
bids: Vec<PriceLevelSnapshot>,
asks: Vec<PriceLevelSnapshot>,
vwap_levels: usize,
imbalance_levels: usize,
) -> Self {
Self::with_metrics(
symbol,
timestamp,
bids,
asks,
vwap_levels,
imbalance_levels,
MetricFlags::ALL,
)
}
pub fn with_metrics(
symbol: String,
timestamp: u64,
bids: Vec<PriceLevelSnapshot>,
asks: Vec<PriceLevelSnapshot>,
vwap_levels: usize,
imbalance_levels: usize,
flags: MetricFlags,
) -> Self {
let mid_price = if flags.contains(MetricFlags::MID_PRICE) {
Self::calculate_mid_price(&bids, &asks)
} else {
None
};
let spread_bps = if flags.contains(MetricFlags::SPREAD) {
Self::calculate_spread_bps(&bids, &asks)
} else {
None
};
let (bid_depth_total, ask_depth_total) = if flags.contains(MetricFlags::DEPTH) {
(
Self::calculate_total_depth(&bids),
Self::calculate_total_depth(&asks),
)
} else {
(0, 0)
};
let (vwap_bid, vwap_ask) = if flags.contains(MetricFlags::VWAP) {
(
Self::calculate_vwap(&bids, vwap_levels),
Self::calculate_vwap(&asks, vwap_levels),
)
} else {
(None, None)
};
let order_book_imbalance = if flags.contains(MetricFlags::IMBALANCE) {
Self::calculate_imbalance(&bids, &asks, imbalance_levels)
} else {
0.0
};
Self {
symbol,
timestamp,
bids,
asks,
mid_price,
spread_bps,
bid_depth_total,
ask_depth_total,
order_book_imbalance,
vwap_bid,
vwap_ask,
}
}
fn calculate_mid_price(
bids: &[PriceLevelSnapshot],
asks: &[PriceLevelSnapshot],
) -> Option<f64> {
let best_bid = bids.first().map(|l| l.price())?;
let best_ask = asks.first().map(|l| l.price())?;
Some((best_bid as f64 + best_ask as f64) / 2.0)
}
fn calculate_spread_bps(
bids: &[PriceLevelSnapshot],
asks: &[PriceLevelSnapshot],
) -> Option<f64> {
let best_bid = bids.first().map(|l| l.price())? as f64;
let best_ask = asks.first().map(|l| l.price())? as f64;
let mid_price = (best_bid + best_ask) / 2.0;
if mid_price == 0.0 {
return None;
}
let spread = best_ask - best_bid;
Some((spread / mid_price) * 10000.0)
}
fn calculate_total_depth(levels: &[PriceLevelSnapshot]) -> u64 {
levels.iter().map(|l| l.total_quantity().unwrap_or(0)).sum()
}
fn calculate_vwap(levels: &[PriceLevelSnapshot], max_levels: usize) -> Option<f64> {
let levels_to_use = levels.iter().take(max_levels);
let mut total_value = 0u128;
let mut total_quantity = 0u64;
for level in levels_to_use {
let quantity = level.total_quantity().unwrap_or(0);
if quantity > 0 {
total_value =
total_value.saturating_add(level.price().saturating_mul(quantity as u128));
total_quantity = total_quantity.saturating_add(quantity);
}
}
if total_quantity == 0 {
None
} else {
Some(total_value as f64 / total_quantity as f64)
}
}
fn calculate_imbalance(
bids: &[PriceLevelSnapshot],
asks: &[PriceLevelSnapshot],
max_levels: usize,
) -> f64 {
let bid_volume: u64 = bids
.iter()
.take(max_levels)
.map(|l| l.total_quantity().unwrap_or(0))
.sum();
let ask_volume: u64 = asks
.iter()
.take(max_levels)
.map(|l| l.total_quantity().unwrap_or(0))
.sum();
let total = bid_volume + ask_volume;
if total == 0 {
0.0
} else {
(bid_volume as f64 - ask_volume as f64) / total as f64
}
}
}