1use crate::core::errors::{Result, RustyQLibError};
15use crate::core::quotes::Quote;
16use crate::core::trade::Transection;
17
18#[derive(Debug, Clone, Copy, PartialEq)]
20pub struct DepthLevel {
21 pub price: f64,
22 pub size: f64,
23}
24
25#[derive(Debug, Clone, PartialEq)]
30pub struct MarketDepth {
31 bids: Vec<DepthLevel>,
32 asks: Vec<DepthLevel>,
33}
34
35fn validate_side(levels: &[DepthLevel], side: &str, descending: bool) -> Result<()> {
36 for level in levels {
37 if !level.price.is_finite() || !level.size.is_finite() || level.size <= 0.0 {
38 return Err(RustyQLibError::invalid_input(
39 "market depth",
40 format!("{side} level must have finite price and positive size, got {level:?}"),
41 ));
42 }
43 }
44 let ordered = levels.windows(2).all(|w| {
45 if descending { w[1].price < w[0].price } else { w[1].price > w[0].price }
46 });
47 if !ordered {
48 return Err(RustyQLibError::invalid_input(
49 "market depth",
50 format!("{side} levels must be strictly best-first, got {levels:?}"),
51 ));
52 }
53 Ok(())
54}
55
56impl MarketDepth {
57 pub fn new(bids: Vec<DepthLevel>, asks: Vec<DepthLevel>) -> Result<Self> {
60 validate_side(&bids, "bid", true)?;
61 validate_side(&asks, "ask", false)?;
62 if let (Some(bid), Some(ask)) = (bids.first(), asks.first()) {
63 if bid.price > ask.price {
64 return Err(RustyQLibError::invalid_input(
65 "market depth",
66 format!("crossed book: best bid {} > best ask {}", bid.price, ask.price),
67 ));
68 }
69 }
70 Ok(MarketDepth { bids, asks })
71 }
72
73 pub fn bids(&self) -> &[DepthLevel] {
74 &self.bids
75 }
76
77 pub fn asks(&self) -> &[DepthLevel] {
78 &self.asks
79 }
80
81 pub fn best_bid(&self) -> Option<DepthLevel> {
82 self.bids.first().copied()
83 }
84
85 pub fn best_ask(&self) -> Option<DepthLevel> {
86 self.asks.first().copied()
87 }
88
89 pub fn mid(&self) -> Option<f64> {
91 Some(0.5 * (self.best_bid()?.price + self.best_ask()?.price))
92 }
93
94 pub fn vwap_for_size(&self, side: Transection, quantity: f64) -> Option<f64> {
99 if !(quantity > 0.0) || !quantity.is_finite() {
100 return None;
101 }
102 let levels = match side {
103 Transection::Buy => &self.asks,
104 Transection::Sell => &self.bids,
105 };
106 let mut remaining = quantity;
107 let mut cost = 0.0;
108 for level in levels {
109 let fill = remaining.min(level.size);
110 cost += fill * level.price;
111 remaining -= fill;
112 if remaining <= 0.0 {
113 return Some(cost / quantity);
114 }
115 }
116 None
117 }
118
119 pub fn slippage_for_size(&self, side: Transection, quantity: f64) -> Option<f64> {
122 let mid = self.mid()?;
123 let vwap = self.vwap_for_size(side.clone(), quantity)?;
124 Some(match side {
125 Transection::Buy => vwap - mid,
126 Transection::Sell => mid - vwap,
127 })
128 }
129
130 pub fn to_quote(&self) -> Option<Quote> {
134 let bid = self.best_bid()?;
135 let ask = self.best_ask()?;
136 Quote::from_bid_ask_sized(bid.price, bid.size, ask.price, ask.size).ok()
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 fn level(price: f64, size: f64) -> DepthLevel {
145 DepthLevel { price, size }
146 }
147
148 fn book() -> MarketDepth {
149 MarketDepth::new(
150 vec![level(99.0, 100.0), level(98.5, 200.0), level(98.0, 500.0)],
151 vec![level(101.0, 150.0), level(101.5, 300.0), level(102.0, 400.0)],
152 )
153 .unwrap()
154 }
155
156 #[test]
157 fn construction_validates_ordering_sizes_and_crossing() {
158 assert!(MarketDepth::new(vec![level(98.0, 1.0), level(99.0, 1.0)], vec![]).is_err());
160 assert!(MarketDepth::new(vec![], vec![level(102.0, 1.0), level(101.0, 1.0)]).is_err());
162 assert!(
164 MarketDepth::new(vec![level(101.5, 1.0)], vec![level(101.0, 1.0)]).is_err()
165 );
166 assert!(MarketDepth::new(vec![level(99.0, 0.0)], vec![]).is_err());
168 assert!(MarketDepth::new(vec![level(99.0, 1.0)], vec![]).is_ok());
170 assert!(MarketDepth::new(vec![], vec![]).is_ok());
171 assert_eq!(book().mid(), Some(100.0));
172 }
173
174 #[test]
175 fn vwap_walks_the_ladder_and_refuses_to_extrapolate() {
176 let depth = book();
177 assert_eq!(depth.vwap_for_size(Transection::Buy, 150.0), Some(101.0));
179 let vwap = depth.vwap_for_size(Transection::Buy, 300.0).unwrap();
181 assert!((vwap - (150.0 * 101.0 + 150.0 * 101.5) / 300.0).abs() < 1e-12);
182 let sell = depth.vwap_for_size(Transection::Sell, 250.0).unwrap();
184 assert!((sell - (100.0 * 99.0 + 150.0 * 98.5) / 250.0).abs() < 1e-12);
185 assert_eq!(depth.vwap_for_size(Transection::Buy, 1_000.0), None);
187 assert_eq!(depth.vwap_for_size(Transection::Buy, 0.0), None);
188 assert!(depth.slippage_for_size(Transection::Buy, 300.0).unwrap() > 0.0);
190 assert!(depth.slippage_for_size(Transection::Sell, 250.0).unwrap() > 0.0);
191 }
192
193 #[test]
194 fn to_quote_collapses_the_top_of_book_for_pricing() {
195 let quote = book().to_quote().unwrap();
196 assert_eq!(quote.bid(), Some(99.0));
197 assert_eq!(quote.ask(), Some(101.0));
198 assert_eq!(quote.mid(), 100.0);
199 match quote {
200 Quote::Sized { bid_size, ask_size, .. } => {
201 assert_eq!((bid_size, ask_size), (100.0, 150.0));
202 }
203 other => panic!("expected a sized quote, got {other:?}"),
204 }
205 let one_sided = MarketDepth::new(vec![level(99.0, 1.0)], vec![]).unwrap();
207 assert_eq!(one_sided.to_quote(), None);
208 }
209}