Skip to main content

wickra_core/indicators/
microprice.rs

1//! Microprice — size-weighted fair value of the top of book.
2
3use crate::microstructure::OrderBook;
4use crate::traits::Indicator;
5
6/// Microprice — the size-weighted mid of the top of book.
7///
8/// The microprice tilts the mid toward the side that is *more likely to be
9/// hit*: it weights each touch price by the size resting on the **opposite**
10/// side, so a heavy ask (sell pressure) pulls the fair value down toward the
11/// bid, and vice versa:
12///
13/// ```text
14/// microprice = (bidPrice₁·askSize₁ + askPrice₁·bidSize₁) / (bidSize₁ + askSize₁)
15/// ```
16///
17/// When both top sizes are zero the weighting is undefined and the plain mid
18/// `(bidPrice₁ + askPrice₁) / 2` is returned. An empty book yields `0`.
19///
20/// `Input = OrderBook`, `Output = f64`. Stateless; ready after the first
21/// snapshot.
22///
23/// # Example
24///
25/// ```
26/// use wickra_core::{Indicator, Level, Microprice, OrderBook};
27///
28/// let book = OrderBook::new(
29///     vec![Level::new(100.0, 1.0).unwrap()],
30///     vec![Level::new(101.0, 3.0).unwrap()],
31/// )
32/// .unwrap();
33/// let mut mp = Microprice::new();
34/// // (100·3 + 101·1) / (1 + 3) = 401 / 4 = 100.25 — pulled toward the bid.
35/// assert_eq!(mp.update(book), Some(100.25));
36/// ```
37#[derive(Debug, Clone, Default)]
38pub struct Microprice {
39    has_emitted: bool,
40}
41
42impl Microprice {
43    /// Construct a new microprice indicator.
44    pub const fn new() -> Self {
45        Self { has_emitted: false }
46    }
47}
48
49impl Indicator for Microprice {
50    type Input = OrderBook;
51    type Output = f64;
52
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 total = bid.size + ask.size;
59        if total <= 0.0 {
60            return Some(f64::midpoint(bid.price, ask.price));
61        }
62        Some((bid.price * ask.size + ask.price * bid.size) / total)
63    }
64
65    fn reset(&mut self) {
66        self.has_emitted = false;
67    }
68
69    fn warmup_period(&self) -> usize {
70        1
71    }
72
73    fn is_ready(&self) -> bool {
74        self.has_emitted
75    }
76
77    fn name(&self) -> &'static str {
78        "Microprice"
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 mp = Microprice::new();
100        assert_eq!(mp.name(), "Microprice");
101        assert_eq!(mp.warmup_period(), 1);
102        assert!(!mp.is_ready());
103    }
104
105    #[test]
106    fn weights_toward_thin_side() {
107        let mut mp = Microprice::new();
108        // Heavy ask -> microprice pulled toward bid.
109        assert_eq!(
110            mp.update(book(&[(100.0, 1.0)], &[(101.0, 3.0)])),
111            Some(100.25)
112        );
113        assert!(mp.is_ready());
114    }
115
116    #[test]
117    fn balanced_top_equals_mid() {
118        let mut mp = Microprice::new();
119        assert_eq!(
120            mp.update(book(&[(100.0, 2.0)], &[(101.0, 2.0)])),
121            Some(100.5)
122        );
123    }
124
125    #[test]
126    fn zero_size_falls_back_to_mid() {
127        let mut mp = Microprice::new();
128        assert_eq!(
129            mp.update(book(&[(100.0, 0.0)], &[(102.0, 0.0)])),
130            Some(101.0)
131        );
132    }
133
134    #[test]
135    fn empty_book_is_zero() {
136        let mut mp = Microprice::new();
137        assert_eq!(
138            mp.update(OrderBook::new_unchecked(vec![], vec![])),
139            Some(0.0)
140        );
141    }
142
143    #[test]
144    fn batch_equals_streaming() {
145        let books: Vec<OrderBook> = (0..20)
146            .map(|i| {
147                let ask = 1.0 + f64::from(i % 4);
148                book(&[(100.0, 2.0)], &[(101.0, ask)])
149            })
150            .collect();
151        let mut a = Microprice::new();
152        let mut b = Microprice::new();
153        assert_eq!(
154            a.batch(&books),
155            books
156                .iter()
157                .map(|x| b.update(x.clone()))
158                .collect::<Vec<_>>()
159        );
160    }
161
162    #[test]
163    fn reset_clears_state() {
164        let mut mp = Microprice::new();
165        mp.update(book(&[(100.0, 1.0)], &[(101.0, 1.0)]));
166        assert!(mp.is_ready());
167        mp.reset();
168        assert!(!mp.is_ready());
169    }
170}