wickra_core/indicators/
burke_ratio.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
45pub struct BurkeRatio {
46 period: usize,
47 window: VecDeque<f64>,
48}
49
50impl BurkeRatio {
51 pub fn new(period: usize) -> Result<Self> {
57 if period < 2 {
58 return Err(Error::InvalidPeriod {
59 message: "burke ratio needs period >= 2",
60 });
61 }
62 if period > crate::error::MAX_PERIOD {
63 return Err(Error::InvalidPeriod {
64 message: crate::error::PERIOD_ABOVE_MAX,
65 });
66 }
67 Ok(Self {
68 period,
69 window: VecDeque::with_capacity(period),
70 })
71 }
72
73 pub const fn period(&self) -> usize {
75 self.period
76 }
77
78 fn compute(&self) -> f64 {
79 #[allow(clippy::cast_precision_loss)]
80 let length = self.window.len() as f64;
81 let mut sum_return = 0.0;
82 let mut sum_drawdown_sq = 0.0;
83 let mut equity = 1.0;
84 let mut peak: f64 = 1.0;
85 for ret in &self.window {
86 sum_return += *ret;
87 equity *= 1.0 + *ret;
88 peak = peak.max(equity);
89 let drawdown = (peak - equity) / peak;
90 sum_drawdown_sq += drawdown * drawdown;
91 }
92 let denom = sum_drawdown_sq.sqrt();
93 if denom > 0.0 {
94 (sum_return / length) / denom
95 } else {
96 0.0
97 }
98 }
99}
100
101impl Indicator for BurkeRatio {
102 type Input = f64;
103 type Output = f64;
104
105 #[inline]
106 fn update(&mut self, ret: f64) -> Option<f64> {
107 if !ret.is_finite() {
108 return None;
109 }
110 if self.window.len() == self.period {
111 self.window.pop_front();
112 }
113 self.window.push_back(ret);
114 if self.window.len() < self.period {
115 return None;
116 }
117 Some(self.compute())
118 }
119
120 fn reset(&mut self) {
121 self.window.clear();
122 }
123
124 #[inline]
125 fn warmup_period(&self) -> usize {
126 self.period
127 }
128
129 #[inline]
130 fn is_ready(&self) -> bool {
131 self.window.len() == self.period
132 }
133
134 #[inline]
135 fn name(&self) -> &'static str {
136 "BurkeRatio"
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143 use crate::traits::BatchExt;
144 use approx::assert_relative_eq;
145
146 #[test]
147 fn rejects_period_less_than_two() {
148 assert!(matches!(
149 BurkeRatio::new(1),
150 Err(Error::InvalidPeriod { .. })
151 ));
152 }
153
154 #[test]
155 fn accessors_and_metadata() {
156 let br = BurkeRatio::new(12).unwrap();
157 assert_eq!(br.period(), 12);
158 assert_eq!(br.warmup_period(), 12);
159 assert_eq!(br.name(), "BurkeRatio");
160 assert!(!br.is_ready());
161 }
162
163 #[test]
164 fn reference_value() {
165 let mut br = BurkeRatio::new(3).unwrap();
169 let out = br.batch(&[0.1, -0.1, 0.1]);
170 let expected = (0.1_f64 / 3.0) / (0.0101_f64).sqrt();
171 assert_relative_eq!(out[2].unwrap(), expected, epsilon = 1e-9);
172 }
173
174 #[test]
175 fn no_drawdown_is_zero() {
176 let mut br = BurkeRatio::new(3).unwrap();
177 let last = br
178 .batch(&[0.01, 0.02, 0.03])
179 .into_iter()
180 .flatten()
181 .last()
182 .unwrap();
183 assert_relative_eq!(last, 0.0, epsilon = 1e-12);
184 }
185
186 #[test]
187 fn losing_window_is_negative() {
188 let mut br = BurkeRatio::new(3).unwrap();
189 let last = br
190 .batch(&[-0.05, -0.02, -0.03])
191 .into_iter()
192 .flatten()
193 .last()
194 .unwrap();
195 assert!(last < 0.0);
196 }
197
198 #[test]
199 fn ignores_non_finite_input() {
200 let mut br = BurkeRatio::new(3).unwrap();
201 assert_eq!(br.update(0.1), None);
202 assert_eq!(br.update(f64::NAN), None);
203 assert_eq!(br.update(-0.1), None);
204 assert!(br.update(0.1).is_some());
205 }
206
207 #[test]
208 fn reset_clears_state() {
209 let mut br = BurkeRatio::new(3).unwrap();
210 br.batch(&[0.1, -0.1, 0.1]);
211 assert!(br.is_ready());
212 br.reset();
213 assert!(!br.is_ready());
214 assert_eq!(br.update(0.1), None);
215 }
216
217 #[test]
218 fn batch_equals_streaming() {
219 let rets: Vec<f64> = (0..60)
220 .map(|i| (f64::from(i) * 0.25).sin() * 0.05)
221 .collect();
222 let batch = BurkeRatio::new(12).unwrap().batch(&rets);
223 let mut streamer = BurkeRatio::new(12).unwrap();
224 let streamed: Vec<_> = rets.iter().map(|r| streamer.update(*r)).collect();
225 assert_eq!(batch, streamed);
226 }
227}