use ndarray::Array1;
use ndarray::Array2;
use ndarray::ArrayView2;
use ndarray_linalg::LeastSquaresSvd;
#[derive(Debug, Clone)]
pub struct FamaMacBethResult {
pub gamma: Array1<f64>,
pub std_errors: Array1<f64>,
pub t_statistics: Array1<f64>,
pub gamma_series: Array2<f64>,
pub betas: Array2<f64>,
}
pub fn fama_macbeth(returns: ArrayView2<f64>, factors: ArrayView2<f64>) -> FamaMacBethResult {
let (t, n) = returns.dim();
let (tt, k) = factors.dim();
assert_eq!(
t, tt,
"returns and factors must have the same number of rows"
);
assert!(t >= k + 2, "not enough observations");
let mut design_ts = Array2::<f64>::zeros((t, k + 1));
for r in 0..t {
design_ts[[r, 0]] = 1.0;
for c in 0..k {
design_ts[[r, c + 1]] = factors[[r, c]];
}
}
let mut betas = Array2::<f64>::zeros((n, k + 1));
for asset in 0..n {
let y: Array1<f64> = returns.column(asset).to_owned();
let sol = design_ts.least_squares(&y).expect("first-pass OLS failed");
for j in 0..(k + 1) {
betas[[asset, j]] = sol.solution[j];
}
}
let mut design_xs = Array2::<f64>::zeros((n, k + 1));
for asset in 0..n {
design_xs[[asset, 0]] = 1.0;
for j in 0..k {
design_xs[[asset, j + 1]] = betas[[asset, j + 1]];
}
}
let mut gamma_series = Array2::<f64>::zeros((t, k + 1));
for time in 0..t {
let y: Array1<f64> = returns.row(time).to_owned();
let sol = design_xs.least_squares(&y).expect("second-pass OLS failed");
for j in 0..(k + 1) {
gamma_series[[time, j]] = sol.solution[j];
}
}
let mut gamma_mean = Array1::<f64>::zeros(k + 1);
for j in 0..(k + 1) {
gamma_mean[j] = gamma_series.column(j).iter().sum::<f64>() / t as f64;
}
let mut std_errors = Array1::<f64>::zeros(k + 1);
for j in 0..(k + 1) {
let m = gamma_mean[j];
let var = gamma_series
.column(j)
.iter()
.map(|v| (v - m).powi(2))
.sum::<f64>()
/ (t as f64 - 1.0);
std_errors[j] = (var / t as f64).sqrt();
}
let mut t_stats = Array1::<f64>::zeros(k + 1);
for j in 0..(k + 1) {
t_stats[j] = if std_errors[j] > 0.0 {
gamma_mean[j] / std_errors[j]
} else {
0.0
};
}
FamaMacBethResult {
gamma: gamma_mean,
std_errors,
t_statistics: t_stats,
gamma_series,
betas,
}
}
#[cfg(test)]
mod tests {
use stochastic_rs_distributions::normal::SimdNormal;
use super::*;
#[test]
fn fama_macbeth_recovers_factor_premium() {
let t = 600usize;
let n = 30usize;
let k = 1usize;
let true_premium = 0.005_f64;
let true_intercept = 0.001_f64;
let factor_dist = SimdNormal::<f64>::with_seed(0.0, 0.02, 1);
let mut factors_buf = vec![0.0_f64; t];
factor_dist.fill_slice_fast(&mut factors_buf);
let factors = Array2::from_shape_vec((t, k), factors_buf.clone()).unwrap();
let beta_dist = SimdNormal::<f64>::with_seed(1.0, 0.3, 2);
let mut betas_buf = vec![0.0_f64; n];
beta_dist.fill_slice_fast(&mut betas_buf);
let resid_dist = SimdNormal::<f64>::with_seed(0.0, 0.005, 3);
let mut resid_buf = vec![0.0_f64; t * n];
resid_dist.fill_slice_fast(&mut resid_buf);
let mut returns = Array2::<f64>::zeros((t, n));
for time in 0..t {
for asset in 0..n {
let f = factors_buf[time];
let beta_i = betas_buf[asset];
let resid = resid_buf[time * n + asset];
returns[[time, asset]] = true_intercept + beta_i * (true_premium + f) + resid;
}
}
let res = fama_macbeth(returns.view(), factors.view());
assert!((res.gamma[1] - true_premium).abs() < 0.002);
assert!(res.t_statistics[1].abs() > 1.5);
assert!(res.std_errors.iter().all(|&v| v.is_finite()));
}
#[test]
fn fama_macbeth_betas_match_first_pass_ols_dimensions() {
let dist = SimdNormal::<f64>::with_seed(0.0, 0.01, 5);
let mut buf = vec![0.0_f64; 120 * 5];
dist.fill_slice_fast(&mut buf);
let returns = Array2::from_shape_vec((120, 5), buf).unwrap();
let mut fbuf = vec![0.0_f64; 120 * 2];
dist.fill_slice_fast(&mut fbuf);
let factors = Array2::from_shape_vec((120, 2), fbuf).unwrap();
let res = fama_macbeth(returns.view(), factors.view());
assert_eq!(res.betas.dim(), (5, 3));
assert_eq!(res.gamma.len(), 3);
assert_eq!(res.gamma_series.dim(), (120, 3));
}
}