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