use ndarray::Array1;
use ndarray::ArrayView1;
use crate::traits::FloatExt;
pub fn log_returns<T: FloatExt>(prices: ArrayView1<T>) -> Array1<T> {
assert!(prices.len() >= 2, "need at least two prices");
let n = prices.len() - 1;
let mut out = Array1::<T>::zeros(n);
for i in 0..n {
let p0 = prices[i];
let p1 = prices[i + 1];
assert!(
p0 > T::zero() && p1 > T::zero(),
"prices must be strictly positive"
);
out[i] = (p1 / p0).ln();
}
out
}
pub fn realized_variance<T: FloatExt>(returns: ArrayView1<T>) -> T {
returns.iter().fold(T::zero(), |acc, &r| acc + r * r)
}
pub fn realized_volatility<T: FloatExt>(returns: ArrayView1<T>, annualisation: T) -> T {
(annualisation * realized_variance(returns)).sqrt()
}
pub fn realized_semivariance<T: FloatExt>(returns: ArrayView1<T>, threshold: T) -> (T, T) {
let mut down = T::zero();
let mut up = T::zero();
for &r in returns.iter() {
let r2 = r * r;
if r < threshold {
down += r2;
} else if r > threshold {
up += r2;
}
}
(down, up)
}
pub fn realized_skewness<T: FloatExt>(returns: ArrayView1<T>) -> T {
let n = returns.len();
assert!(n >= 2, "need at least two returns for skewness");
let rv = realized_variance(returns);
if rv <= T::zero() {
return T::zero();
}
let cube_sum = returns.iter().fold(T::zero(), |acc, &r| acc + r * r * r);
T::from_usize_(n).sqrt() * cube_sum / rv.powf(T::from_f64_fast(1.5))
}
pub fn realized_kurtosis<T: FloatExt>(returns: ArrayView1<T>) -> T {
let n = returns.len();
assert!(n >= 2, "need at least two returns for kurtosis");
let rv = realized_variance(returns);
if rv <= T::zero() {
return T::zero();
}
let quad_sum = returns.iter().fold(T::zero(), |acc, &r| {
let r2 = r * r;
acc + r2 * r2
});
T::from_usize_(n) * quad_sum / (rv * rv)
}
pub fn realized_quarticity<T: FloatExt>(returns: ArrayView1<T>) -> T {
let n = returns.len();
let quad_sum = returns.iter().fold(T::zero(), |acc, &r| {
let r2 = r * r;
acc + r2 * r2
});
T::from_usize_(n) / T::from_f64_fast(3.0) * quad_sum
}
#[cfg(test)]
mod tests {
use ndarray::array;
use super::*;
fn approx(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() <= tol
}
#[test]
fn log_returns_matches_hand_computation() {
let p = array![100.0_f64, 101.0, 100.5, 102.0];
let r = log_returns(p.view());
assert_eq!(r.len(), 3);
assert!(approx(r[0], (101.0_f64 / 100.0).ln(), 1e-12));
assert!(approx(r[1], (100.5_f64 / 101.0).ln(), 1e-12));
assert!(approx(r[2], (102.0_f64 / 100.5).ln(), 1e-12));
}
#[test]
fn rv_constant_returns_equals_n_times_r_squared() {
let r = Array1::<f64>::from_elem(100, 0.01);
let rv = realized_variance(r.view());
assert!(approx(rv, 100.0 * 0.0001, 1e-12));
}
#[test]
fn semivariance_decomposes_rv() {
let r = array![-0.02_f64, 0.01, 0.0, 0.03, -0.005];
let (rs_minus, rs_plus) = realized_semivariance(r.view(), 0.0);
let rv = realized_variance(r.view());
assert!(approx(rs_minus + rs_plus, rv, 1e-15));
assert!(approx(
rs_minus,
0.02_f64.powi(2) + 0.005_f64.powi(2),
1e-15
));
assert!(approx(rs_plus, 0.01_f64.powi(2) + 0.03_f64.powi(2), 1e-15));
}
#[test]
fn skewness_zero_for_symmetric_sample() {
let r = array![-0.02_f64, 0.02, -0.01, 0.01];
let s = realized_skewness(r.view());
assert!(s.abs() < 1e-12);
}
#[test]
fn kurtosis_of_constant_returns_equals_one() {
let r = Array1::<f64>::from_elem(50, 0.01);
let k = realized_kurtosis(r.view());
assert!(approx(k, 1.0, 1e-9));
}
}