use crate::error::{IntegrateError, IntegrateResult as Result};
use std::f64::consts::PI;
#[derive(Debug, Clone)]
pub struct GarchModel {
pub omega: f64,
pub alpha: f64,
pub beta: f64,
conditional_variances: Vec<f64>,
fitted: bool,
}
impl GarchModel {
pub fn new() -> Self {
Self {
omega: 0.0,
alpha: 0.0,
beta: 0.0,
conditional_variances: Vec::new(),
fitted: false,
}
}
pub fn with_parameters(omega: f64, alpha: f64, beta: f64) -> Result<Self> {
Self::validate_parameters(omega, alpha, beta)?;
Ok(Self {
omega,
alpha,
beta,
conditional_variances: Vec::new(),
fitted: false,
})
}
pub fn fit(&mut self, returns: &[f64]) -> Result<()> {
if returns.len() < 10 {
return Err(IntegrateError::ValueError(
"At least 10 observations required for GARCH estimation".to_string(),
));
}
let initial_params = vec![
0.00001, 0.05, 0.90, ];
let optimized = self.quasi_mle_optimization(returns, initial_params)?;
self.omega = optimized[0];
self.alpha = optimized[1];
self.beta = optimized[2];
self.conditional_variances = self.compute_conditional_variances(returns)?;
self.fitted = true;
Ok(())
}
pub fn forecast(&self, steps: usize) -> Result<Vec<f64>> {
if !self.fitted {
return Err(IntegrateError::ValueError(
"Model must be fitted before forecasting".to_string(),
));
}
let mut forecasts = Vec::with_capacity(steps);
let last_variance = self.conditional_variances.last().copied().ok_or_else(|| {
IntegrateError::ValueError("No conditional variances available".to_string())
})?;
let mut current_var = last_variance;
for _ in 0..steps {
current_var = self.omega + (self.alpha + self.beta) * current_var;
forecasts.push(current_var.sqrt()); }
Ok(forecasts)
}
pub fn unconditional_volatility(&self) -> Result<f64> {
if !self.fitted {
return Err(IntegrateError::ValueError("Model not fitted".to_string()));
}
let long_run_var = self.omega / (1.0 - self.alpha - self.beta);
Ok(long_run_var.sqrt())
}
fn compute_conditional_variances(&self, returns: &[f64]) -> Result<Vec<f64>> {
let n = returns.len();
let mut variances = Vec::with_capacity(n);
let sample_var: f64 = returns.iter().map(|r| r * r).sum::<f64>() / n as f64;
variances.push(sample_var);
for i in 1..n {
let epsilon_sq = returns[i - 1] * returns[i - 1];
let var_t = self.omega + self.alpha * epsilon_sq + self.beta * variances[i - 1];
variances.push(var_t.max(1e-10)); }
Ok(variances)
}
fn quasi_mle_optimization(&self, returns: &[f64], initial: Vec<f64>) -> Result<Vec<f64>> {
let objective = |params: &[f64]| -> f64 {
if Self::validate_parameters(params[0], params[1], params[2]).is_err() {
return 1e10; }
self.negative_log_likelihood(returns, params[0], params[1], params[2])
.unwrap_or(1e10)
};
let mut best_params = initial.clone();
let mut best_value = objective(&best_params);
for _ in 0..50 {
let mut trial = best_params.clone();
trial[0] *= 1.0 + (scirs2_core::random::random::<f64>() - 0.5) * 0.2; trial[1] *= 1.0 + (scirs2_core::random::random::<f64>() - 0.5) * 0.2; trial[2] *= 1.0 + (scirs2_core::random::random::<f64>() - 0.5) * 0.2;
trial[0] = trial[0].max(1e-6).min(0.01);
trial[1] = trial[1].max(0.01).min(0.30);
trial[2] = trial[2].max(0.50).min(0.95);
if trial[1] + trial[2] < 0.9999 {
let value = objective(&trial);
if value < best_value {
best_params = trial;
best_value = value;
}
}
}
Ok(best_params)
}
fn negative_log_likelihood(
&self,
returns: &[f64],
omega: f64,
alpha: f64,
beta: f64,
) -> Result<f64> {
let n = returns.len();
let mut log_likelihood = 0.0;
let sample_var: f64 = returns.iter().map(|r| r * r).sum::<f64>() / n as f64;
let mut var_t = sample_var;
for &ret in returns {
let epsilon_sq = ret * ret;
var_t = omega + alpha * epsilon_sq + beta * var_t;
var_t = var_t.max(1e-10);
log_likelihood += 0.5 * ((2.0 * PI * var_t).ln() + epsilon_sq / var_t);
}
Ok(log_likelihood)
}
fn validate_parameters(omega: f64, alpha: f64, beta: f64) -> Result<()> {
if omega <= 0.0 {
return Err(IntegrateError::ValueError(
"Omega must be positive".to_string(),
));
}
if !(0.0..=1.0).contains(&alpha) {
return Err(IntegrateError::ValueError(
"Alpha must be in [0, 1]".to_string(),
));
}
if !(0.0..=1.0).contains(&beta) {
return Err(IntegrateError::ValueError(
"Beta must be in [0, 1]".to_string(),
));
}
if alpha + beta >= 1.0 {
return Err(IntegrateError::ValueError(
"Alpha + Beta must be < 1 for stationarity".to_string(),
));
}
Ok(())
}
}
impl Default for GarchModel {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct EWMAModel {
lambda: f64,
}
impl EWMAModel {
pub fn new(lambda: f64) -> Self {
assert!(lambda > 0.0 && lambda < 1.0, "Lambda must be in (0, 1)");
Self { lambda }
}
pub fn riskmetrics_daily() -> Self {
Self::new(0.94)
}
pub fn riskmetrics_monthly() -> Self {
Self::new(0.97)
}
pub fn estimate(&self, returns: &[f64]) -> Result<f64> {
if returns.is_empty() {
return Err(IntegrateError::ValueError(
"Returns array is empty".to_string(),
));
}
let mut variance = returns[0] * returns[0];
for &ret in &returns[1..] {
variance = self.lambda * variance + (1.0 - self.lambda) * ret * ret;
}
Ok(variance.sqrt())
}
pub fn compute_series(&self, returns: &[f64]) -> Result<Vec<f64>> {
if returns.is_empty() {
return Err(IntegrateError::ValueError(
"Returns array is empty".to_string(),
));
}
let mut volatilities = Vec::with_capacity(returns.len());
let mut variance = returns[0] * returns[0];
volatilities.push(variance.sqrt());
for &ret in &returns[1..] {
variance = self.lambda * variance + (1.0 - self.lambda) * ret * ret;
volatilities.push(variance.sqrt());
}
Ok(volatilities)
}
pub fn forecast(&self, current_vol: f64, steps: usize) -> Vec<f64> {
let mut variance = current_vol * current_vol;
let mut forecasts = Vec::with_capacity(steps);
for _ in 0..steps {
variance *= self.lambda;
forecasts.push(variance.sqrt());
}
forecasts
}
}
pub struct HistoricalVolatility;
impl HistoricalVolatility {
pub fn simple(returns: &[f64]) -> Result<f64> {
if returns.is_empty() {
return Err(IntegrateError::ValueError(
"Returns array is empty".to_string(),
));
}
let mean: f64 = returns.iter().sum::<f64>() / returns.len() as f64;
let variance: f64 =
returns.iter().map(|r| (r - mean).powi(2)).sum::<f64>() / returns.len() as f64;
Ok(variance.sqrt())
}
pub fn realized(returns: &[f64]) -> Result<f64> {
if returns.is_empty() {
return Err(IntegrateError::ValueError(
"Returns array is empty".to_string(),
));
}
let sum_sq: f64 = returns.iter().map(|r| r * r).sum();
Ok((sum_sq / returns.len() as f64).sqrt())
}
pub fn parkinson(high_prices: &[f64], low_prices: &[f64]) -> Result<f64> {
if high_prices.len() != low_prices.len() {
return Err(IntegrateError::ValueError(
"High and low price arrays must have same length".to_string(),
));
}
if high_prices.is_empty() {
return Err(IntegrateError::ValueError(
"Price arrays are empty".to_string(),
));
}
let n = high_prices.len() as f64;
let sum: f64 = high_prices
.iter()
.zip(low_prices.iter())
.map(|(&h, &l)| {
let ratio = h / l;
ratio.ln().powi(2)
})
.sum();
let constant = 1.0 / (4.0 * 2_f64.ln());
Ok((constant * sum / n).sqrt())
}
pub fn annualize(vol: f64, periods_per_year: f64) -> f64 {
vol * periods_per_year.sqrt()
}
}
pub trait VolatilityForecaster {
fn fit(&mut self, returns: &[f64]) -> Result<()>;
fn forecast(&self, steps: usize) -> Result<Vec<f64>>;
fn current_volatility(&self) -> Result<f64>;
}
impl VolatilityForecaster for GarchModel {
fn fit(&mut self, returns: &[f64]) -> Result<()> {
GarchModel::fit(self, returns)
}
fn forecast(&self, steps: usize) -> Result<Vec<f64>> {
GarchModel::forecast(self, steps)
}
fn current_volatility(&self) -> Result<f64> {
if !self.fitted {
return Err(IntegrateError::ValueError("Model not fitted".to_string()));
}
self.conditional_variances
.last()
.map(|v| v.sqrt())
.ok_or_else(|| IntegrateError::ValueError("No variances available".to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn generate_test_returns() -> Vec<f64> {
vec![
0.01, -0.02, 0.015, -0.01, 0.005, 0.02, -0.015, 0.01, -0.005, 0.012, -0.018, 0.025,
-0.012, 0.008, -0.022, 0.014, -0.009, 0.016, -0.013, 0.011,
]
}
#[test]
fn test_garch_model_creation() {
let garch = GarchModel::new();
assert!(!garch.fitted);
assert_eq!(garch.omega, 0.0);
assert_eq!(garch.alpha, 0.0);
assert_eq!(garch.beta, 0.0);
}
#[test]
fn test_garch_parameter_validation() {
assert!(GarchModel::with_parameters(0.00001, 0.05, 0.90).is_ok());
assert!(GarchModel::with_parameters(0.0, 0.05, 0.90).is_err());
assert!(GarchModel::with_parameters(0.00001, 0.50, 0.50).is_err());
assert!(GarchModel::with_parameters(0.00001, -0.05, 0.90).is_err());
}
#[test]
fn test_garch_fit() {
let returns = generate_test_returns();
let mut garch = GarchModel::new();
let result = garch.fit(&returns);
assert!(result.is_ok());
assert!(garch.fitted);
assert!(garch.omega > 0.0);
assert!(garch.alpha >= 0.0 && garch.alpha < 1.0);
assert!(garch.beta >= 0.0 && garch.beta < 1.0);
assert!(garch.alpha + garch.beta < 1.0);
assert_eq!(garch.conditional_variances.len(), returns.len());
}
#[test]
fn test_garch_forecast() {
let returns = generate_test_returns();
let mut garch = GarchModel::new();
garch.fit(&returns).expect("Operation failed");
let forecast = garch.forecast(5).expect("Operation failed");
assert_eq!(forecast.len(), 5);
assert!(forecast.iter().all(|&v| v > 0.0));
let long_run = garch.unconditional_volatility().expect("Operation failed");
assert!(forecast[4] > forecast[0] * 0.5); }
#[test]
fn test_ewma_basic() {
let ewma = EWMAModel::new(0.94);
assert_eq!(ewma.lambda, 0.94);
let returns = generate_test_returns();
let vol = ewma.estimate(&returns).expect("Operation failed");
assert!(vol > 0.0);
assert!(vol < 1.0); }
#[test]
fn test_ewma_riskmetrics() {
let daily = EWMAModel::riskmetrics_daily();
assert_eq!(daily.lambda, 0.94);
let monthly = EWMAModel::riskmetrics_monthly();
assert_eq!(monthly.lambda, 0.97);
}
#[test]
fn test_ewma_series() {
let ewma = EWMAModel::new(0.94);
let returns = generate_test_returns();
let series = ewma.compute_series(&returns).expect("Operation failed");
assert_eq!(series.len(), returns.len());
assert!(series.iter().all(|&v| v > 0.0));
}
#[test]
fn test_ewma_forecast() {
let ewma = EWMAModel::new(0.94);
let current_vol = 0.15;
let forecast = ewma.forecast(current_vol, 5);
assert_eq!(forecast.len(), 5);
assert!(forecast[0] < current_vol);
assert!(forecast[4] < forecast[0]);
}
#[test]
fn test_historical_simple() {
let returns = generate_test_returns();
let vol = HistoricalVolatility::simple(&returns).expect("Operation failed");
assert!(vol > 0.0);
assert!(vol < 0.5); }
#[test]
fn test_historical_realized() {
let returns = generate_test_returns();
let vol = HistoricalVolatility::realized(&returns).expect("Operation failed");
assert!(vol > 0.0);
assert!(vol < 0.5);
}
#[test]
fn test_parkinson_estimator() {
let highs = vec![101.0, 102.5, 103.0, 102.0, 104.0];
let lows = vec![99.0, 100.5, 101.0, 100.0, 102.0];
let vol = HistoricalVolatility::parkinson(&highs, &lows).expect("Operation failed");
assert!(vol > 0.0);
}
#[test]
fn test_annualization() {
let daily_vol = 0.01; let annual_vol = HistoricalVolatility::annualize(daily_vol, 252.0);
assert!((annual_vol - 0.1587).abs() < 0.001);
}
#[test]
fn test_garch_insufficient_data() {
let short_returns = vec![0.01, 0.02]; let mut garch = GarchModel::new();
assert!(garch.fit(&short_returns).is_err());
}
#[test]
fn test_ewma_empty_returns() {
let ewma = EWMAModel::new(0.94);
let empty: Vec<f64> = vec![];
assert!(ewma.estimate(&empty).is_err());
}
}