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    #[inline]
54    fn update(&mut self, book: OrderBook) -> Option<f64> {
55        self.has_emitted = true;
56        let (Some(bid), Some(ask)) = (book.best_bid(), book.best_ask()) else {
57            return Some(0.0);
58        };
59        let total = bid.size + ask.size;
60        if total <= 0.0 {
61            return Some(f64::midpoint(bid.price, ask.price));
62        }
63        Some((bid.price * ask.size + ask.price * bid.size) / total)
64    }
65
66    fn reset(&mut self) {
67        self.has_emitted = false;
68    }
69
70    #[inline]
71    fn warmup_period(&self) -> usize {
72        1
73    }
74
75    #[inline]
76    fn is_ready(&self) -> bool {
77        self.has_emitted
78    }
79
80    #[inline]
81    fn name(&self) -> &'static str {
82        "Microprice"
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89    use crate::microstructure::Level;
90    use crate::traits::BatchExt;
91
92    fn book(bids: &[(f64, f64)], asks: &[(f64, f64)]) -> OrderBook {
93        let to_levels = |xs: &[(f64, f64)]| {
94            xs.iter()
95                .map(|&(p, s)| Level::new(p, s).unwrap())
96                .collect::<Vec<_>>()
97        };
98        OrderBook::new(to_levels(bids), to_levels(asks)).unwrap()
99    }
100
101    #[test]
102    fn accessors_and_metadata() {
103        let mp = Microprice::new();
104        assert_eq!(mp.name(), "Microprice");
105        assert_eq!(mp.warmup_period(), 1);
106        assert!(!mp.is_ready());
107    }
108
109    #[test]
110    fn weights_toward_thin_side() {
111        let mut mp = Microprice::new();
112        // Heavy ask -> microprice pulled toward bid.
113        assert_eq!(
114            mp.update(book(&[(100.0, 1.0)], &[(101.0, 3.0)])),
115            Some(100.25)
116        );
117        assert!(mp.is_ready());
118    }
119
120    #[test]
121    fn balanced_top_equals_mid() {
122        let mut mp = Microprice::new();
123        assert_eq!(
124            mp.update(book(&[(100.0, 2.0)], &[(101.0, 2.0)])),
125            Some(100.5)
126        );
127    }
128
129    #[test]
130    fn zero_size_falls_back_to_mid() {
131        let mut mp = Microprice::new();
132        assert_eq!(
133            mp.update(book(&[(100.0, 0.0)], &[(102.0, 0.0)])),
134            Some(101.0)
135        );
136    }
137
138    #[test]
139    fn empty_book_is_zero() {
140        let mut mp = Microprice::new();
141        assert_eq!(
142            mp.update(OrderBook::new_unchecked(vec![], vec![])),
143            Some(0.0)
144        );
145    }
146
147    #[test]
148    fn batch_equals_streaming() {
149        let books: Vec<OrderBook> = (0..20)
150            .map(|i| {
151                let ask = 1.0 + f64::from(i % 4);
152                book(&[(100.0, 2.0)], &[(101.0, ask)])
153            })
154            .collect();
155        let mut a = Microprice::new();
156        let mut b = Microprice::new();
157        assert_eq!(
158            a.batch(&books),
159            books
160                .iter()
161                .map(|x| b.update(x.clone()))
162                .collect::<Vec<_>>()
163        );
164    }
165
166    #[test]
167    fn reset_clears_state() {
168        let mut mp = Microprice::new();
169        mp.update(book(&[(100.0, 1.0)], &[(101.0, 1.0)]));
170        assert!(mp.is_ready());
171        mp.reset();
172        assert!(!mp.is_ready());
173    }
174}