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 fn update(&mut self, input: f64) -> Option<f64> {
77 if !input.is_finite() {
78 return self.value();
79 }
80 if self.seen {
81 if input > self.peak {
82 self.peak = input;
83 }
84 if self.peak > 0.0 {
85 let dd = (self.peak - input) / self.peak;
86 if dd > self.max_dd {
87 self.max_dd = dd;
88 }
89 }
90 } else {
91 self.first = input;
92 self.peak = input;
93 self.seen = true;
94 }
95 self.last = input;
96 self.value()
97 }
98
99 fn reset(&mut self) {
100 self.first = 0.0;
101 self.last = 0.0;
102 self.peak = f64::NEG_INFINITY;
103 self.max_dd = 0.0;
104 self.seen = false;
105 }
106
107 fn warmup_period(&self) -> usize {
108 1
109 }
110
111 fn is_ready(&self) -> bool {
112 self.seen && self.first != 0.0
113 }
114
115 fn name(&self) -> &'static str {
116 "RecoveryFactor"
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123 use crate::traits::BatchExt;
124 use approx::assert_relative_eq;
125
126 #[test]
127 fn accessors_and_metadata() {
128 let r = RecoveryFactor::new();
129 assert_eq!(r.name(), "RecoveryFactor");
130 assert_eq!(r.warmup_period(), 1);
131 assert_eq!(r.value(), None);
132 }
133
134 #[test]
135 fn pure_uptrend_yields_zero() {
136 let mut r = RecoveryFactor::new();
137 for v in 1..=10 {
138 r.update(f64::from(v));
139 }
140 assert_eq!(r.value(), Some(0.0));
142 }
143
144 #[test]
145 fn reference_value() {
146 let mut r = RecoveryFactor::new();
149 let out = r.batch(&[100.0, 110.0, 105.0, 95.0, 88.0, 100.0, 120.0, 130.0]);
150 let last = out.last().copied().unwrap().unwrap();
151 assert_relative_eq!(last, 0.30 / 0.20, epsilon = 1e-9);
152 }
153
154 #[test]
155 fn ignores_non_finite_input() {
156 let mut r = RecoveryFactor::new();
157 r.update(100.0);
158 r.update(90.0);
159 let v = r.value();
160 assert_eq!(r.update(f64::NAN), v);
161 assert_eq!(r.update(f64::INFINITY), v);
162 }
163
164 #[test]
165 fn first_value_alone_yields_zero() {
166 let mut r = RecoveryFactor::new();
168 assert_eq!(r.update(100.0), Some(0.0));
169 }
170
171 #[test]
172 fn first_zero_equity_keeps_value_none() {
173 let mut r = RecoveryFactor::new();
176 assert_eq!(r.update(0.0), None);
177 assert!(!r.is_ready());
178 }
179
180 #[test]
181 fn reset_clears_state() {
182 let mut r = RecoveryFactor::new();
183 r.batch(&[100.0, 90.0, 80.0]);
184 assert!(r.is_ready());
185 r.reset();
186 assert!(!r.is_ready());
187 assert_eq!(r.update(100.0), Some(0.0));
188 }
189
190 #[test]
191 fn batch_equals_streaming() {
192 let prices: Vec<f64> = (0..40)
193 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 8.0)
194 .collect();
195 let batch = RecoveryFactor::new().batch(&prices);
196 let mut s = RecoveryFactor::new();
197 let streamed: Vec<_> = prices.iter().map(|p| s.update(*p)).collect();
198 assert_eq!(batch, streamed);
199 }
200
201 #[test]
202 fn non_positive_peak_skips_drawdown_calc() {
203 let mut r = RecoveryFactor::new();
207 assert_eq!(r.update(-1.0), Some(0.0));
208 assert_eq!(r.update(-2.0), Some(0.0));
209 assert_eq!(r.update(-0.5), Some(0.0));
210 assert!(r.is_ready());
211 }
212}