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