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)]
26pub struct GainLossRatio {
27 period: usize,
28 window: VecDeque<f64>,
29}
30
31impl GainLossRatio {
32 pub fn new(period: usize) -> Result<Self> {
37 if period == 0 {
38 return Err(Error::PeriodZero);
39 }
40 Ok(Self {
41 period,
42 window: VecDeque::with_capacity(period),
43 })
44 }
45
46 pub const fn period(&self) -> usize {
48 self.period
49 }
50}
51
52impl Indicator for GainLossRatio {
53 type Input = f64;
54 type Output = f64;
55
56 fn update(&mut self, input: f64) -> Option<f64> {
57 if !input.is_finite() {
58 return None;
59 }
60 if self.window.len() == self.period {
61 self.window.pop_front();
62 }
63 self.window.push_back(input);
64 if self.window.len() < self.period {
65 return None;
66 }
67 let mut sum_win = 0.0_f64;
68 let mut n_win = 0_u32;
69 let mut sum_loss = 0.0_f64;
70 let mut n_loss = 0_u32;
71 for &r in &self.window {
72 if r > 0.0 {
73 sum_win += r;
74 n_win += 1;
75 } else if r < 0.0 {
76 sum_loss += -r;
77 n_loss += 1;
78 }
79 }
80 if n_loss == 0 {
81 return Some(if n_win == 0 { 0.0 } else { f64::INFINITY });
82 }
83 let avg_win = if n_win == 0 {
84 0.0
85 } else {
86 sum_win / f64::from(n_win)
87 };
88 let avg_loss = sum_loss / f64::from(n_loss);
89 Some(avg_win / avg_loss)
90 }
91
92 fn reset(&mut self) {
93 self.window.clear();
94 }
95
96 fn warmup_period(&self) -> usize {
97 self.period
98 }
99
100 fn is_ready(&self) -> bool {
101 self.window.len() == self.period
102 }
103
104 fn name(&self) -> &'static str {
105 "GainLossRatio"
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112 use crate::traits::BatchExt;
113 use approx::assert_relative_eq;
114
115 #[test]
116 fn rejects_zero_period() {
117 assert!(matches!(GainLossRatio::new(0), Err(Error::PeriodZero)));
118 }
119
120 #[test]
121 fn accessors_and_metadata() {
122 let g = GainLossRatio::new(10).unwrap();
123 assert_eq!(g.period(), 10);
124 assert_eq!(g.name(), "GainLossRatio");
125 assert_eq!(g.warmup_period(), 10);
126 }
127
128 #[test]
129 fn reference_value() {
130 let mut g = GainLossRatio::new(4).unwrap();
133 let out = g.batch(&[0.02, -0.01, 0.04, -0.03]);
134 assert_relative_eq!(out[3].unwrap(), 1.5, epsilon = 1e-9);
135 }
136
137 #[test]
138 fn no_losses_yields_infinity() {
139 let mut g = GainLossRatio::new(3).unwrap();
140 let out = g.batch(&[0.01, 0.02, 0.03]);
141 assert!(out[2].unwrap().is_infinite());
142 }
143
144 #[test]
145 fn flat_window_yields_zero() {
146 let mut g = GainLossRatio::new(3).unwrap();
147 let out = g.batch(&[0.0_f64; 3]);
148 assert_eq!(out[2], Some(0.0));
149 }
150
151 #[test]
152 fn ignores_non_finite_input() {
153 let mut g = GainLossRatio::new(3).unwrap();
154 assert_eq!(g.update(f64::NAN), None);
155 assert_eq!(g.update(f64::INFINITY), None);
156 }
157
158 #[test]
159 fn no_wins_but_losses_yields_zero() {
160 let mut g = GainLossRatio::new(3).unwrap();
162 let out = g.batch(&[-0.01, -0.02, -0.03]);
163 assert_eq!(out[2], Some(0.0));
164 }
165
166 #[test]
167 fn reset_clears_state() {
168 let mut g = GainLossRatio::new(3).unwrap();
169 g.batch(&[0.01, -0.02, 0.03]);
170 assert!(g.is_ready());
171 g.reset();
172 assert!(!g.is_ready());
173 assert_eq!(g.update(0.01), None);
174 }
175
176 #[test]
177 fn batch_equals_streaming() {
178 let returns: Vec<f64> = (0..40).map(|i| (f64::from(i) * 0.3).sin() * 0.01).collect();
179 let batch = GainLossRatio::new(10).unwrap().batch(&returns);
180 let mut s = GainLossRatio::new(10).unwrap();
181 let streamed: Vec<_> = returns.iter().map(|r| s.update(*r)).collect();
182 assert_eq!(batch, streamed);
183 }
184}