wickra_core/indicators/
omega_ratio.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
41pub struct OmegaRatio {
42 period: usize,
43 threshold: f64,
44 window: VecDeque<f64>,
45}
46
47impl OmegaRatio {
48 pub fn new(period: usize, threshold: f64) -> Result<Self> {
53 if period == 0 {
54 return Err(Error::PeriodZero);
55 }
56 Ok(Self {
57 period,
58 threshold,
59 window: VecDeque::with_capacity(period),
60 })
61 }
62
63 pub const fn period(&self) -> usize {
65 self.period
66 }
67
68 pub const fn threshold(&self) -> f64 {
70 self.threshold
71 }
72}
73
74impl Indicator for OmegaRatio {
75 type Input = f64;
76 type Output = f64;
77
78 fn update(&mut self, input: f64) -> Option<f64> {
79 if !input.is_finite() {
80 return None;
81 }
82 if self.window.len() == self.period {
83 self.window.pop_front();
84 }
85 self.window.push_back(input);
86 if self.window.len() < self.period {
87 return None;
88 }
89 let mut gains = 0.0_f64;
90 let mut losses = 0.0_f64;
91 for &r in &self.window {
92 let d = r - self.threshold;
93 if d >= 0.0 {
94 gains += d;
95 } else {
96 losses += -d;
97 }
98 }
99 if losses == 0.0 {
100 return Some(if gains == 0.0 { 0.0 } else { f64::INFINITY });
101 }
102 Some(gains / losses)
103 }
104
105 fn reset(&mut self) {
106 self.window.clear();
107 }
108
109 fn warmup_period(&self) -> usize {
110 self.period
111 }
112
113 fn is_ready(&self) -> bool {
114 self.window.len() == self.period
115 }
116
117 fn name(&self) -> &'static str {
118 "OmegaRatio"
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125 use crate::traits::BatchExt;
126 use approx::assert_relative_eq;
127
128 #[test]
129 fn rejects_zero_period() {
130 assert!(matches!(OmegaRatio::new(0, 0.0), Err(Error::PeriodZero)));
131 }
132
133 #[test]
134 fn accessors_and_metadata() {
135 let o = OmegaRatio::new(10, 0.001).unwrap();
136 assert_eq!(o.period(), 10);
137 assert_relative_eq!(o.threshold(), 0.001, epsilon = 1e-12);
138 assert_eq!(o.name(), "OmegaRatio");
139 assert_eq!(o.warmup_period(), 10);
140 }
141
142 #[test]
143 fn all_above_threshold_yields_infinity() {
144 let mut o = OmegaRatio::new(4, 0.0).unwrap();
145 let out = o.batch(&[0.01, 0.02, 0.03, 0.04]);
146 assert!(out[3].unwrap().is_infinite());
147 }
148
149 #[test]
150 fn flat_at_threshold_yields_zero() {
151 let mut o = OmegaRatio::new(4, 0.01).unwrap();
154 let out = o.batch(&[0.01; 4]);
155 assert_eq!(out[3], Some(0.0));
156 }
157
158 #[test]
159 fn reference_value() {
160 let mut o = OmegaRatio::new(4, 0.0).unwrap();
165 let out = o.batch(&[-0.02, 0.01, -0.01, 0.03]);
166 assert_relative_eq!(out[3].unwrap(), 0.04 / 0.03, epsilon = 1e-9);
167 }
168
169 #[test]
170 fn ignores_non_finite_input() {
171 let mut o = OmegaRatio::new(3, 0.0).unwrap();
172 assert_eq!(o.update(f64::NAN), None);
173 assert_eq!(o.update(f64::INFINITY), None);
174 }
175
176 #[test]
177 fn reset_clears_state() {
178 let mut o = OmegaRatio::new(3, 0.0).unwrap();
179 o.batch(&[0.01, -0.02, 0.005]);
180 assert!(o.is_ready());
181 o.reset();
182 assert!(!o.is_ready());
183 assert_eq!(o.update(0.01), None);
184 }
185
186 #[test]
187 fn batch_equals_streaming() {
188 let returns: Vec<f64> = (0..50).map(|i| (f64::from(i) * 0.4).sin() * 0.01).collect();
189 let batch = OmegaRatio::new(10, 0.0).unwrap().batch(&returns);
190 let mut s = OmegaRatio::new(10, 0.0).unwrap();
191 let streamed: Vec<_> = returns.iter().map(|r| s.update(*r)).collect();
192 assert_eq!(batch, streamed);
193 }
194}