wickra_core/indicators/
calmar_ratio.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
41pub struct CalmarRatio {
42 period: usize,
43 window: VecDeque<f64>,
44 sum: f64,
45}
46
47impl CalmarRatio {
48 pub fn new(period: usize) -> Result<Self> {
53 if period < 2 {
54 return Err(Error::InvalidPeriod {
55 message: "calmar ratio needs period >= 2",
56 });
57 }
58 Ok(Self {
59 period,
60 window: VecDeque::with_capacity(period),
61 sum: 0.0,
62 })
63 }
64
65 pub const fn period(&self) -> usize {
67 self.period
68 }
69}
70
71impl Indicator for CalmarRatio {
72 type Input = f64;
73 type Output = f64;
74
75 fn update(&mut self, input: f64) -> Option<f64> {
76 if !input.is_finite() {
77 return None;
78 }
79 if self.window.len() == self.period {
80 let old = self.window.pop_front().expect("non-empty");
81 self.sum -= old;
82 }
83 self.window.push_back(input);
84 self.sum += input;
85 if self.window.len() < self.period {
86 return None;
87 }
88 let n = self.period as f64;
89 let mean = self.sum / n;
90 let mut equity = 1.0_f64;
92 let mut peak = 1.0_f64;
93 let mut mdd = 0.0_f64;
94 for &r in &self.window {
95 equity *= 1.0 + r;
96 if equity > peak {
97 peak = equity;
98 }
99 let dd = (peak - equity) / peak;
101 if dd > mdd {
102 mdd = dd;
103 }
104 }
105 if mdd == 0.0 {
106 return Some(0.0);
107 }
108 Some(mean / mdd)
109 }
110
111 fn reset(&mut self) {
112 self.window.clear();
113 self.sum = 0.0;
114 }
115
116 fn warmup_period(&self) -> usize {
117 self.period
118 }
119
120 fn is_ready(&self) -> bool {
121 self.window.len() == self.period
122 }
123
124 fn name(&self) -> &'static str {
125 "CalmarRatio"
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use crate::traits::BatchExt;
133 use approx::assert_relative_eq;
134
135 #[test]
136 fn rejects_period_less_than_two() {
137 assert!(matches!(
138 CalmarRatio::new(1),
139 Err(Error::InvalidPeriod { .. })
140 ));
141 }
142
143 #[test]
144 fn accessors_and_metadata() {
145 let c = CalmarRatio::new(10).unwrap();
146 assert_eq!(c.period(), 10);
147 assert_eq!(c.name(), "CalmarRatio");
148 assert_eq!(c.warmup_period(), 10);
149 }
150
151 #[test]
152 fn pure_uptrend_yields_zero() {
153 let mut c = CalmarRatio::new(5).unwrap();
155 let out = c.batch(&[0.01; 10]);
156 for v in out.into_iter().flatten() {
157 assert_eq!(v, 0.0);
158 }
159 }
160
161 #[test]
162 fn reference_value() {
163 let mut c = CalmarRatio::new(3).unwrap();
169 let out = c.batch(&[0.10, -0.20, 0.05]);
170 let mean = (0.10 - 0.20 + 0.05) / 3.0;
171 let expected = mean / 0.20;
172 assert_relative_eq!(out[2].unwrap(), expected, epsilon = 1e-9);
173 }
174
175 #[test]
176 fn ignores_non_finite_input() {
177 let mut c = CalmarRatio::new(3).unwrap();
178 assert_eq!(c.update(f64::NAN), None);
179 assert_eq!(c.update(f64::INFINITY), None);
180 }
181
182 #[test]
183 fn reset_clears_state() {
184 let mut c = CalmarRatio::new(3).unwrap();
185 c.batch(&[0.10, -0.20, 0.05]);
186 assert!(c.is_ready());
187 c.reset();
188 assert!(!c.is_ready());
189 assert_eq!(c.update(0.01), None);
190 }
191
192 #[test]
193 fn batch_equals_streaming() {
194 let returns: Vec<f64> = (0..50)
195 .map(|i| 0.001 + (f64::from(i) * 0.25).sin() * 0.02)
196 .collect();
197 let batch = CalmarRatio::new(10).unwrap().batch(&returns);
198 let mut s = CalmarRatio::new(10).unwrap();
199 let streamed: Vec<_> = returns.iter().map(|r| s.update(*r)).collect();
200 assert_eq!(batch, streamed);
201 }
202}