use crate::core::errors::{Result, RustyQLibError};
use crate::core::quotes::Quote;
use crate::core::trade::Transection;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct DepthLevel {
pub price: f64,
pub size: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct MarketDepth {
bids: Vec<DepthLevel>,
asks: Vec<DepthLevel>,
}
fn validate_side(levels: &[DepthLevel], side: &str, descending: bool) -> Result<()> {
for level in levels {
if !level.price.is_finite() || !level.size.is_finite() || level.size <= 0.0 {
return Err(RustyQLibError::invalid_input(
"market depth",
format!("{side} level must have finite price and positive size, got {level:?}"),
));
}
}
let ordered = levels.windows(2).all(|w| {
if descending { w[1].price < w[0].price } else { w[1].price > w[0].price }
});
if !ordered {
return Err(RustyQLibError::invalid_input(
"market depth",
format!("{side} levels must be strictly best-first, got {levels:?}"),
));
}
Ok(())
}
impl MarketDepth {
pub fn new(bids: Vec<DepthLevel>, asks: Vec<DepthLevel>) -> Result<Self> {
validate_side(&bids, "bid", true)?;
validate_side(&asks, "ask", false)?;
if let (Some(bid), Some(ask)) = (bids.first(), asks.first()) {
if bid.price > ask.price {
return Err(RustyQLibError::invalid_input(
"market depth",
format!("crossed book: best bid {} > best ask {}", bid.price, ask.price),
));
}
}
Ok(MarketDepth { bids, asks })
}
pub fn bids(&self) -> &[DepthLevel] {
&self.bids
}
pub fn asks(&self) -> &[DepthLevel] {
&self.asks
}
pub fn best_bid(&self) -> Option<DepthLevel> {
self.bids.first().copied()
}
pub fn best_ask(&self) -> Option<DepthLevel> {
self.asks.first().copied()
}
pub fn mid(&self) -> Option<f64> {
Some(0.5 * (self.best_bid()?.price + self.best_ask()?.price))
}
pub fn vwap_for_size(&self, side: Transection, quantity: f64) -> Option<f64> {
if !(quantity > 0.0) || !quantity.is_finite() {
return None;
}
let levels = match side {
Transection::Buy => &self.asks,
Transection::Sell => &self.bids,
};
let mut remaining = quantity;
let mut cost = 0.0;
for level in levels {
let fill = remaining.min(level.size);
cost += fill * level.price;
remaining -= fill;
if remaining <= 0.0 {
return Some(cost / quantity);
}
}
None
}
pub fn slippage_for_size(&self, side: Transection, quantity: f64) -> Option<f64> {
let mid = self.mid()?;
let vwap = self.vwap_for_size(side.clone(), quantity)?;
Some(match side {
Transection::Buy => vwap - mid,
Transection::Sell => mid - vwap,
})
}
pub fn to_quote(&self) -> Option<Quote> {
let bid = self.best_bid()?;
let ask = self.best_ask()?;
Quote::from_bid_ask_sized(bid.price, bid.size, ask.price, ask.size).ok()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn level(price: f64, size: f64) -> DepthLevel {
DepthLevel { price, size }
}
fn book() -> MarketDepth {
MarketDepth::new(
vec![level(99.0, 100.0), level(98.5, 200.0), level(98.0, 500.0)],
vec![level(101.0, 150.0), level(101.5, 300.0), level(102.0, 400.0)],
)
.unwrap()
}
#[test]
fn construction_validates_ordering_sizes_and_crossing() {
assert!(MarketDepth::new(vec![level(98.0, 1.0), level(99.0, 1.0)], vec![]).is_err());
assert!(MarketDepth::new(vec![], vec![level(102.0, 1.0), level(101.0, 1.0)]).is_err());
assert!(
MarketDepth::new(vec![level(101.5, 1.0)], vec![level(101.0, 1.0)]).is_err()
);
assert!(MarketDepth::new(vec![level(99.0, 0.0)], vec![]).is_err());
assert!(MarketDepth::new(vec![level(99.0, 1.0)], vec![]).is_ok());
assert!(MarketDepth::new(vec![], vec![]).is_ok());
assert_eq!(book().mid(), Some(100.0));
}
#[test]
fn vwap_walks_the_ladder_and_refuses_to_extrapolate() {
let depth = book();
assert_eq!(depth.vwap_for_size(Transection::Buy, 150.0), Some(101.0));
let vwap = depth.vwap_for_size(Transection::Buy, 300.0).unwrap();
assert!((vwap - (150.0 * 101.0 + 150.0 * 101.5) / 300.0).abs() < 1e-12);
let sell = depth.vwap_for_size(Transection::Sell, 250.0).unwrap();
assert!((sell - (100.0 * 99.0 + 150.0 * 98.5) / 250.0).abs() < 1e-12);
assert_eq!(depth.vwap_for_size(Transection::Buy, 1_000.0), None);
assert_eq!(depth.vwap_for_size(Transection::Buy, 0.0), None);
assert!(depth.slippage_for_size(Transection::Buy, 300.0).unwrap() > 0.0);
assert!(depth.slippage_for_size(Transection::Sell, 250.0).unwrap() > 0.0);
}
#[test]
fn to_quote_collapses_the_top_of_book_for_pricing() {
let quote = book().to_quote().unwrap();
assert_eq!(quote.bid(), Some(99.0));
assert_eq!(quote.ask(), Some(101.0));
assert_eq!(quote.mid(), 100.0);
match quote {
Quote::Sized { bid_size, ask_size, .. } => {
assert_eq!((bid_size, ask_size), (100.0, 150.0));
}
other => panic!("expected a sized quote, got {other:?}"),
}
let one_sided = MarketDepth::new(vec![level(99.0, 1.0)], vec![]).unwrap();
assert_eq!(one_sided.to_quote(), None);
}
}