wickra_core/indicators/
k_ratio.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
51pub struct KRatio {
52 period: usize,
53 window: VecDeque<f64>,
54}
55
56impl KRatio {
57 pub fn new(period: usize) -> Result<Self> {
64 if period < 3 {
65 return Err(Error::InvalidPeriod {
66 message: "k-ratio needs period >= 3",
67 });
68 }
69 if period > crate::error::MAX_PERIOD {
70 return Err(Error::InvalidPeriod {
71 message: crate::error::PERIOD_ABOVE_MAX,
72 });
73 }
74 Ok(Self {
75 period,
76 window: VecDeque::with_capacity(period),
77 })
78 }
79
80 pub const fn period(&self) -> usize {
82 self.period
83 }
84
85 fn compute(&self) -> f64 {
86 let count = self.window.len();
87 #[allow(clippy::cast_precision_loss)]
88 let length = count as f64;
89 let mut equity = 0.0;
91 let mut curve: Vec<f64> = Vec::with_capacity(count);
92 let mut sum_equity = 0.0;
93 for ret in &self.window {
94 equity += *ret;
95 curve.push(equity);
96 sum_equity += equity;
97 }
98 let mean_time = f64::midpoint(length, 1.0);
100 let mean_equity = sum_equity / length;
101 let mut sxx = 0.0;
102 let mut sxy = 0.0;
103 for (index, value) in curve.iter().enumerate() {
104 #[allow(clippy::cast_precision_loss)]
105 let time = (index + 1) as f64;
106 let dt = time - mean_time;
107 sxx += dt * dt;
108 sxy += dt * (value - mean_equity);
109 }
110 let slope = sxy / sxx;
112 let intercept = mean_equity - slope * mean_time;
113 let mut sse = 0.0;
114 for (index, value) in curve.iter().enumerate() {
115 #[allow(clippy::cast_precision_loss)]
116 let time = (index + 1) as f64;
117 let residual = value - (intercept + slope * time);
118 sse += residual * residual;
119 }
120 if sse <= 0.0 {
121 return 0.0;
122 }
123 let se_slope = (sse / (length - 2.0) / sxx).sqrt();
124 slope / se_slope
125 }
126}
127
128impl Indicator for KRatio {
129 type Input = f64;
130 type Output = f64;
131
132 #[inline]
133 fn update(&mut self, ret: f64) -> Option<f64> {
134 if !ret.is_finite() {
135 return None;
136 }
137 if self.window.len() == self.period {
138 self.window.pop_front();
139 }
140 self.window.push_back(ret);
141 if self.window.len() < self.period {
142 return None;
143 }
144 Some(self.compute())
145 }
146
147 fn reset(&mut self) {
148 self.window.clear();
149 }
150
151 #[inline]
152 fn warmup_period(&self) -> usize {
153 self.period
154 }
155
156 #[inline]
157 fn is_ready(&self) -> bool {
158 self.window.len() == self.period
159 }
160
161 #[inline]
162 fn name(&self) -> &'static str {
163 "KRatio"
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170 use crate::traits::BatchExt;
171 use approx::assert_relative_eq;
172
173 #[test]
174 fn rejects_period_less_than_three() {
175 assert!(matches!(KRatio::new(2), Err(Error::InvalidPeriod { .. })));
176 assert!(matches!(KRatio::new(0), Err(Error::InvalidPeriod { .. })));
177 }
178
179 #[test]
180 fn accessors_and_metadata() {
181 let kr = KRatio::new(30).unwrap();
182 assert_eq!(kr.period(), 30);
183 assert_eq!(kr.warmup_period(), 30);
184 assert_eq!(kr.name(), "KRatio");
185 assert!(!kr.is_ready());
186 }
187
188 #[test]
189 fn reference_value() {
190 let mut kr = KRatio::new(3).unwrap();
194 let out = kr.batch(&[0.01, 0.02, 0.03]);
195 let expected = 0.025_f64 / (1.0_f64 / 120_000.0).sqrt();
196 assert_relative_eq!(out[2].unwrap(), expected, epsilon = 1e-6);
197 }
198
199 #[test]
200 fn constant_returns_are_degenerate_zero() {
201 let mut kr = KRatio::new(4).unwrap();
203 let last = kr.batch(&[0.01; 4]).into_iter().flatten().last().unwrap();
204 assert_relative_eq!(last, 0.0, epsilon = 1e-12);
205 }
206
207 #[test]
208 fn rising_curve_is_positive() {
209 let mut kr = KRatio::new(5).unwrap();
210 let last = kr
211 .batch(&[0.01, 0.012, 0.009, 0.011, 0.013])
212 .into_iter()
213 .flatten()
214 .last()
215 .unwrap();
216 assert!(last > 0.0);
217 }
218
219 #[test]
220 fn ignores_non_finite_input() {
221 let mut kr = KRatio::new(3).unwrap();
222 assert_eq!(kr.update(0.01), None);
223 assert_eq!(kr.update(f64::NAN), None);
224 assert_eq!(kr.update(0.02), None);
225 assert!(kr.update(0.03).is_some());
226 }
227
228 #[test]
229 fn reset_clears_state() {
230 let mut kr = KRatio::new(3).unwrap();
231 kr.batch(&[0.01, 0.02, 0.03]);
232 assert!(kr.is_ready());
233 kr.reset();
234 assert!(!kr.is_ready());
235 assert_eq!(kr.update(0.01), None);
236 }
237
238 #[test]
239 fn batch_equals_streaming() {
240 let rets: Vec<f64> = (0..60)
241 .map(|i| 0.001 + (f64::from(i) * 0.25).sin() * 0.01)
242 .collect();
243 let batch = KRatio::new(20).unwrap().batch(&rets);
244 let mut streamer = KRatio::new(20).unwrap();
245 let streamed: Vec<_> = rets.iter().map(|r| streamer.update(*r)).collect();
246 assert_eq!(batch, streamed);
247 }
248}