use crate::errors::QlResult;
use crate::types::Real;
use crate::{ensure, fail, require};
use super::EmpiricalStatistics;
pub trait RiskStatistics: EmpiricalStatistics {
fn semi_variance(&self) -> QlResult<Real> {
self.regret(self.mean()?)
}
fn semi_deviation(&self) -> QlResult<Real> {
Ok(self.semi_variance()?.sqrt())
}
fn downside_variance(&self) -> QlResult<Real> {
self.regret(0.0)
}
fn downside_deviation(&self) -> QlResult<Real> {
Ok(self.downside_variance()?.sqrt())
}
fn regret(&self, target: Real) -> QlResult<Real> {
let result = self.expectation_value(
|x| {
let d = x - target;
d * d
},
|x| x < target,
);
let Some((value, n)) = result else {
fail!("samples under target <= 1: insufficient");
};
require!(n > 1, "samples under target <= 1: insufficient");
let n = n as Real;
Ok(n / (n - 1.0) * value)
}
fn potential_upside(&mut self, percentile: Real) -> QlResult<Real> {
require_var_percentile(percentile)?;
Ok(self.percentile(percentile)?.max(0.0))
}
fn value_at_risk(&mut self, percentile: Real) -> QlResult<Real> {
require_var_percentile(percentile)?;
Ok(-self.percentile(1.0 - percentile)?.min(0.0))
}
fn expected_shortfall(&mut self, percentile: Real) -> QlResult<Real> {
require_var_percentile(percentile)?;
ensure!(self.samples() != 0, "empty sample set");
let target = -self.value_at_risk(percentile)?;
let result = self.expectation_value(|x| x, |x| x < target);
let Some((value, _)) = result else {
fail!("no data below the target");
};
Ok(-value.min(0.0))
}
fn shortfall(&self, target: Real) -> QlResult<Real> {
ensure!(self.samples() != 0, "empty sample set");
let (value, _) = self
.expectation_value(|x| if x < target { 1.0 } else { 0.0 }, |_| true)
.expect("sample set is not empty");
Ok(value)
}
fn average_shortfall(&self, target: Real) -> QlResult<Real> {
let result = self.expectation_value(|x| target - x, |x| x < target);
let Some((value, _)) = result else {
fail!("no data below the target");
};
Ok(value)
}
}
impl<T: EmpiricalStatistics + ?Sized> RiskStatistics for T {}
fn require_var_percentile(percentile: Real) -> QlResult<()> {
if !(0.9..1.0).contains(&percentile) {
fail!("percentile ({percentile}) out of range [0.9, 1.0)");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::math::distributions::normal::{CumulativeNormalDistribution, NormalDistribution};
use crate::math::statistics::testutil::{AVERAGES, SIGMAS, check, sobol_normal_samples};
use crate::math::statistics::{GaussianStatistics, GeneralStatistics, Statistics};
#[test]
fn matches_quantlib_riskstats_oracle() {
for average in AVERAGES {
for sigma in SIGMAS {
let normal = NormalDistribution::new(average, sigma).unwrap();
let cumulative = CumulativeNormalDistribution::new(average, sigma).unwrap();
let data = sobol_normal_samples(average, sigma);
let mut s = GeneralStatistics::new();
s.add_sequence(data).unwrap();
let upper_tail = average + 2.0 * sigma;
let lower_tail = average - 2.0 * sigma;
let two_sigma = cumulative.value(upper_tail);
let expected = upper_tail.max(0.0);
let tolerance = if expected == 0.0 {
1e-3
} else {
(expected * 1e-3).abs()
};
check(
"potential upside",
average,
sigma,
s.potential_upside(two_sigma).unwrap(),
expected,
tolerance,
);
let expected = -lower_tail.min(0.0);
let tolerance = if expected == 0.0 {
1e-3
} else {
(expected * 1e-3).abs()
};
check(
"value-at-risk",
average,
sigma,
s.value_at_risk(two_sigma).unwrap(),
expected,
tolerance,
);
if average > 0.0 && sigma < average {
continue;
}
let expected = -(average
- sigma * sigma * normal.value(lower_tail) / (1.0 - two_sigma))
.min(0.0);
let tolerance = if expected == 0.0 {
1e-4
} else {
(expected * 1e-2).abs()
};
check(
"expected shortfall",
average,
sigma,
s.expected_shortfall(two_sigma).unwrap(),
expected,
tolerance,
);
check(
"shortfall",
average,
sigma,
s.shortfall(average).unwrap(),
0.5,
0.5e-3,
);
let expected = sigma / (2.0 * std::f64::consts::PI).sqrt() * 2.0;
check(
"average shortfall",
average,
sigma,
s.average_shortfall(average).unwrap(),
expected,
expected * 1e-3,
);
let expected = sigma * sigma;
check(
"regret",
average,
sigma,
s.regret(average).unwrap(),
expected,
expected * 1e-1,
);
let expected = s.downside_variance().unwrap();
let tolerance = if expected == 0.0 {
1e-3
} else {
(expected * 1e-3).abs()
};
check(
"gaussian downside variance vs empirical",
average,
sigma,
s.gaussian_downside_variance().unwrap(),
expected,
tolerance,
);
if average == 0.0 {
let expected = sigma * sigma;
check(
"downside variance",
average,
sigma,
s.downside_variance().unwrap(),
expected,
expected * 1e-3,
);
check(
"semi variance",
average,
sigma,
s.semi_variance().unwrap(),
expected,
expected * 1e-3,
);
}
}
}
}
#[test]
fn rejects_out_of_range_and_insufficient_data() {
let mut s = GeneralStatistics::new();
s.add_sequence([0.1, 0.2, 0.3]).unwrap();
assert!(s.potential_upside(0.5).is_err());
assert!(s.value_at_risk(1.0).is_err());
assert!(s.expected_shortfall(0.89).is_err());
assert!(s.downside_variance().is_err());
assert!(s.average_shortfall(0.05).is_err());
let mut empty = GeneralStatistics::new();
assert!(empty.shortfall(0.0).is_err());
assert!(empty.expected_shortfall(0.95).is_err());
}
#[test]
fn shortfall_measures_on_known_data() {
let mut s = GeneralStatistics::new();
s.add_sequence([-4.0, -2.0, 1.0, 5.0]).unwrap();
assert_eq!(s.shortfall(0.0).unwrap(), 0.5);
assert_eq!(s.average_shortfall(0.0).unwrap(), 3.0);
assert_eq!(s.regret(0.0).unwrap(), 20.0);
assert_eq!(s.downside_variance().unwrap(), s.regret(0.0).unwrap());
}
}