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