use crate::bar_indicators::indicator_value::IndicatorValue;
use crate::bar_indicators::utils::math::linalg::{cholesky, determinant, ols};
#[derive(Clone)]
pub struct Var {
p: usize, n_vars: usize,
data: Vec<Vec<f64>>,
coefficients: Vec<Vec<Vec<f64>>>, constants: Vec<f64>,
residuals: Vec<Vec<f64>>, fitted_values: Vec<Vec<f64>>, forecasts: Vec<f64>,
residual_covariance: Vec<Vec<f64>>,
log_likelihood: f64,
aic: f64,
bic: f64,
impulse_responses: Vec<Vec<Vec<f64>>>,
is_fitted: bool,
min_observations: usize,
}
impl Var {
pub fn new(p: usize, n_vars: usize) -> Self {
let p = p.min(8); let n_vars = n_vars.min(16); let min_obs = (p * n_vars + 10).max(30);
Self {
p,
n_vars,
data: Vec::with_capacity(512),
coefficients: Vec::with_capacity(8),
constants: Vec::with_capacity(16),
residuals: Vec::with_capacity(512),
fitted_values: Vec::with_capacity(512),
forecasts: Vec::with_capacity(16),
residual_covariance: Vec::with_capacity(16),
log_likelihood: f64::NEG_INFINITY,
aic: f64::INFINITY,
bic: f64::INFINITY,
impulse_responses: Vec::with_capacity(20),
is_fitted: false,
min_observations: min_obs,
}
}
pub fn update(&mut self, values: &[f64]) -> &[f64] {
if values.len() != self.n_vars {
return &[];
}
let mut data_row: Vec<f64> = Vec::with_capacity(self.n_vars);
for &val in values.iter().take(self.n_vars) {
data_row.push(val);
}
if self.data.len() >= 512 {
self.data.remove(0);
}
self.data.push(data_row);
if self.data.len() >= self.min_observations {
self.fit_model();
self.generate_forecasts();
}
&self.forecasts
}
fn fit_model(&mut self) {
if self.data.len() < self.min_observations {
return;
}
self.initialize_coefficient_structures();
self.estimate_coefficients();
self.calculate_residuals_and_fitted();
self.calculate_residual_covariance();
self.calculate_model_metrics();
self.calculate_impulse_responses();
self.is_fitted = true;
}
fn initialize_coefficient_structures(&mut self) {
self.coefficients.clear();
self.constants.clear();
for _ in 0..self.p {
let mut lag_coeffs: Vec<Vec<f64>> = Vec::with_capacity(self.n_vars);
for _ in 0..self.n_vars {
lag_coeffs.push(vec![0.0; self.n_vars]);
}
self.coefficients.push(lag_coeffs);
}
for _ in 0..self.n_vars {
self.constants.push(0.0);
}
}
fn estimate_coefficients(&mut self) {
let n_obs = self.data.len();
let start_idx = self.p;
if n_obs <= start_idx {
return;
}
for eq_idx in 0..self.n_vars {
self.estimate_single_equation(eq_idx, start_idx);
}
}
fn estimate_single_equation(&mut self, eq_idx: usize, start_idx: usize) {
let n_obs = self.data.len() - start_idx;
let n_regressors = self.n_vars * self.p + 1;
let mut x_matrix = vec![vec![0.0; n_regressors]; n_obs];
let mut y_vector = vec![0.0; n_obs];
for t in 0..n_obs {
let data_idx = start_idx + t;
y_vector[t] = self.data[data_idx][eq_idx];
x_matrix[t][0] = 1.0;
let mut regressor_idx = 1;
for lag in 1..=self.p {
for var_idx in 0..self.n_vars {
if data_idx >= lag {
x_matrix[t][regressor_idx] = self.data[data_idx - lag][var_idx];
}
regressor_idx += 1;
}
}
}
let coeffs = self.solve_ols(&x_matrix, &y_vector);
if !coeffs.is_empty() {
if eq_idx < self.constants.len() {
self.constants[eq_idx] = coeffs[0];
}
let mut coeff_idx = 1;
for lag in 0..self.p {
for var_idx in 0..self.n_vars {
if lag < self.coefficients.len() &&
var_idx < self.coefficients[lag].len() &&
eq_idx < self.coefficients[lag][var_idx].len() &&
coeff_idx < coeffs.len() {
self.coefficients[lag][var_idx][eq_idx] = coeffs[coeff_idx];
}
coeff_idx += 1;
}
}
}
}
fn solve_ols(&self, x_matrix: &[Vec<f64>], y_vector: &[f64]) -> Vec<f64> {
if x_matrix.is_empty() || y_vector.is_empty() {
return Vec::new();
}
let rows = x_matrix.len();
let k = x_matrix[0].len();
let mut flat = Vec::with_capacity(rows * k);
for row in &x_matrix[..rows] {
flat.extend_from_slice(&row[..k]);
}
ols(&flat, &y_vector[..rows], rows, k).unwrap_or_else(|| vec![0.0; k])
}
fn calculate_residuals_and_fitted(&mut self) {
self.residuals.clear();
self.fitted_values.clear();
let start_idx = self.p;
if self.data.len() <= start_idx {
return;
}
for t in start_idx..self.data.len() {
let mut fitted_row: Vec<f64> = Vec::with_capacity(self.n_vars);
let mut residual_row: Vec<f64> = Vec::with_capacity(self.n_vars);
for var_idx in 0..self.n_vars {
let mut fitted_value = if var_idx < self.constants.len() {
self.constants[var_idx]
} else {
0.0
};
for lag in 0..self.p {
for lag_var in 0..self.n_vars {
if t > lag &&
lag < self.coefficients.len() &&
lag_var < self.coefficients[lag].len() &&
var_idx < self.coefficients[lag][lag_var].len() {
let coeff = self.coefficients[lag][lag_var][var_idx];
fitted_value += coeff * self.data[t - 1 - lag][lag_var];
}
}
}
let actual_value = self.data[t][var_idx];
let residual = actual_value - fitted_value;
fitted_row.push(fitted_value);
residual_row.push(residual);
}
self.fitted_values.push(fitted_row);
self.residuals.push(residual_row);
}
}
fn calculate_residual_covariance(&mut self) {
self.residual_covariance.clear();
if self.residuals.is_empty() {
return;
}
let n_obs = self.residuals.len() as f64;
for i in 0..self.n_vars {
let mut cov_row: Vec<f64> = Vec::with_capacity(self.n_vars);
for j in 0..self.n_vars {
let mut covariance = 0.0;
for residual_row in &self.residuals {
if i < residual_row.len() && j < residual_row.len() {
covariance += residual_row[i] * residual_row[j];
}
}
covariance /= n_obs;
cov_row.push(covariance);
}
self.residual_covariance.push(cov_row);
}
}
fn calculate_model_metrics(&mut self) {
if self.residuals.is_empty() || self.residual_covariance.is_empty() {
return;
}
let n_obs = self.residuals.len() as f64;
let n_params = (self.n_vars * self.n_vars * self.p + self.n_vars) as f64;
let det_sigma = self.calculate_determinant(&self.residual_covariance);
if det_sigma > 0.0 {
self.log_likelihood = -0.5 * n_obs * (
self.n_vars as f64 * (2.0 * std::f64::consts::PI).ln() +
det_sigma.ln() +
self.n_vars as f64
);
}
self.aic = -2.0 * self.log_likelihood + 2.0 * n_params;
self.bic = -2.0 * self.log_likelihood + n_params * n_obs.ln();
}
fn calculate_determinant(&self, matrix: &[Vec<f64>]) -> f64 {
let n = self.n_vars;
if matrix.len() < n {
return 0.0;
}
let mut flat = Vec::with_capacity(n * n);
for row in matrix.iter().take(n) {
if row.len() < n {
return 0.0;
}
flat.extend_from_slice(&row[..n]);
}
determinant(&flat, n).unwrap_or(0.0)
}
fn calculate_impulse_responses(&mut self) {
self.impulse_responses.clear();
let n = self.n_vars;
let max_horizon = 20;
if n == 0 || self.coefficients.is_empty() {
return;
}
let a: Vec<Vec<f64>> = (0..self.p)
.map(|lag| {
let mut m = vec![0.0; n * n];
for from in 0..n {
for to in 0..n {
m[to * n + from] = self.coefficients[lag][from][to];
}
}
m
})
.collect();
let sigma_flat: Vec<f64> = {
let mut s = vec![0.0; n * n];
for i in 0..n {
for j in 0..n {
s[i * n + j] = self.residual_covariance[i][j];
}
}
s
};
let p_chol = cholesky(&sigma_flat, n).unwrap_or_else(|| {
let mut d = vec![0.0; n * n];
for i in 0..n {
d[i * n + i] = self.residual_covariance[i][i].max(0.0).sqrt();
}
d
});
let mut psi: Vec<Vec<f64>> = Vec::with_capacity(max_horizon);
let mut psi0 = vec![0.0; n * n];
for i in 0..n {
psi0[i * n + i] = 1.0;
}
psi.push(psi0);
for h in 1..max_horizon {
let mut psi_h = vec![0.0; n * n];
for i in 1..=self.p.min(h) {
let ai = &a[i - 1];
let prev = &psi[h - i];
for r in 0..n {
for c in 0..n {
let mut acc = 0.0;
for k in 0..n {
acc += ai[r * n + k] * prev[k * n + c];
}
psi_h[r * n + c] += acc;
}
}
}
psi.push(psi_h);
}
for psi_h in psi.iter().take(max_horizon) {
let mut theta = vec![0.0; n * n];
for r in 0..n {
for c in 0..n {
let mut acc = 0.0;
for k in 0..n {
acc += psi_h[r * n + k] * p_chol[k * n + c];
}
theta[r * n + c] = acc;
}
}
let mut horizon: Vec<Vec<f64>> = Vec::with_capacity(n);
for shock in 0..n {
let mut row = Vec::with_capacity(n);
for response in 0..n {
row.push(theta[response * n + shock]);
}
horizon.push(row);
}
self.impulse_responses.push(horizon);
}
}
fn generate_forecasts(&mut self) {
self.forecasts.clear();
if !self.is_fitted || self.data.is_empty() {
return;
}
for var_idx in 0..self.n_vars {
let mut forecast = if var_idx < self.constants.len() {
self.constants[var_idx]
} else {
0.0
};
for lag in 0..self.p {
for lag_var in 0..self.n_vars {
if self.data.len() > lag &&
lag < self.coefficients.len() &&
lag_var < self.coefficients[lag].len() &&
var_idx < self.coefficients[lag][lag_var].len() {
let data_idx = self.data.len() - 1 - lag;
let coeff = self.coefficients[lag][lag_var][var_idx];
forecast += coeff * self.data[data_idx][lag_var];
}
}
}
self.forecasts.push(forecast);
}
}
pub fn forecasts(&self) -> &[f64] {
&self.forecasts
}
pub fn get_coefficients(&self) -> &[Vec<Vec<f64>>] {
&self.coefficients
}
pub fn residual_covariance(&self) -> &[Vec<f64>] {
&self.residual_covariance
}
pub fn impulse_responses(&self) -> &[Vec<Vec<f64>>] {
&self.impulse_responses
}
pub fn get_metrics(&self) -> (f64, f64, f64) {
(self.log_likelihood, self.aic, self.bic)
}
pub fn get_parameters(&self) -> (usize, usize) {
(self.p, self.n_vars)
}
pub fn is_fitted(&self) -> bool {
self.is_fitted
}
pub fn reset(&mut self) {
self.data.clear();
self.coefficients.clear();
self.constants.clear();
self.residuals.clear();
self.fitted_values.clear();
self.forecasts.clear();
self.residual_covariance.clear();
self.impulse_responses.clear();
self.log_likelihood = f64::NEG_INFINITY;
self.aic = f64::INFINITY;
self.bic = f64::INFINITY;
self.is_fitted = false;
}
#[inline]
pub fn is_ready(&self) -> bool {
self.is_fitted && self.data.len() >= self.min_observations
}
pub fn value(&self) -> IndicatorValue {
if !self.forecasts.is_empty() {
IndicatorValue::Single(self.forecasts[0])
} else {
IndicatorValue::Single(0.0)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_var_creation() {
let ind = Var::new(1, 2);
assert!(!ind.is_ready());
assert!(ind.forecasts().is_empty());
}
#[test]
fn test_var_warmup() {
let mut ind = Var::new(1, 2);
for i in 0..50 {
let values = [
100.0 + (i as f64 * 0.1).sin() * 5.0,
50.0 + (i as f64 * 0.15).cos() * 3.0,
];
ind.update(&values);
}
assert!(ind.is_ready());
}
#[test]
fn test_var_forecasts_finite() {
let mut ind = Var::new(1, 2);
for i in 0..50 {
let values = [100.0 + i as f64 * 0.5, 50.0 + i as f64 * 0.3];
ind.update(&values);
}
for &forecast in ind.forecasts() {
assert!(forecast.is_finite());
}
}
#[test]
fn test_var_reset() {
let mut ind = Var::new(1, 2);
for i in 0..50 {
let values = [100.0 + i as f64, 50.0 + i as f64 * 0.5];
ind.update(&values);
}
ind.reset();
assert!(!ind.is_ready());
assert!(ind.forecasts().is_empty());
}
#[test]
fn test_var_wrong_dimensions() {
let mut ind = Var::new(1, 3);
let result = ind.update(&[100.0, 50.0]);
assert!(result.is_empty());
}
fn lcg(n: usize, seed: u64) -> Vec<f64> {
let mut s = seed;
let mut out = Vec::with_capacity(n);
for _ in 0..n {
s = s
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
out.push(((s >> 33) as f64) / (1u64 << 31) as f64 - 1.0);
}
out
}
#[test]
fn ols_recovers_cross_equation_coefficient() {
let e1 = lcg(800, 31);
let e2 = lcg(800, 67);
let mut y1 = vec![0.0_f64; e1.len()];
let mut y2 = vec![0.0_f64; e2.len()];
for t in 1..e1.len() {
y1[t] = 0.5 * y1[t - 1] + e1[t];
y2[t] = 0.4 * y1[t - 1] + 0.3 * y2[t - 1] + e2[t];
}
let mut v = Var::new(1, 2);
for t in 0..y1.len() {
v.update(&[y1[t], y2[t]]);
}
let c = v.get_coefficients();
assert_eq!(c.len(), 1);
assert!((c[0][0][0] - 0.5).abs() < 0.1, "φ₁₁ {} ≈ 0.5", c[0][0][0]);
assert!((c[0][0][1] - 0.4).abs() < 0.1, "cross {} ≈ 0.4", c[0][0][1]);
assert!((c[0][1][1] - 0.3).abs() < 0.1, "φ₂₂ {} ≈ 0.3", c[0][1][1]);
assert!(c[0][1][0].abs() < 0.1, "no feedback {} ≈ 0", c[0][1][0]);
}
#[test]
fn impulse_response_impact_is_cholesky() {
let e1 = lcg(600, 5);
let e2 = lcg(600, 9);
let mut y1 = vec![0.0_f64; e1.len()];
let mut y2 = vec![0.0_f64; e2.len()];
for t in 1..e1.len() {
y1[t] = 0.3 * y1[t - 1] + e1[t];
y2[t] = 0.2 * y2[t - 1] + e2[t] + 0.5 * e1[t];
}
let mut v = Var::new(1, 2);
for t in 0..y1.len() {
v.update(&[y1[t], y2[t]]);
}
let irf = v.impulse_responses();
assert!(!irf.is_empty());
let shock1_response0 = irf[0][1][0];
assert!(
shock1_response0.abs() < 1e-9,
"impact of shock-to-1 on var-0 must be 0 (lower-tri), got {shock1_response0}"
);
assert!(irf[0][0][0] > 0.0, "own impact must be positive");
}
}