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