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