wickra_core/indicators/
gain_loss_ratio.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
39pub struct GainLossRatio {
40 period: usize,
41 window: VecDeque<f64>,
42}
43
44impl GainLossRatio {
45 pub fn new(period: usize) -> Result<Self> {
50 if period == 0 {
51 return Err(Error::PeriodZero);
52 }
53 if period > crate::error::MAX_PERIOD {
54 return Err(Error::InvalidPeriod {
55 message: crate::error::PERIOD_ABOVE_MAX,
56 });
57 }
58 Ok(Self {
59 period,
60 window: VecDeque::with_capacity(period),
61 })
62 }
63
64 pub const fn period(&self) -> usize {
66 self.period
67 }
68}
69
70impl Indicator for GainLossRatio {
71 type Input = f64;
72 type Output = f64;
73
74 #[inline]
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 self.window.pop_front();
81 }
82 self.window.push_back(input);
83 if self.window.len() < self.period {
84 return None;
85 }
86 let mut sum_win = 0.0_f64;
87 let mut n_win = 0_u32;
88 let mut sum_loss = 0.0_f64;
89 let mut n_loss = 0_u32;
90 for &r in &self.window {
91 if r > 0.0 {
92 sum_win += r;
93 n_win += 1;
94 } else if r < 0.0 {
95 sum_loss += -r;
96 n_loss += 1;
97 }
98 }
99 if n_loss == 0 {
100 return Some(if n_win == 0 { 1.0 } else { f64::INFINITY });
104 }
105 let avg_win = if n_win == 0 {
106 0.0
107 } else {
108 sum_win / f64::from(n_win)
109 };
110 let avg_loss = sum_loss / f64::from(n_loss);
111 Some(avg_win / avg_loss)
112 }
113
114 fn reset(&mut self) {
115 self.window.clear();
116 }
117
118 #[inline]
119 fn warmup_period(&self) -> usize {
120 self.period
121 }
122
123 #[inline]
124 fn is_ready(&self) -> bool {
125 self.window.len() == self.period
126 }
127
128 #[inline]
129 fn name(&self) -> &'static str {
130 "GainLossRatio"
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use crate::traits::BatchExt;
138 use approx::assert_relative_eq;
139
140 #[test]
141 fn rejects_zero_period() {
142 assert!(matches!(GainLossRatio::new(0), Err(Error::PeriodZero)));
143 }
144
145 #[test]
146 fn accessors_and_metadata() {
147 let g = GainLossRatio::new(10).unwrap();
148 assert_eq!(g.period(), 10);
149 assert_eq!(g.name(), "GainLossRatio");
150 assert_eq!(g.warmup_period(), 10);
151 }
152
153 #[test]
154 fn reference_value() {
155 let mut g = GainLossRatio::new(4).unwrap();
158 let out = g.batch(&[0.02, -0.01, 0.04, -0.03]);
159 assert_relative_eq!(out[3].unwrap(), 1.5, epsilon = 1e-9);
160 }
161
162 #[test]
163 fn no_losses_yields_infinity() {
164 let mut g = GainLossRatio::new(3).unwrap();
165 let out = g.batch(&[0.01, 0.02, 0.03]);
166 assert!(out[2].unwrap().is_infinite());
167 }
168
169 #[test]
170 fn flat_window_is_break_even() {
171 let mut g = GainLossRatio::new(3).unwrap();
172 let out = g.batch(&[0.0_f64; 3]);
173 assert_eq!(out[2], Some(1.0));
174 }
175
176 #[test]
177 fn ignores_non_finite_input() {
178 let mut g = GainLossRatio::new(3).unwrap();
179 assert_eq!(g.update(f64::NAN), None);
180 assert_eq!(g.update(f64::INFINITY), None);
181 }
182
183 #[test]
184 fn no_wins_but_losses_yields_zero() {
185 let mut g = GainLossRatio::new(3).unwrap();
187 let out = g.batch(&[-0.01, -0.02, -0.03]);
188 assert_eq!(out[2], Some(0.0));
189 }
190
191 #[test]
192 fn reset_clears_state() {
193 let mut g = GainLossRatio::new(3).unwrap();
194 g.batch(&[0.01, -0.02, 0.03]);
195 assert!(g.is_ready());
196 g.reset();
197 assert!(!g.is_ready());
198 assert_eq!(g.update(0.01), None);
199 }
200
201 #[test]
202 fn batch_equals_streaming() {
203 let returns: Vec<f64> = (0..40).map(|i| (f64::from(i) * 0.3).sin() * 0.01).collect();
204 let batch = GainLossRatio::new(10).unwrap().batch(&returns);
205 let mut s = GainLossRatio::new(10).unwrap();
206 let streamed: Vec<_> = returns.iter().map(|r| s.update(*r)).collect();
207 assert_eq!(batch, streamed);
208 }
209 #[test]
214 fn a_flat_window_is_not_confused_with_an_all_losing_one() {
215 let flat = [0.0_f64; 20];
216 let losing = [-0.01_f64; 20];
217
218 let mut a = GainLossRatio::new(14).unwrap();
219 let mut b = GainLossRatio::new(14).unwrap();
220 let (mut flat_value, mut losing_value) = (None, None);
221 for i in 0..flat.len() {
222 flat_value = a.update(flat[i]).or(flat_value);
223 losing_value = b.update(losing[i]).or(losing_value);
224 }
225
226 assert_eq!(flat_value, Some(1.0), "a flat window is break-even");
227 assert_eq!(losing_value, Some(0.0), "an all-losing window has no gains");
228 assert_ne!(flat_value, losing_value);
229 }
230}