wickra_core/indicators/
recovery_factor.rs1use crate::traits::Indicator;
4
5#[derive(Debug, Clone, Default)]
39pub struct RecoveryFactor {
40 first: f64,
41 last: f64,
42 peak: f64,
43 max_dd: f64,
44 seen: bool,
45}
46
47impl RecoveryFactor {
48 pub const fn new() -> Self {
50 Self {
51 first: 0.0,
52 last: 0.0,
53 peak: f64::NEG_INFINITY,
54 max_dd: 0.0,
55 seen: false,
56 }
57 }
58
59 pub fn value(&self) -> Option<f64> {
61 if !self.seen || self.first == 0.0 {
62 return None;
63 }
64 if self.max_dd == 0.0 {
65 return Some(0.0);
66 }
67 let net_return = (self.last / self.first) - 1.0;
68 Some(net_return / self.max_dd)
69 }
70}
71
72impl Indicator for RecoveryFactor {
73 type Input = f64;
74 type Output = f64;
75
76 #[inline]
77 fn update(&mut self, input: f64) -> Option<f64> {
78 if !input.is_finite() {
79 return None;
80 }
81 if self.seen {
82 if input > self.peak {
83 self.peak = input;
84 }
85 if self.peak > 0.0 {
86 let dd = (self.peak - input) / self.peak;
87 if dd > self.max_dd {
88 self.max_dd = dd;
89 }
90 }
91 } else {
92 self.first = input;
93 self.peak = input;
94 self.seen = true;
95 }
96 self.last = input;
97 self.value()
98 }
99
100 fn reset(&mut self) {
101 self.first = 0.0;
102 self.last = 0.0;
103 self.peak = f64::NEG_INFINITY;
104 self.max_dd = 0.0;
105 self.seen = false;
106 }
107
108 #[inline]
109 fn warmup_period(&self) -> usize {
110 1
111 }
112
113 #[inline]
114 fn is_ready(&self) -> bool {
115 self.seen && self.first != 0.0
116 }
117
118 #[inline]
119 fn name(&self) -> &'static str {
120 "RecoveryFactor"
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use crate::traits::BatchExt;
128 use approx::assert_relative_eq;
129
130 #[test]
131 fn accessors_and_metadata() {
132 let r = RecoveryFactor::new();
133 assert_eq!(r.name(), "RecoveryFactor");
134 assert_eq!(r.warmup_period(), 1);
135 assert_eq!(r.value(), None);
136 }
137
138 #[test]
139 fn pure_uptrend_yields_zero() {
140 let mut r = RecoveryFactor::new();
141 for v in 1..=10 {
142 r.update(f64::from(v));
143 }
144 assert_eq!(r.value(), Some(0.0));
146 }
147
148 #[test]
149 fn reference_value() {
150 let mut r = RecoveryFactor::new();
153 let out = r.batch(&[100.0, 110.0, 105.0, 95.0, 88.0, 100.0, 120.0, 130.0]);
154 let last = out.last().copied().unwrap().unwrap();
155 assert_relative_eq!(last, 0.30 / 0.20, epsilon = 1e-9);
156 }
157
158 #[test]
159 fn ignores_non_finite_input() {
160 let mut r = RecoveryFactor::new();
161 r.update(100.0);
162 r.update(90.0);
163 let v = r.value();
164 assert_eq!(r.update(f64::NAN), None);
165 assert_eq!(r.update(f64::INFINITY), None);
166 assert_eq!(r.value(), v);
168 }
169
170 #[test]
171 fn first_value_alone_yields_zero() {
172 let mut r = RecoveryFactor::new();
174 assert_eq!(r.update(100.0), Some(0.0));
175 }
176
177 #[test]
178 fn first_zero_equity_keeps_value_none() {
179 let mut r = RecoveryFactor::new();
182 assert_eq!(r.update(0.0), None);
183 assert!(!r.is_ready());
184 }
185
186 #[test]
187 fn reset_clears_state() {
188 let mut r = RecoveryFactor::new();
189 r.batch(&[100.0, 90.0, 80.0]);
190 assert!(r.is_ready());
191 r.reset();
192 assert!(!r.is_ready());
193 assert_eq!(r.update(100.0), Some(0.0));
194 }
195
196 #[test]
197 fn batch_equals_streaming() {
198 let prices: Vec<f64> = (0..40)
199 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 8.0)
200 .collect();
201 let batch = RecoveryFactor::new().batch(&prices);
202 let mut s = RecoveryFactor::new();
203 let streamed: Vec<_> = prices.iter().map(|p| s.update(*p)).collect();
204 assert_eq!(batch, streamed);
205 }
206
207 #[test]
208 fn non_positive_peak_skips_drawdown_calc() {
209 let mut r = RecoveryFactor::new();
213 assert_eq!(r.update(-1.0), Some(0.0));
214 assert_eq!(r.update(-2.0), Some(0.0));
215 assert_eq!(r.update(-0.5), Some(0.0));
216 assert!(r.is_ready());
217 }
218}