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