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