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    fn update(&mut self, book: OrderBook) -> Option<f64> {
53        self.has_emitted = true;
54        let (Some(bid), Some(ask)) = (book.best_bid(), book.best_ask()) else {
55            return Some(0.0);
56        };
57        let mid = f64::midpoint(bid.price, ask.price);
58        Some((ask.price - bid.price) / mid * 10_000.0)
59    }
60
61    fn reset(&mut self) {
62        self.has_emitted = false;
63    }
64
65    fn warmup_period(&self) -> usize {
66        1
67    }
68
69    fn is_ready(&self) -> bool {
70        self.has_emitted
71    }
72
73    fn name(&self) -> &'static str {
74        "QuotedSpread"
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81    use crate::microstructure::Level;
82    use crate::traits::BatchExt;
83
84    fn book(bids: &[(f64, f64)], asks: &[(f64, f64)]) -> OrderBook {
85        let to_levels = |xs: &[(f64, f64)]| {
86            xs.iter()
87                .map(|&(p, s)| Level::new(p, s).unwrap())
88                .collect::<Vec<_>>()
89        };
90        OrderBook::new(to_levels(bids), to_levels(asks)).unwrap()
91    }
92
93    #[test]
94    fn accessors_and_metadata() {
95        let qs = QuotedSpread::new();
96        assert_eq!(qs.name(), "QuotedSpread");
97        assert_eq!(qs.warmup_period(), 1);
98        assert!(!qs.is_ready());
99    }
100
101    #[test]
102    fn known_value_in_bps() {
103        let mut qs = QuotedSpread::new();
104        // spread 1.0, mid 100.5 -> 1 / 100.5 * 10_000 ≈ 99.5025 bps.
105        let bps = qs.update(book(&[(100.0, 1.0)], &[(101.0, 1.0)])).unwrap();
106        assert!((bps - 99.502_487_56).abs() < 1e-6);
107        assert!(qs.is_ready());
108    }
109
110    #[test]
111    fn tight_book_is_small() {
112        let mut qs = QuotedSpread::new();
113        let bps = qs.update(book(&[(100.0, 1.0)], &[(100.01, 1.0)])).unwrap();
114        assert!(bps > 0.0 && bps < 2.0);
115    }
116
117    #[test]
118    fn empty_book_is_zero() {
119        let mut qs = QuotedSpread::new();
120        assert_eq!(
121            qs.update(OrderBook::new_unchecked(vec![], vec![])),
122            Some(0.0)
123        );
124    }
125
126    #[test]
127    fn batch_equals_streaming() {
128        let books: Vec<OrderBook> = (0..20)
129            .map(|i| {
130                let ask = 100.5 + f64::from(i % 4) * 0.1;
131                book(&[(100.0, 1.0)], &[(ask, 1.0)])
132            })
133            .collect();
134        let mut a = QuotedSpread::new();
135        let mut b = QuotedSpread::new();
136        assert_eq!(
137            a.batch(&books),
138            books
139                .iter()
140                .map(|x| b.update(x.clone()))
141                .collect::<Vec<_>>()
142        );
143    }
144
145    #[test]
146    fn reset_clears_state() {
147        let mut qs = QuotedSpread::new();
148        qs.update(book(&[(100.0, 1.0)], &[(101.0, 1.0)]));
149        assert!(qs.is_ready());
150        qs.reset();
151        assert!(!qs.is_ready());
152    }
153}