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