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