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