wickra_core/indicators/
upside_potential_ratio.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
45pub struct UpsidePotentialRatio {
46 period: usize,
47 mar: f64,
48 window: VecDeque<f64>,
49 sum_upside: f64,
50 sum_downside_sq: f64,
51}
52
53impl UpsidePotentialRatio {
54 pub fn new(period: usize, mar: f64) -> Result<Self> {
62 if period < 2 {
63 return Err(Error::InvalidPeriod {
64 message: "upside potential ratio needs period >= 2",
65 });
66 }
67 if !mar.is_finite() {
68 return Err(Error::InvalidParameter {
69 message: "mar must be finite",
70 });
71 }
72 Ok(Self {
73 period,
74 mar,
75 window: VecDeque::with_capacity(period),
76 sum_upside: 0.0,
77 sum_downside_sq: 0.0,
78 })
79 }
80
81 pub const fn period(&self) -> usize {
83 self.period
84 }
85
86 pub const fn mar(&self) -> f64 {
88 self.mar
89 }
90}
91
92impl Indicator for UpsidePotentialRatio {
93 type Input = f64;
94 type Output = f64;
95
96 fn update(&mut self, ret: f64) -> Option<f64> {
97 if !ret.is_finite() {
98 return None;
99 }
100 if self.window.len() == self.period {
101 let old = self.window.pop_front().expect("non-empty");
102 let excess = old - self.mar;
103 self.sum_upside -= excess.max(0.0);
104 self.sum_downside_sq -= excess.min(0.0).powi(2);
105 }
106 let excess = ret - self.mar;
107 self.sum_upside += excess.max(0.0);
108 self.sum_downside_sq += excess.min(0.0).powi(2);
109 self.window.push_back(ret);
110 if self.window.len() < self.period {
111 return None;
112 }
113 let n = self.period as f64;
114 let upside_mean = self.sum_upside / n;
115 let downside_dev = (self.sum_downside_sq / n).sqrt();
116 if downside_dev > 0.0 {
117 Some(upside_mean / downside_dev)
118 } else {
119 Some(0.0)
120 }
121 }
122
123 fn reset(&mut self) {
124 self.window.clear();
125 self.sum_upside = 0.0;
126 self.sum_downside_sq = 0.0;
127 }
128
129 fn warmup_period(&self) -> usize {
130 self.period
131 }
132
133 fn is_ready(&self) -> bool {
134 self.window.len() == self.period
135 }
136
137 fn name(&self) -> &'static str {
138 "UpsidePotentialRatio"
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use crate::traits::BatchExt;
146 use approx::assert_relative_eq;
147
148 #[test]
149 fn rejects_period_less_than_two() {
150 assert!(matches!(
151 UpsidePotentialRatio::new(1, 0.0),
152 Err(Error::InvalidPeriod { .. })
153 ));
154 }
155
156 #[test]
157 fn rejects_non_finite_mar() {
158 assert!(matches!(
159 UpsidePotentialRatio::new(10, f64::NAN),
160 Err(Error::InvalidParameter { .. })
161 ));
162 }
163
164 #[test]
165 fn accessors_and_metadata() {
166 let upr = UpsidePotentialRatio::new(20, 0.001).unwrap();
167 assert_eq!(upr.period(), 20);
168 assert_relative_eq!(upr.mar(), 0.001, epsilon = 1e-12);
169 assert_eq!(upr.warmup_period(), 20);
170 assert_eq!(upr.name(), "UpsidePotentialRatio");
171 }
172
173 #[test]
174 fn reference_value() {
175 let mut upr = UpsidePotentialRatio::new(4, 0.0).unwrap();
180 let out = upr.batch(&[0.02, -0.01, 0.03, -0.02]);
181 let expected = 0.0125_f64 / (0.000_125_f64).sqrt();
182 assert_relative_eq!(out[3].unwrap(), expected, epsilon = 1e-9);
183 }
184
185 #[test]
186 fn no_downside_is_zero() {
187 let mut upr = UpsidePotentialRatio::new(3, 0.0).unwrap();
188 let last = upr
189 .batch(&[0.01, 0.02, 0.03])
190 .into_iter()
191 .flatten()
192 .last()
193 .unwrap();
194 assert_relative_eq!(last, 0.0, epsilon = 1e-12);
195 }
196
197 #[test]
198 fn ignores_non_finite_input() {
199 let mut upr = UpsidePotentialRatio::new(3, 0.0).unwrap();
200 assert_eq!(upr.update(0.01), None);
201 assert_eq!(upr.update(f64::INFINITY), None);
202 assert_eq!(upr.update(-0.02), None);
203 assert!(upr.update(0.03).is_some());
204 }
205
206 #[test]
207 fn reset_clears_state() {
208 let mut upr = UpsidePotentialRatio::new(2, 0.0).unwrap();
209 upr.batch(&[0.02, -0.01]);
210 assert!(upr.is_ready());
211 upr.reset();
212 assert!(!upr.is_ready());
213 assert_eq!(upr.update(0.01), None);
214 }
215
216 #[test]
217 fn batch_equals_streaming() {
218 let rets: Vec<f64> = (0..60)
219 .map(|i| (f64::from(i) * 0.25).sin() * 0.02)
220 .collect();
221 let batch = UpsidePotentialRatio::new(12, 0.0).unwrap().batch(&rets);
222 let mut streamer = UpsidePotentialRatio::new(12, 0.0).unwrap();
223 let streamed: Vec<_> = rets.iter().map(|r| streamer.update(*r)).collect();
224 assert_eq!(batch, streamed);
225 }
226}