wickra_core/indicators/
roll_measure.rs1use std::collections::VecDeque;
4
5use crate::microstructure::Trade;
6use crate::traits::Indicator;
7use crate::{Error, Result};
8
9#[derive(Debug, Clone)]
44pub struct RollMeasure {
45 period: usize,
46 prev_price: Option<f64>,
47 window: VecDeque<f64>,
48 scratch: Vec<f64>,
50}
51
52impl RollMeasure {
53 pub fn new(period: usize) -> Result<Self> {
59 if period < 3 {
60 return Err(Error::InvalidPeriod {
61 message: "Roll measure needs period >= 3",
62 });
63 }
64 if period > crate::error::MAX_PERIOD {
65 return Err(Error::InvalidPeriod {
66 message: crate::error::PERIOD_ABOVE_MAX,
67 });
68 }
69 Ok(Self {
70 period,
71 prev_price: None,
72 window: VecDeque::with_capacity(period),
73 scratch: Vec::with_capacity(period),
74 })
75 }
76
77 pub const fn period(&self) -> usize {
79 self.period
80 }
81}
82
83impl Indicator for RollMeasure {
84 type Input = Trade;
85 type Output = f64;
86
87 #[inline]
88 fn update(&mut self, trade: Trade) -> Option<f64> {
89 let Some(prev) = self.prev_price else {
90 self.prev_price = Some(trade.price);
91 return None;
92 };
93 let change = trade.price - prev;
94 self.prev_price = Some(trade.price);
95 if self.window.len() == self.period {
96 self.window.pop_front();
97 }
98 self.window.push_back(change);
99 if self.window.len() < self.period {
100 return None;
101 }
102 self.scratch.clear();
104 self.scratch.extend(self.window.iter().copied());
105 let changes = &self.scratch;
106 let count = changes.len() as f64;
107 let mean = changes.iter().sum::<f64>() / count;
108 let pairs = (changes.len() - 1) as f64;
109 let mut cov = 0.0;
110 for pair in changes.windows(2) {
111 cov += (pair[0] - mean) * (pair[1] - mean);
112 }
113 cov /= pairs;
114 let spread = if cov < 0.0 { 2.0 * (-cov).sqrt() } else { 0.0 };
115 Some(spread)
116 }
117
118 fn reset(&mut self) {
119 self.prev_price = None;
120 self.window.clear();
121 self.scratch.clear();
122 }
123
124 #[inline]
125 fn warmup_period(&self) -> usize {
126 self.period + 1
127 }
128
129 #[inline]
130 fn is_ready(&self) -> bool {
131 self.window.len() == self.period
132 }
133
134 #[inline]
135 fn name(&self) -> &'static str {
136 "RollMeasure"
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143 use crate::microstructure::Side;
144 use crate::traits::BatchExt;
145 use approx::assert_relative_eq;
146
147 fn trade(price: f64) -> Trade {
148 Trade::new(price, 1.0, Side::Buy, 0).unwrap()
149 }
150
151 #[test]
152 fn rejects_period_below_three() {
153 assert!(matches!(
154 RollMeasure::new(2),
155 Err(Error::InvalidPeriod { .. })
156 ));
157 assert!(RollMeasure::new(3).is_ok());
158 }
159
160 #[test]
161 fn accessors_and_metadata() {
162 let roll = RollMeasure::new(20).unwrap();
163 assert_eq!(roll.period(), 20);
164 assert_eq!(roll.warmup_period(), 21);
165 assert_eq!(roll.name(), "RollMeasure");
166 assert!(!roll.is_ready());
167 }
168
169 #[test]
170 fn bid_ask_bounce_implies_spread() {
171 let mut roll = RollMeasure::new(6).unwrap();
174 let prices: Vec<Trade> = (0..20)
175 .map(|i| trade(if i % 2 == 0 { 100.0 } else { 101.0 }))
176 .collect();
177 let last = roll.batch(&prices).into_iter().flatten().last().unwrap();
178 assert_relative_eq!(last, 2.0, epsilon = 1e-12);
179 }
180
181 #[test]
182 fn trending_prices_imply_no_spread() {
183 let mut roll = RollMeasure::new(6).unwrap();
186 let prices: Vec<Trade> = (0..20).map(|i| trade(100.0 + f64::from(i))).collect();
187 for v in roll.batch(&prices).into_iter().flatten() {
188 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
189 }
190 }
191
192 #[test]
193 fn output_is_non_negative() {
194 let mut roll = RollMeasure::new(20).unwrap();
195 let prices: Vec<Trade> = (0..200)
196 .map(|i| trade(100.0 + (f64::from(i) * 0.7).sin() * 2.0))
197 .collect();
198 for v in roll.batch(&prices).into_iter().flatten() {
199 assert!(v >= 0.0, "spread must be non-negative, got {v}");
200 }
201 }
202
203 #[test]
204 fn reset_clears_state() {
205 let mut roll = RollMeasure::new(5).unwrap();
206 for i in 0..20 {
207 roll.update(trade(100.0 + f64::from(i % 2)));
208 }
209 assert!(roll.is_ready());
210 roll.reset();
211 assert!(!roll.is_ready());
212 assert_eq!(roll.update(trade(100.0)), None);
213 }
214
215 #[test]
216 fn batch_equals_streaming() {
217 let prices: Vec<Trade> = (0..80)
218 .map(|i| trade(100.0 + (f64::from(i) * 0.6).sin() * 3.0))
219 .collect();
220 let batch = RollMeasure::new(14).unwrap().batch(&prices);
221 let mut b = RollMeasure::new(14).unwrap();
222 let streamed: Vec<_> = prices.iter().map(|t| b.update(*t)).collect();
223 assert_eq!(batch, streamed);
224 }
225}