wickra_core/indicators/
m2_measure.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::indicators::rolling_moments::ShiftedMoments;
7use crate::traits::Indicator;
8
9#[derive(Debug, Clone)]
45pub struct M2Measure {
46 period: usize,
47 risk_free: f64,
48 benchmark_stddev: f64,
49 window: VecDeque<f64>,
50 moments: ShiftedMoments,
51}
52
53impl M2Measure {
54 pub fn new(period: usize, risk_free: f64, benchmark_stddev: f64) -> Result<Self> {
63 if period < 2 {
64 return Err(Error::InvalidPeriod {
65 message: "m2 measure needs period >= 2",
66 });
67 }
68 if period > crate::error::MAX_PERIOD {
69 return Err(Error::InvalidPeriod {
70 message: crate::error::PERIOD_ABOVE_MAX,
71 });
72 }
73 if !risk_free.is_finite() || !benchmark_stddev.is_finite() || benchmark_stddev < 0.0 {
74 return Err(Error::InvalidParameter {
75 message: "risk_free must be finite and benchmark_stddev finite and non-negative",
76 });
77 }
78 Ok(Self {
79 period,
80 risk_free,
81 benchmark_stddev,
82 window: VecDeque::with_capacity(period),
83 moments: ShiftedMoments::new(),
84 })
85 }
86
87 pub const fn period(&self) -> usize {
89 self.period
90 }
91
92 pub const fn risk_free(&self) -> f64 {
94 self.risk_free
95 }
96
97 pub const fn benchmark_stddev(&self) -> f64 {
99 self.benchmark_stddev
100 }
101}
102
103impl Indicator for M2Measure {
104 type Input = f64;
105 type Output = f64;
106
107 #[inline]
108 fn update(&mut self, ret: f64) -> Option<f64> {
109 if !ret.is_finite() {
110 return None;
111 }
112 if self.window.len() == self.period {
113 let old = self.window.pop_front().expect("non-empty");
114 self.moments.evict(old);
115 }
116 self.window.push_back(ret);
117 self.moments.push(ret);
118 if self.moments.needs_reseed(self.period) {
119 self.moments.reseed(self.window.iter().copied());
120 }
121 if self.window.len() < self.period {
122 return None;
123 }
124 let mean = self.moments.mean(self.period);
125 let var = self.moments.sample_variance(self.period);
126 let sd = var.sqrt();
127 if sd == 0.0 {
128 return Some(0.0);
129 }
130 let sharpe = (mean - self.risk_free) / sd;
131 Some(self.risk_free + sharpe * self.benchmark_stddev)
132 }
133
134 fn reset(&mut self) {
135 self.window.clear();
136 self.moments.reset();
137 }
138
139 #[inline]
140 fn warmup_period(&self) -> usize {
141 self.period
142 }
143
144 #[inline]
145 fn is_ready(&self) -> bool {
146 self.window.len() == self.period
147 }
148
149 #[inline]
150 fn name(&self) -> &'static str {
151 "M2Measure"
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158 use crate::traits::BatchExt;
159 use approx::assert_relative_eq;
160
161 #[test]
162 fn rejects_period_less_than_two() {
163 assert!(matches!(
164 M2Measure::new(1, 0.0, 0.02),
165 Err(Error::InvalidPeriod { .. })
166 ));
167 }
168
169 #[test]
170 fn rejects_invalid_benchmark_stddev() {
171 assert!(matches!(
172 M2Measure::new(10, 0.0, -0.01),
173 Err(Error::InvalidParameter { .. })
174 ));
175 assert!(matches!(
176 M2Measure::new(10, f64::NAN, 0.02),
177 Err(Error::InvalidParameter { .. })
178 ));
179 }
180
181 #[test]
182 fn accessors_and_metadata() {
183 let m2 = M2Measure::new(20, 0.001, 0.02).unwrap();
184 assert_eq!(m2.period(), 20);
185 assert_relative_eq!(m2.risk_free(), 0.001, epsilon = 1e-12);
186 assert_relative_eq!(m2.benchmark_stddev(), 0.02, epsilon = 1e-12);
187 assert_eq!(m2.warmup_period(), 20);
188 assert_eq!(m2.name(), "M2Measure");
189 }
190
191 #[test]
192 fn reference_value() {
193 let mut m2 = M2Measure::new(4, 0.0, 0.02).unwrap();
197 let out = m2.batch(&[0.01, 0.02, 0.03, 0.04]);
198 let sharpe = 0.025_f64 / (0.000_166_666_666_666_666_67_f64).sqrt();
199 assert_relative_eq!(out[3].unwrap(), sharpe * 0.02, epsilon = 1e-9);
200 }
201
202 #[test]
203 fn constant_returns_yield_zero() {
204 let mut m2 = M2Measure::new(5, 0.0, 0.02).unwrap();
205 for v in m2.batch(&[0.01; 10]).into_iter().flatten() {
206 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
207 }
208 }
209
210 #[test]
211 fn ignores_non_finite_input() {
212 let mut m2 = M2Measure::new(3, 0.0, 0.02).unwrap();
213 assert_eq!(m2.update(0.01), None);
214 assert_eq!(m2.update(f64::NAN), None);
215 assert_eq!(m2.update(0.02), None);
216 assert!(m2.update(0.03).is_some());
217 }
218
219 #[test]
220 fn reset_clears_state() {
221 let mut m2 = M2Measure::new(3, 0.0, 0.02).unwrap();
222 m2.batch(&[0.01, 0.02, 0.03]);
223 assert!(m2.is_ready());
224 m2.reset();
225 assert!(!m2.is_ready());
226 assert_eq!(m2.update(0.01), None);
227 }
228
229 #[test]
230 fn batch_equals_streaming() {
231 let rets: Vec<f64> = (0..50)
232 .map(|i| 0.001 + (f64::from(i) * 0.2).sin() * 0.01)
233 .collect();
234 let batch = M2Measure::new(10, 0.0, 0.02).unwrap().batch(&rets);
235 let mut streamer = M2Measure::new(10, 0.0, 0.02).unwrap();
236 let streamed: Vec<_> = rets.iter().map(|r| streamer.update(*r)).collect();
237 assert_eq!(batch, streamed);
238 }
239}