wickra_core/indicators/
realized_spread.rs1use std::collections::VecDeque;
5
6use crate::error::{Error, Result};
7use crate::microstructure::TradeQuote;
8use crate::traits::Indicator;
9
10#[derive(Debug, Clone)]
53pub struct RealizedSpread {
54 horizon: usize,
55 pending: VecDeque<(f64, f64, f64)>,
57 has_emitted: bool,
58}
59
60impl RealizedSpread {
61 pub fn new(horizon: usize) -> Result<Self> {
69 if horizon == 0 {
70 return Err(Error::PeriodZero);
71 }
72 if horizon > crate::error::MAX_PERIOD {
73 return Err(Error::InvalidPeriod {
74 message: crate::error::PERIOD_ABOVE_MAX,
75 });
76 }
77 Ok(Self {
78 horizon,
79 pending: VecDeque::with_capacity(horizon + 1),
80 has_emitted: false,
81 })
82 }
83
84 pub const fn horizon(&self) -> usize {
86 self.horizon
87 }
88}
89
90impl Indicator for RealizedSpread {
91 type Input = TradeQuote;
92 type Output = f64;
93
94 #[inline]
95 fn update(&mut self, quote: TradeQuote) -> Option<f64> {
96 let sign = quote.trade.side.sign();
97 self.pending.push_back((sign, quote.trade.price, quote.mid));
98 if self.pending.len() <= self.horizon {
99 return None;
100 }
101 let (old_sign, old_price, old_mid) = self.pending.pop_front().expect("len > horizon >= 1");
102 self.has_emitted = true;
103 Some(2.0 * old_sign * (old_price - quote.mid) / old_mid * 10_000.0)
106 }
107
108 fn reset(&mut self) {
109 self.pending.clear();
110 self.has_emitted = false;
111 }
112
113 #[inline]
114 fn warmup_period(&self) -> usize {
115 self.horizon + 1
116 }
117
118 #[inline]
119 fn is_ready(&self) -> bool {
120 self.has_emitted
121 }
122
123 #[inline]
124 fn name(&self) -> &'static str {
125 "RealizedSpread"
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use crate::microstructure::{Side, Trade};
133 use crate::traits::BatchExt;
134
135 fn tq(price: f64, side: Side, mid: f64) -> TradeQuote {
136 TradeQuote::new(Trade::new(price, 1.0, side, 0).unwrap(), mid).unwrap()
137 }
138
139 #[test]
140 fn rejects_zero_horizon() {
141 assert!(matches!(RealizedSpread::new(0), Err(Error::PeriodZero)));
142 assert!(RealizedSpread::new(1).is_ok());
143 }
144
145 #[test]
146 fn accessors_and_metadata() {
147 let rs = RealizedSpread::new(3).unwrap();
148 assert_eq!(rs.name(), "RealizedSpread");
149 assert_eq!(rs.horizon(), 3);
150 assert_eq!(rs.warmup_period(), 4);
151 assert!(!rs.is_ready());
152 }
153
154 #[test]
155 fn resolves_against_future_mid() {
156 let mut rs = RealizedSpread::new(1).unwrap();
157 assert_eq!(rs.update(tq(100.10, Side::Buy, 100.0)), None);
158 assert!(!rs.is_ready());
159 let out = rs.update(tq(99.90, Side::Sell, 100.20)).unwrap();
161 assert!((out - (-20.0)).abs() < 1e-9);
162 assert!(rs.is_ready());
163 }
164
165 #[test]
166 fn no_adverse_move_equals_effective_spread() {
167 let mut rs = RealizedSpread::new(1).unwrap();
169 rs.update(tq(100.05, Side::Buy, 100.0));
170 let out = rs.update(tq(100.0, Side::Buy, 100.0)).unwrap();
172 assert!((out - 10.0).abs() < 1e-9);
173 }
174
175 #[test]
176 fn longer_horizon_warms_up() {
177 let mut rs = RealizedSpread::new(3).unwrap();
178 for _ in 0..3 {
179 assert_eq!(rs.update(tq(100.0, Side::Buy, 100.0)), None);
180 }
181 assert!(!rs.is_ready());
182 assert!(rs.update(tq(100.0, Side::Buy, 100.0)).is_some());
183 assert!(rs.is_ready());
184 }
185
186 #[test]
187 fn batch_equals_streaming() {
188 let quotes: Vec<TradeQuote> = (0..30)
189 .map(|i| {
190 let side = if i % 2 == 0 { Side::Buy } else { Side::Sell };
191 let mid = 100.0 + f64::from(i % 5) * 0.05;
192 tq(mid + 0.02, side, mid)
193 })
194 .collect();
195 let mut a = RealizedSpread::new(4).unwrap();
196 let mut b = RealizedSpread::new(4).unwrap();
197 assert_eq!(
198 a.batch("es),
199 quotes.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
200 );
201 }
202
203 #[test]
204 fn reset_clears_state() {
205 let mut rs = RealizedSpread::new(1).unwrap();
206 rs.update(tq(100.05, Side::Buy, 100.0));
207 rs.update(tq(100.0, Side::Buy, 100.0));
208 assert!(rs.is_ready());
209 rs.reset();
210 assert!(!rs.is_ready());
211 assert_eq!(rs.update(tq(100.05, Side::Buy, 100.0)), None);
212 }
213}