Skip to main content

wickra_core/indicators/
quoted_spread.rs

1//! Quoted Spread — top-of-book spread in basis points.
2
3use crate::microstructure::OrderBook;
4use crate::traits::Indicator;
5
6/// Quoted Spread — the top-of-book bid-ask spread expressed in basis points of
7/// the mid price.
8///
9/// ```text
10/// mid           = (bidPrice₁ + askPrice₁) / 2
11/// quotedSpread  = (askPrice₁ − bidPrice₁) / mid · 10_000   (bps)
12/// ```
13///
14/// This is the round-trip cost of crossing the spread at the touch, normalised
15/// by price so it is comparable across instruments. For a valid (uncrossed)
16/// book the result is non-negative. An empty book yields `0`.
17///
18/// `Input = OrderBook`, `Output = f64`. Stateless; ready after the first
19/// snapshot.
20///
21/// # Example
22///
23/// ```
24/// use wickra_core::{Indicator, Level, OrderBook, QuotedSpread};
25///
26/// let book = OrderBook::new(
27///     vec![Level::new(100.0, 1.0).unwrap()],
28///     vec![Level::new(100.5, 1.0).unwrap()],
29/// )
30/// .unwrap();
31/// let mut qs = QuotedSpread::new();
32/// // spread 0.5, mid 100.25 -> 0.5 / 100.25 * 10_000 ≈ 49.875 bps.
33/// let bps = qs.update(book).unwrap();
34/// assert!((bps - 49.875_311_72).abs() < 1e-6);
35/// ```
36#[derive(Debug, Clone, Default)]
37pub struct QuotedSpread {
38    has_emitted: bool,
39}
40
41impl QuotedSpread {
42    /// Construct a new quoted-spread indicator.
43    pub const fn new() -> Self {
44        Self { has_emitted: false }
45    }
46}
47
48impl Indicator for QuotedSpread {
49    type Input = OrderBook;
50    type Output = f64;
51
52    #[inline]
53    fn update(&mut self, book: OrderBook) -> Option<f64> {
54        self.has_emitted = true;
55        let (Some(bid), Some(ask)) = (book.best_bid(), book.best_ask()) else {
56            return Some(0.0);
57        };
58        let mid = f64::midpoint(bid.price, ask.price);
59        Some((ask.price - bid.price) / mid * 10_000.0)
60    }
61
62    fn reset(&mut self) {
63        self.has_emitted = false;
64    }
65
66    #[inline]
67    fn warmup_period(&self) -> usize {
68        1
69    }
70
71    #[inline]
72    fn is_ready(&self) -> bool {
73        self.has_emitted
74    }
75
76    #[inline]
77    fn name(&self) -> &'static str {
78        "QuotedSpread"
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85    use crate::microstructure::Level;
86    use crate::traits::BatchExt;
87
88    fn book(bids: &[(f64, f64)], asks: &[(f64, f64)]) -> OrderBook {
89        let to_levels = |xs: &[(f64, f64)]| {
90            xs.iter()
91                .map(|&(p, s)| Level::new(p, s).unwrap())
92                .collect::<Vec<_>>()
93        };
94        OrderBook::new(to_levels(bids), to_levels(asks)).unwrap()
95    }
96
97    #[test]
98    fn accessors_and_metadata() {
99        let qs = QuotedSpread::new();
100        assert_eq!(qs.name(), "QuotedSpread");
101        assert_eq!(qs.warmup_period(), 1);
102        assert!(!qs.is_ready());
103    }
104
105    #[test]
106    fn known_value_in_bps() {
107        let mut qs = QuotedSpread::new();
108        // spread 1.0, mid 100.5 -> 1 / 100.5 * 10_000 ≈ 99.5025 bps.
109        let bps = qs.update(book(&[(100.0, 1.0)], &[(101.0, 1.0)])).unwrap();
110        assert!((bps - 99.502_487_56).abs() < 1e-6);
111        assert!(qs.is_ready());
112    }
113
114    #[test]
115    fn tight_book_is_small() {
116        let mut qs = QuotedSpread::new();
117        let bps = qs.update(book(&[(100.0, 1.0)], &[(100.01, 1.0)])).unwrap();
118        assert!(bps > 0.0 && bps < 2.0);
119    }
120
121    #[test]
122    fn empty_book_is_zero() {
123        let mut qs = QuotedSpread::new();
124        assert_eq!(
125            qs.update(OrderBook::new_unchecked(vec![], vec![])),
126            Some(0.0)
127        );
128    }
129
130    #[test]
131    fn batch_equals_streaming() {
132        let books: Vec<OrderBook> = (0..20)
133            .map(|i| {
134                let ask = 100.5 + f64::from(i % 4) * 0.1;
135                book(&[(100.0, 1.0)], &[(ask, 1.0)])
136            })
137            .collect();
138        let mut a = QuotedSpread::new();
139        let mut b = QuotedSpread::new();
140        assert_eq!(
141            a.batch(&books),
142            books
143                .iter()
144                .map(|x| b.update(x.clone()))
145                .collect::<Vec<_>>()
146        );
147    }
148
149    #[test]
150    fn reset_clears_state() {
151        let mut qs = QuotedSpread::new();
152        qs.update(book(&[(100.0, 1.0)], &[(101.0, 1.0)]));
153        assert!(qs.is_ready());
154        qs.reset();
155        assert!(!qs.is_ready());
156    }
157}