wickra_core/indicators/
value_at_risk.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
43pub struct ValueAtRisk {
44 period: usize,
45 confidence: f64,
46 window: VecDeque<f64>,
47 scratch: Vec<f64>,
49}
50
51impl ValueAtRisk {
52 pub fn new(period: usize, confidence: f64) -> Result<Self> {
58 if period < 2 {
59 return Err(Error::InvalidPeriod {
60 message: "value-at-risk needs period >= 2",
61 });
62 }
63 if period > crate::error::MAX_PERIOD {
64 return Err(Error::InvalidPeriod {
65 message: crate::error::PERIOD_ABOVE_MAX,
66 });
67 }
68 if !confidence.is_finite() || confidence <= 0.0 || confidence >= 1.0 {
69 return Err(Error::InvalidPeriod {
70 message: "confidence must lie strictly between 0 and 1",
71 });
72 }
73 Ok(Self {
74 period,
75 confidence,
76 window: VecDeque::with_capacity(period),
77 scratch: Vec::with_capacity(period),
78 })
79 }
80
81 pub const fn period(&self) -> usize {
83 self.period
84 }
85
86 pub const fn confidence(&self) -> f64 {
88 self.confidence
89 }
90}
91
92fn percentile_sorted(sorted: &[f64], q: f64) -> f64 {
94 let n = sorted.len();
95 let pos = q * (n - 1) as f64;
96 let lo = pos.floor() as usize;
97 let hi = pos.ceil() as usize;
98 if lo == hi {
99 sorted[lo]
100 } else {
101 let frac = pos - lo as f64;
102 sorted[lo] + (sorted[hi] - sorted[lo]) * frac
103 }
104}
105
106impl Indicator for ValueAtRisk {
107 type Input = f64;
108 type Output = f64;
109
110 #[inline]
111 fn update(&mut self, input: f64) -> Option<f64> {
112 if !input.is_finite() {
113 return None;
114 }
115 if self.window.len() == self.period {
116 self.window.pop_front();
117 }
118 self.window.push_back(input);
119 if self.window.len() < self.period {
120 return None;
121 }
122 self.scratch.clear();
123 self.scratch.extend(self.window.iter().copied());
124 self.scratch.sort_unstable_by(f64::total_cmp);
125 let q = 1.0 - self.confidence;
126 let cut = percentile_sorted(&self.scratch, q);
127 Some((-cut).max(0.0))
129 }
130
131 fn reset(&mut self) {
132 self.window.clear();
133 self.scratch.clear();
134 }
135
136 #[inline]
137 fn warmup_period(&self) -> usize {
138 self.period
139 }
140
141 #[inline]
142 fn is_ready(&self) -> bool {
143 self.window.len() == self.period
144 }
145
146 #[inline]
147 fn name(&self) -> &'static str {
148 "ValueAtRisk"
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155 use crate::traits::BatchExt;
156 use approx::assert_relative_eq;
157
158 #[test]
159 fn rejects_invalid_params() {
160 assert!(matches!(
161 ValueAtRisk::new(1, 0.95),
162 Err(Error::InvalidPeriod { .. })
163 ));
164 assert!(matches!(
165 ValueAtRisk::new(20, 0.0),
166 Err(Error::InvalidPeriod { .. })
167 ));
168 assert!(matches!(
169 ValueAtRisk::new(20, 1.0),
170 Err(Error::InvalidPeriod { .. })
171 ));
172 assert!(matches!(
173 ValueAtRisk::new(20, f64::NAN),
174 Err(Error::InvalidPeriod { .. })
175 ));
176 }
177
178 #[test]
179 fn accessors_and_metadata() {
180 let v = ValueAtRisk::new(100, 0.95).unwrap();
181 assert_eq!(v.period(), 100);
182 assert_relative_eq!(v.confidence(), 0.95, epsilon = 1e-12);
183 assert_eq!(v.name(), "ValueAtRisk");
184 assert_eq!(v.warmup_period(), 100);
185 }
186
187 #[test]
188 fn reference_value() {
189 let mut v = ValueAtRisk::new(10, 0.95).unwrap();
194 let returns: Vec<f64> = (-5..5).map(|i| f64::from(i) * 0.01).collect();
195 let out = v.batch(&returns);
196 assert_relative_eq!(out[9].unwrap(), 0.0455, epsilon = 1e-9);
197 }
198
199 #[test]
200 fn all_positive_returns_yield_zero() {
201 let mut v = ValueAtRisk::new(5, 0.95).unwrap();
202 let out = v.batch(&[0.01, 0.02, 0.03, 0.04, 0.05]);
203 assert_eq!(out[4], Some(0.0));
204 }
205
206 #[test]
207 fn ignores_non_finite_input() {
208 let mut v = ValueAtRisk::new(3, 0.95).unwrap();
209 assert_eq!(v.update(f64::NAN), None);
210 assert_eq!(v.update(f64::INFINITY), None);
211 }
212
213 #[test]
214 fn reset_clears_state() {
215 let mut v = ValueAtRisk::new(3, 0.95).unwrap();
216 v.batch(&[-0.01, -0.02, -0.03]);
217 assert!(v.is_ready());
218 v.reset();
219 assert!(!v.is_ready());
220 assert_eq!(v.update(0.01), None);
221 }
222
223 #[test]
224 fn batch_equals_streaming() {
225 let returns: Vec<f64> = (0..50).map(|i| (f64::from(i) * 0.2).sin() * 0.02).collect();
226 let batch = ValueAtRisk::new(10, 0.95).unwrap().batch(&returns);
227 let mut s = ValueAtRisk::new(10, 0.95).unwrap();
228 let streamed: Vec<_> = returns.iter().map(|r| s.update(*r)).collect();
229 assert_eq!(batch, streamed);
230 }
231
232 #[test]
233 fn integer_position_quantile_branch() {
234 let mut v = ValueAtRisk::new(5, 0.75).unwrap();
237 let out = v.batch(&[-0.05, -0.04, -0.03, -0.02, -0.01]);
238 assert_relative_eq!(out[4].unwrap(), 0.04, epsilon = 1e-12);
240 }
241}