wickra_core/indicators/
kelly_criterion.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
37pub struct KellyCriterion {
38 period: usize,
39 window: VecDeque<f64>,
40}
41
42impl KellyCriterion {
43 pub fn new(period: usize) -> Result<Self> {
48 if period == 0 {
49 return Err(Error::PeriodZero);
50 }
51 if period > crate::error::MAX_PERIOD {
52 return Err(Error::InvalidPeriod {
53 message: crate::error::PERIOD_ABOVE_MAX,
54 });
55 }
56 Ok(Self {
57 period,
58 window: VecDeque::with_capacity(period),
59 })
60 }
61
62 pub const fn period(&self) -> usize {
64 self.period
65 }
66}
67
68impl Indicator for KellyCriterion {
69 type Input = f64;
70 type Output = f64;
71
72 fn update(&mut self, input: f64) -> Option<f64> {
73 if !input.is_finite() {
74 return None;
75 }
76 if self.window.len() == self.period {
77 self.window.pop_front();
78 }
79 self.window.push_back(input);
80 if self.window.len() < self.period {
81 return None;
82 }
83 let mut sum_win = 0.0_f64;
84 let mut n_win = 0_u32;
85 let mut sum_loss = 0.0_f64;
86 let mut n_loss = 0_u32;
87 for &r in &self.window {
88 if r > 0.0 {
89 sum_win += r;
90 n_win += 1;
91 } else if r < 0.0 {
92 sum_loss += -r;
93 n_loss += 1;
94 }
95 }
96 let n = self.period as f64;
97 let win_rate = f64::from(n_win) / n;
98 if n_loss == 0 {
99 return Some(win_rate);
102 }
103 let avg_loss = sum_loss / f64::from(n_loss);
104 if n_win == 0 {
105 return Some(-1.0);
108 }
109 let avg_win = sum_win / f64::from(n_win);
110 let payoff = avg_win / avg_loss;
111 Some(win_rate - (1.0 - win_rate) / payoff)
112 }
113
114 fn reset(&mut self) {
115 self.window.clear();
116 }
117
118 #[inline]
119 fn warmup_period(&self) -> usize {
120 self.period
121 }
122
123 #[inline]
124 fn is_ready(&self) -> bool {
125 self.window.len() == self.period
126 }
127
128 #[inline]
129 fn name(&self) -> &'static str {
130 "KellyCriterion"
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use crate::traits::BatchExt;
138 use approx::assert_relative_eq;
139
140 #[test]
141 fn rejects_zero_period() {
142 assert!(matches!(KellyCriterion::new(0), Err(Error::PeriodZero)));
143 }
144
145 #[test]
146 fn accessors_and_metadata() {
147 let k = KellyCriterion::new(10).unwrap();
148 assert_eq!(k.period(), 10);
149 assert_eq!(k.name(), "KellyCriterion");
150 assert_eq!(k.warmup_period(), 10);
151 }
152
153 #[test]
154 fn reference_value() {
155 let mut k = KellyCriterion::new(4).unwrap();
160 let out = k.batch(&[0.02, 0.04, -0.01, -0.02]);
161 assert_relative_eq!(out[3].unwrap(), 0.25, epsilon = 1e-9);
162 }
163
164 #[test]
165 fn all_winners_returns_win_rate() {
166 let mut k = KellyCriterion::new(3).unwrap();
167 let out = k.batch(&[0.01, 0.02, 0.03]);
168 assert_relative_eq!(out[2].unwrap(), 1.0, epsilon = 1e-12);
169 }
170
171 #[test]
172 fn all_losers_returns_negative_one() {
173 let mut k = KellyCriterion::new(3).unwrap();
174 let out = k.batch(&[-0.01, -0.02, -0.03]);
175 assert_relative_eq!(out[2].unwrap(), -1.0, epsilon = 1e-12);
176 }
177
178 #[test]
179 fn flat_window_yields_zero() {
180 let mut k = KellyCriterion::new(3).unwrap();
181 let out = k.batch(&[0.0_f64; 3]);
182 assert_eq!(out[2], Some(0.0));
183 }
184
185 #[test]
186 fn ignores_non_finite_input() {
187 let mut k = KellyCriterion::new(3).unwrap();
188 assert_eq!(k.update(f64::NAN), None);
189 assert_eq!(k.update(f64::INFINITY), None);
190 }
191
192 #[test]
193 fn reset_clears_state() {
194 let mut k = KellyCriterion::new(3).unwrap();
195 k.batch(&[0.01, -0.02, 0.03]);
196 assert!(k.is_ready());
197 k.reset();
198 assert!(!k.is_ready());
199 assert_eq!(k.update(0.01), None);
200 }
201
202 #[test]
203 fn batch_equals_streaming() {
204 let returns: Vec<f64> = (0..40).map(|i| (f64::from(i) * 0.3).sin() * 0.01).collect();
205 let batch = KellyCriterion::new(10).unwrap().batch(&returns);
206 let mut s = KellyCriterion::new(10).unwrap();
207 let streamed: Vec<_> = returns.iter().map(|r| s.update(*r)).collect();
208 assert_eq!(batch, streamed);
209 }
210}