qualia_core_db/specialized_libs/computational_economics/
risk.rs1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum RiskError {
8 InvalidInput,
9 OutputBufferTooSmall,
10}
11
12fn valid_return(x: f64) -> bool {
13 x.is_finite() && x > -1.0
14}
15
16#[cfg(not(test))]
17fn normal_quantile(probability: f64) -> Result<f64, RiskError> {
18 if !(0.0..1.0).contains(&probability) {
19 return Err(RiskError::InvalidInput);
20 }
21 Ok(crate::solvers::statistics::distributions::normal::standard_quantile(probability))
22}
23
24#[cfg(test)]
25fn normal_quantile(probability: f64) -> Result<f64, RiskError> {
26 if !(0.0..1.0).contains(&probability) {
27 return Err(RiskError::InvalidInput);
28 }
29
30 const A: [f64; 6] = [
32 -3.969_683_028_665_376e1,
33 2.209_460_984_245_205e2,
34 -2.759_285_104_469_687e2,
35 1.383_577_518_672_69e2,
36 -3.066_479_806_614_716e1,
37 2.506_628_277_459_239,
38 ];
39 const B: [f64; 5] = [
40 -5.447_609_879_822_406e1,
41 1.615_858_368_580_409e2,
42 -1.556_989_798_598_866e2,
43 6.680_131_188_771_972e1,
44 -1.328_068_155_288_572e1,
45 ];
46 const C: [f64; 6] = [
47 -7.784_894_002_430_293e-3,
48 -3.223_964_580_411_365e-1,
49 -2.400_758_277_161_838,
50 -2.549_732_539_343_734,
51 4.374_664_141_464_968,
52 2.938_163_982_698_783,
53 ];
54 const D: [f64; 4] = [
55 7.784_695_709_041_462e-3,
56 3.224_671_290_700_398e-1,
57 2.445_134_137_142_996,
58 3.754_408_661_907_416,
59 ];
60
61 let plow = 0.02425;
62 let phigh = 1.0 - plow;
63 if probability < plow {
64 let q = (-2.0 * probability.ln()).sqrt();
65 let numerator = ((((C[0] * q + C[1]) * q + C[2]) * q + C[3]) * q + C[4]) * q + C[5];
66 let denominator = (((D[0] * q + D[1]) * q + D[2]) * q + D[3]) * q + 1.0;
67 Ok(numerator / denominator)
68 } else if probability <= phigh {
69 let q = probability - 0.5;
70 let r = q * q;
71 let numerator = (((((A[0] * r + A[1]) * r + A[2]) * r + A[3]) * r + A[4]) * r + A[5]) * q;
72 let denominator = ((((B[0] * r + B[1]) * r + B[2]) * r + B[3]) * r + B[4]) * r + 1.0;
73 Ok(numerator / denominator)
74 } else {
75 let q = (-2.0 * (1.0 - probability).ln()).sqrt();
76 let numerator = ((((C[0] * q + C[1]) * q + C[2]) * q + C[3]) * q + C[4]) * q + C[5];
77 let denominator = (((D[0] * q + D[1]) * q + D[2]) * q + D[3]) * q + 1.0;
78 Ok(-(numerator / denominator))
79 }
80}
81
82pub fn sorted_returns_into(returns: &[f64], scratch: &mut [f64]) -> Result<usize, RiskError> {
84 if returns.is_empty() || scratch.len() < returns.len() {
85 return Err(RiskError::OutputBufferTooSmall);
86 }
87 for (idx, value) in returns.iter().enumerate() {
88 if !valid_return(*value) {
89 return Err(RiskError::InvalidInput);
90 }
91 scratch[idx] = *value;
92 }
93 scratch[..returns.len()].sort_by(|a, b| a.total_cmp(b));
94 Ok(returns.len())
95}
96
97pub fn historical_var(
99 returns: &[f64],
100 confidence: f64,
101 scratch: &mut [f64],
102) -> Result<f64, RiskError> {
103 if !(0.0..1.0).contains(&confidence) {
104 return Err(RiskError::InvalidInput);
105 }
106 let n = sorted_returns_into(returns, scratch)?;
107 let tail_probability = 1.0 - confidence;
108 let idx = ((tail_probability * n as f64).ceil() as usize).saturating_sub(1);
109 Ok((-scratch[idx]).max(0.0))
110}
111
112pub fn historical_cvar(
114 returns: &[f64],
115 confidence: f64,
116 scratch: &mut [f64],
117) -> Result<f64, RiskError> {
118 if !(0.0..1.0).contains(&confidence) {
119 return Err(RiskError::InvalidInput);
120 }
121 let n = sorted_returns_into(returns, scratch)?;
122 let tail_probability = 1.0 - confidence;
123 let tail_count = ((tail_probability * n as f64).ceil() as usize).max(1);
124 let mut loss = 0.0;
125 for value in scratch.iter().take(tail_count) {
126 loss += (-*value).max(0.0);
127 }
128 Ok(loss / tail_count as f64)
129}
130
131pub fn gaussian_var(mean: f64, std_dev: f64, confidence: f64) -> Result<f64, RiskError> {
133 if !mean.is_finite()
134 || !std_dev.is_finite()
135 || std_dev < 0.0
136 || !(0.0..1.0).contains(&confidence)
137 {
138 return Err(RiskError::InvalidInput);
139 }
140 let z_left_tail = normal_quantile(1.0 - confidence)?;
141 Ok((-(mean + z_left_tail * std_dev)).max(0.0))
142}
143
144pub fn scenario_loss(weights: &[f64], shocks: &[f64]) -> Result<f64, RiskError> {
146 if weights.is_empty() || weights.len() != shocks.len() {
147 return Err(RiskError::InvalidInput);
148 }
149 let mut scenario_return = 0.0;
150 for idx in 0..weights.len() {
151 if !weights[idx].is_finite() || !valid_return(shocks[idx]) {
152 return Err(RiskError::InvalidInput);
153 }
154 scenario_return += weights[idx] * shocks[idx];
155 }
156 Ok((-scenario_return).max(0.0))
157}
158
159pub fn scenario_losses_into(
161 weights: &[f64],
162 scenario_shocks: &[f64],
163 scenario_count: usize,
164 asset_count: usize,
165 out: &mut [f64],
166) -> Result<usize, RiskError> {
167 if scenario_count == 0
168 || asset_count == 0
169 || weights.len() != asset_count
170 || scenario_shocks.len() != scenario_count * asset_count
171 {
172 return Err(RiskError::InvalidInput);
173 }
174 if out.len() < scenario_count {
175 return Err(RiskError::OutputBufferTooSmall);
176 }
177
178 for scenario in 0..scenario_count {
179 let start = scenario * asset_count;
180 out[scenario] = scenario_loss(weights, &scenario_shocks[start..start + asset_count])?;
181 }
182 Ok(scenario_count)
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 #[test]
190 fn historical_var_uses_left_tail_loss() {
191 let returns = [-0.10, -0.05, 0.0, 0.02, 0.03];
192 let mut scratch = [0.0; 5];
193 let var = historical_var(&returns, 0.80, &mut scratch).unwrap();
194 assert!((var - 0.10).abs() < 1e-12);
195 }
196
197 #[test]
198 fn historical_cvar_averages_tail_losses() {
199 let returns = [-0.10, -0.05, 0.0, 0.02, 0.03];
200 let mut scratch = [0.0; 5];
201 let cvar = historical_cvar(&returns, 0.60, &mut scratch).unwrap();
202 assert!((cvar - 0.075).abs() < 1e-12);
203 }
204
205 #[test]
206 fn gaussian_var_is_positive_for_left_tail() {
207 let var = gaussian_var(0.0, 0.02, 0.95).unwrap();
208 assert!(var > 0.032 && var < 0.034);
209 }
210
211 #[test]
212 fn scenario_loss_is_weighted_negative_return() {
213 let weights = [0.25, 0.75];
214 let shocks = [-0.10, -0.20];
215 let loss = scenario_loss(&weights, &shocks).unwrap();
216 assert!((loss - 0.175).abs() < 1e-12);
217 }
218
219 #[test]
220 fn scenario_losses_use_row_major_shocks() {
221 let weights = [0.5, 0.5];
222 let shocks = [-0.10, 0.0, 0.0, -0.20];
223 let mut out = [0.0; 2];
224 scenario_losses_into(&weights, &shocks, 2, 2, &mut out).unwrap();
225 assert!((out[0] - 0.05).abs() < 1e-12);
226 assert!((out[1] - 0.10).abs() < 1e-12);
227 }
228
229 #[test]
230 fn invalid_confidence_is_rejected() {
231 let returns = [0.0, 0.1];
232 let mut scratch = [0.0; 2];
233 assert_eq!(
234 historical_var(&returns, 1.0, &mut scratch),
235 Err(RiskError::InvalidInput)
236 );
237 }
238
239 #[test]
240 fn scratch_too_small_is_rejected() {
241 let returns = [0.0, 0.1];
242 let mut scratch = [0.0; 1];
243 assert_eq!(
244 sorted_returns_into(&returns, &mut scratch),
245 Err(RiskError::OutputBufferTooSmall)
246 );
247 }
248}