wickra_core/indicators/
omega_ratio.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
55pub struct OmegaRatio {
56 period: usize,
57 threshold: f64,
58 window: VecDeque<f64>,
59}
60
61impl OmegaRatio {
62 pub fn new(period: usize, threshold: f64) -> Result<Self> {
67 if period == 0 {
68 return Err(Error::PeriodZero);
69 }
70 if period > crate::error::MAX_PERIOD {
71 return Err(Error::InvalidPeriod {
72 message: crate::error::PERIOD_ABOVE_MAX,
73 });
74 }
75 Ok(Self {
76 period,
77 threshold,
78 window: VecDeque::with_capacity(period),
79 })
80 }
81
82 pub const fn period(&self) -> usize {
84 self.period
85 }
86
87 pub const fn threshold(&self) -> f64 {
89 self.threshold
90 }
91}
92
93impl Indicator for OmegaRatio {
94 type Input = f64;
95 type Output = f64;
96
97 #[inline]
98 fn update(&mut self, input: f64) -> Option<f64> {
99 if !input.is_finite() {
100 return None;
101 }
102 if self.window.len() == self.period {
103 self.window.pop_front();
104 }
105 self.window.push_back(input);
106 if self.window.len() < self.period {
107 return None;
108 }
109 let mut gains = 0.0_f64;
110 let mut losses = 0.0_f64;
111 for &r in &self.window {
112 let d = r - self.threshold;
113 if d >= 0.0 {
114 gains += d;
115 } else {
116 losses += -d;
117 }
118 }
119 if losses == 0.0 {
120 return Some(if gains == 0.0 { 1.0 } else { f64::INFINITY });
124 }
125 Some(gains / losses)
126 }
127
128 fn reset(&mut self) {
129 self.window.clear();
130 }
131
132 #[inline]
133 fn warmup_period(&self) -> usize {
134 self.period
135 }
136
137 #[inline]
138 fn is_ready(&self) -> bool {
139 self.window.len() == self.period
140 }
141
142 #[inline]
143 fn name(&self) -> &'static str {
144 "OmegaRatio"
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_zero_period() {
156 assert!(matches!(OmegaRatio::new(0, 0.0), Err(Error::PeriodZero)));
157 }
158
159 #[test]
160 fn accessors_and_metadata() {
161 let o = OmegaRatio::new(10, 0.001).unwrap();
162 assert_eq!(o.period(), 10);
163 assert_relative_eq!(o.threshold(), 0.001, epsilon = 1e-12);
164 assert_eq!(o.name(), "OmegaRatio");
165 assert_eq!(o.warmup_period(), 10);
166 }
167
168 #[test]
169 fn all_above_threshold_yields_infinity() {
170 let mut o = OmegaRatio::new(4, 0.0).unwrap();
171 let out = o.batch(&[0.01, 0.02, 0.03, 0.04]);
172 assert!(out[3].unwrap().is_infinite());
173 }
174
175 #[test]
176 fn flat_at_threshold_is_break_even() {
177 let mut o = OmegaRatio::new(4, 0.01).unwrap();
180 let out = o.batch(&[0.01; 4]);
181 assert_eq!(out[3], Some(1.0));
182 }
183
184 #[test]
185 fn reference_value() {
186 let mut o = OmegaRatio::new(4, 0.0).unwrap();
191 let out = o.batch(&[-0.02, 0.01, -0.01, 0.03]);
192 assert_relative_eq!(out[3].unwrap(), 0.04 / 0.03, epsilon = 1e-9);
193 }
194
195 #[test]
196 fn ignores_non_finite_input() {
197 let mut o = OmegaRatio::new(3, 0.0).unwrap();
198 assert_eq!(o.update(f64::NAN), None);
199 assert_eq!(o.update(f64::INFINITY), None);
200 }
201
202 #[test]
203 fn reset_clears_state() {
204 let mut o = OmegaRatio::new(3, 0.0).unwrap();
205 o.batch(&[0.01, -0.02, 0.005]);
206 assert!(o.is_ready());
207 o.reset();
208 assert!(!o.is_ready());
209 assert_eq!(o.update(0.01), None);
210 }
211
212 #[test]
213 fn batch_equals_streaming() {
214 let returns: Vec<f64> = (0..50).map(|i| (f64::from(i) * 0.4).sin() * 0.01).collect();
215 let batch = OmegaRatio::new(10, 0.0).unwrap().batch(&returns);
216 let mut s = OmegaRatio::new(10, 0.0).unwrap();
217 let streamed: Vec<_> = returns.iter().map(|r| s.update(*r)).collect();
218 assert_eq!(batch, streamed);
219 }
220 #[test]
225 fn a_negative_threshold_makes_a_flat_window_unbounded() {
226 let flat = [0.0_f64; 20];
227
228 let mut at_zero = OmegaRatio::new(14, 0.0).unwrap();
229 let mut below = OmegaRatio::new(14, -0.005).unwrap();
230 let (mut last_at_zero, mut last_below) = (None, None);
231 for &r in &flat {
232 last_at_zero = at_zero.update(r).or(last_at_zero);
233 last_below = below.update(r).or(last_below);
234 }
235
236 assert_eq!(last_at_zero, Some(1.0));
237 assert_eq!(last_below, Some(f64::INFINITY));
238 }
239 #[test]
244 fn a_flat_window_is_not_confused_with_an_all_losing_one() {
245 let flat = [0.0_f64; 20];
246 let losing = [-0.01_f64; 20];
247
248 let mut a = OmegaRatio::new(14, 0.0).unwrap();
249 let mut b = OmegaRatio::new(14, 0.0).unwrap();
250 let (mut flat_value, mut losing_value) = (None, None);
251 for i in 0..flat.len() {
252 flat_value = a.update(flat[i]).or(flat_value);
253 losing_value = b.update(losing[i]).or(losing_value);
254 }
255
256 assert_eq!(flat_value, Some(1.0), "a flat window is break-even");
257 assert_eq!(losing_value, Some(0.0), "an all-losing window has no gains");
258 assert_ne!(flat_value, losing_value);
259 }
260}