use crate::align::align;
use crate::ops::stat::{argsort_stable as argsort, average_ranks, mean_std as mean_std_ddof};
use crate::panel::Panel;
use serde::Serialize;
pub fn forward_returns(prices: &Panel, horizon: usize) -> Panel {
let (nrows, ncols) = prices.data.dim();
let mut data = ndarray::Array2::from_elem((nrows, ncols), f64::NAN);
if horizon > 0 && nrows > horizon {
for r in 0..nrows - horizon {
for c in 0..ncols {
let p0 = prices.data[[r, c]];
let p1 = prices.data[[r + horizon, c]];
if p0.is_finite() && p1.is_finite() && p0 != 0.0 {
data[[r, c]] = p1 / p0 - 1.0;
}
}
}
}
Panel {
dates: prices.dates.clone(),
symbols: prices.symbols.clone(),
data,
}
}
pub fn daily_returns(prices: &Panel) -> Panel {
let (nrows, ncols) = prices.data.dim();
let mut data = ndarray::Array2::from_elem((nrows, ncols), f64::NAN);
for r in 1..nrows {
for c in 0..ncols {
let p0 = prices.data[[r - 1, c]];
let p1 = prices.data[[r, c]];
if p0.is_finite() && p1.is_finite() && p0 != 0.0 {
data[[r, c]] = p1 / p0 - 1.0;
}
}
}
Panel {
dates: prices.dates.clone(),
symbols: prices.symbols.clone(),
data,
}
}
#[derive(Debug, Serialize)]
pub struct FactorReport {
pub dates: Vec<i32>,
pub ic: Vec<f64>,
pub mean_ic: f64,
pub ic_std: f64,
pub icir: f64,
pub quantiles: usize,
pub quantile_returns: Vec<f64>,
pub long_short: f64,
pub top_quantile_turnover: f64,
}
pub fn factor_report(factor: &Panel, forward_return: &Panel, quantiles: usize) -> FactorReport {
let q = quantiles.max(1);
let (fa, fr) = align(factor, forward_return);
let fret = fr.project_onto(&fa.dates, &fa.symbols);
let (nrows, ncols) = fa.data.dim();
let mut dates = Vec::new();
let mut ic = Vec::new();
let mut q_sum = vec![0.0_f64; q];
let mut q_cnt = vec![0usize; q];
let mut prev_top: Option<Vec<usize>> = None;
let mut turnover_sum = 0.0_f64;
let mut turnover_cnt = 0usize;
for r in 0..nrows {
let valid: Vec<usize> = (0..ncols)
.filter(|&c| fa.data[[r, c]].is_finite() && fret[[r, c]].is_finite())
.collect();
if valid.len() < 2 {
prev_top = None; continue;
}
let fvals: Vec<f64> = valid.iter().map(|&c| fa.data[[r, c]]).collect();
let rvals: Vec<f64> = valid.iter().map(|&c| fret[[r, c]]).collect();
dates.push(fa.dates[r]);
ic.push(spearman(&fvals, &rvals));
let order = argsort(&fvals); let m = valid.len();
let mut top_cols = Vec::new();
for (rank0, &vi) in order.iter().enumerate() {
let b = ((rank0 * q) / m).min(q - 1);
q_sum[b] += rvals[vi];
q_cnt[b] += 1;
if b == q - 1 {
top_cols.push(valid[vi]);
}
}
if let Some(prev) = &prev_top {
if !prev.is_empty() {
let left = prev.iter().filter(|c| !top_cols.contains(c)).count();
turnover_sum += left as f64 / prev.len() as f64;
turnover_cnt += 1;
}
}
prev_top = Some(top_cols);
}
let quantile_returns: Vec<f64> = (0..q)
.map(|b| {
if q_cnt[b] == 0 {
f64::NAN
} else {
q_sum[b] / q_cnt[b] as f64
}
})
.collect();
let long_short = match (quantile_returns.first(), quantile_returns.last()) {
(Some(&lo), Some(&hi)) => hi - lo,
_ => f64::NAN,
};
let (mean_ic, ic_std) = mean_std(&ic);
let icir = if ic_std > 0.0 {
mean_ic / ic_std
} else {
f64::NAN
};
let top_quantile_turnover = if turnover_cnt == 0 {
f64::NAN
} else {
turnover_sum / turnover_cnt as f64
};
FactorReport {
dates,
ic,
mean_ic,
ic_std,
icir,
quantiles: q,
quantile_returns,
long_short,
top_quantile_turnover,
}
}
#[derive(Debug, Serialize)]
pub struct EventStudy {
pub pre: usize,
pub post: usize,
pub lags: Vec<i64>,
pub avg_return: Vec<f64>,
pub cumulative: Vec<f64>,
pub event_count: usize,
}
pub fn event_study(events: &Panel, returns: &Panel, pre: usize, post: usize) -> EventStudy {
let (ev, rt) = align(events, returns);
let ret = rt.project_onto(&ev.dates, &ev.symbols);
let (nrows, ncols) = ev.data.dim();
let width = pre + post + 1;
let mut sums = vec![0.0_f64; width];
let mut counts = vec![0usize; width];
let mut event_count = 0usize;
for r in 0..nrows {
for c in 0..ncols {
if !crate::panel::is_true(ev.data[[r, c]]) {
continue;
}
if ret[[r, c]].is_finite() {
event_count += 1;
}
for (k, slot) in (0..width).enumerate() {
let lag = k as i64 - pre as i64;
let rr = r as i64 + lag;
if rr < 0 || rr as usize >= nrows {
continue;
}
let v = ret[[rr as usize, c]];
if v.is_finite() {
sums[slot] += v;
counts[slot] += 1;
}
}
}
}
let lags: Vec<i64> = (0..width).map(|k| k as i64 - pre as i64).collect();
let avg_return: Vec<f64> = (0..width)
.map(|k| {
if counts[k] == 0 {
f64::NAN
} else {
sums[k] / counts[k] as f64
}
})
.collect();
let mut cumulative = vec![f64::NAN; width];
let mut acc = 0.0;
for k in 0..width {
if avg_return[k].is_finite() {
acc += avg_return[k];
cumulative[k] = acc;
} else {
cumulative[k] = f64::NAN;
}
}
EventStudy {
pre,
post,
lags,
avg_return,
cumulative,
event_count,
}
}
fn spearman(a: &[f64], b: &[f64]) -> f64 {
let ra = average_ranks(a);
let rb = average_ranks(b);
pearson(&ra, &rb)
}
fn pearson(a: &[f64], b: &[f64]) -> f64 {
let n = a.len() as f64;
let ma = a.iter().sum::<f64>() / n;
let mb = b.iter().sum::<f64>() / n;
let mut cov = 0.0;
let mut va = 0.0;
let mut vb = 0.0;
for (x, y) in a.iter().zip(b) {
cov += (x - ma) * (y - mb);
va += (x - ma).powi(2);
vb += (y - mb).powi(2);
}
if va == 0.0 || vb == 0.0 {
return 0.0;
}
cov / (va.sqrt() * vb.sqrt())
}
#[inline]
fn mean_std(xs: &[f64]) -> (f64, f64) {
mean_std_ddof(xs, 1)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::panel::Panel;
fn panel(dates: Vec<i32>, syms: Vec<&str>, rows: Vec<Vec<f64>>) -> Panel {
Panel::from_rows(dates, syms.into_iter().map(String::from).collect(), rows).unwrap()
}
#[test]
fn forward_returns_look_ahead_by_horizon() {
let px = panel(
vec![1, 2, 3, 4],
vec!["A"],
vec![vec![10.0], vec![11.0], vec![12.0], vec![13.0]],
);
let f = forward_returns(&px, 1);
assert!((f.data[[0, 0]] - 0.1).abs() < 1e-12); assert!((f.data[[2, 0]] - (13.0 / 12.0 - 1.0)).abs() < 1e-12);
assert!(f.data[[3, 0]].is_nan()); }
#[test]
fn daily_returns_are_backward_one_day() {
let px = panel(
vec![1, 2, 3],
vec!["A"],
vec![vec![10.0], vec![11.0], vec![12.0]],
);
let r = daily_returns(&px);
assert!(r.data[[0, 0]].is_nan()); assert!((r.data[[1, 0]] - 0.1).abs() < 1e-12); assert!((r.data[[2, 0]] - (12.0 / 11.0 - 1.0)).abs() < 1e-12);
}
#[test]
fn factor_ic_is_perfect_when_factor_orders_returns() {
let factor = panel(
vec![1, 2],
vec!["A", "B", "C", "D"],
vec![vec![1.0, 2.0, 3.0, 4.0], vec![4.0, 3.0, 2.0, 1.0]],
);
let fret = panel(
vec![1, 2],
vec!["A", "B", "C", "D"],
vec![vec![0.1, 0.2, 0.3, 0.4], vec![0.4, 0.3, 0.2, 0.1]],
);
let rep = factor_report(&factor, &fret, 2);
assert_eq!(rep.ic.len(), 2);
assert!((rep.mean_ic - 1.0).abs() < 1e-12);
assert!(rep.long_short > 0.0);
assert!((rep.quantile_returns[1] - 0.35).abs() < 1e-12);
assert!((rep.quantile_returns[0] - 0.15).abs() < 1e-12);
assert!((rep.long_short - 0.2).abs() < 1e-12);
}
#[test]
fn factor_ic_negative_when_factor_inverts_returns() {
let factor = panel(vec![1], vec!["A", "B", "C"], vec![vec![1.0, 2.0, 3.0]]);
let fret = panel(vec![1], vec!["A", "B", "C"], vec![vec![0.3, 0.2, 0.1]]);
let rep = factor_report(&factor, &fret, 3);
assert!((rep.mean_ic - (-1.0)).abs() < 1e-12);
assert!(rep.ic_std.is_nan());
assert!(rep.icir.is_nan());
}
#[test]
fn factor_top_quantile_turnover_tracks_membership() {
let factor = panel(
vec![1, 2, 3],
vec!["A", "B", "C", "D"],
vec![
vec![1.0, 2.0, 3.0, 4.0], vec![4.0, 3.0, 2.0, 1.0], vec![1.0, 2.0, 3.0, 4.0], ],
);
let fret = panel(
vec![1, 2, 3],
vec!["A", "B", "C", "D"],
vec![
vec![0.0, 0.0, 0.0, 0.0],
vec![0.0, 0.0, 0.0, 0.0],
vec![0.0, 0.0, 0.0, 0.0],
],
);
let rep = factor_report(&factor, &fret, 2);
assert!((rep.top_quantile_turnover - 1.0).abs() < 1e-12);
}
#[test]
fn event_study_averages_returns_by_lag() {
let events = panel(
vec![1, 2, 3, 4],
vec!["A", "B"],
vec![
vec![0.0, 0.0],
vec![0.0, 0.0],
vec![1.0, 1.0], vec![0.0, 0.0],
],
);
let returns = panel(
vec![1, 2, 3, 4],
vec!["A", "B"],
vec![
vec![0.01, 0.02],
vec![0.01, 0.02], vec![0.10, 0.20], vec![0.05, 0.06], ],
);
let es = event_study(&events, &returns, 1, 1);
assert_eq!(es.event_count, 2);
assert_eq!(es.lags, vec![-1, 0, 1]);
assert!((es.avg_return[0] - 0.015).abs() < 1e-12); assert!((es.avg_return[1] - 0.15).abs() < 1e-12); assert!((es.avg_return[2] - 0.055).abs() < 1e-12); assert!((es.cumulative[2] - 0.22).abs() < 1e-12);
}
#[test]
fn event_study_handles_edges_and_no_events() {
let events = panel(vec![1, 2], vec!["A"], vec![vec![1.0], vec![0.0]]);
let returns = panel(vec![1, 2], vec!["A"], vec![vec![0.1], vec![0.2]]);
let es = event_study(&events, &returns, 1, 1);
assert!(es.avg_return[0].is_nan()); assert!((es.avg_return[1] - 0.1).abs() < 1e-12); assert!((es.avg_return[2] - 0.2).abs() < 1e-12);
let none = panel(vec![1, 2], vec!["A"], vec![vec![0.0], vec![0.0]]);
let es2 = event_study(&none, &returns, 1, 1);
assert_eq!(es2.event_count, 0);
assert!(es2.avg_return.iter().all(|v| v.is_nan()));
}
#[test]
fn serializes_to_json() {
let factor = panel(vec![1], vec!["A", "B"], vec![vec![1.0, 2.0]]);
let fret = panel(vec![1], vec!["A", "B"], vec![vec![0.1, 0.2]]);
let json = serde_json::to_string(&factor_report(&factor, &fret, 2)).unwrap();
assert!(json.contains("\"mean_ic\""));
assert!(json.contains("\"quantile_returns\""));
let events = panel(vec![1, 2], vec!["A"], vec![vec![0.0], vec![1.0]]);
let es = event_study(&events, &fret_pad(), 1, 0);
let ejson = serde_json::to_string(&es).unwrap();
assert!(ejson.contains("\"avg_return\""));
}
fn fret_pad() -> Panel {
panel(vec![1, 2], vec!["A"], vec![vec![0.1], vec![0.2]])
}
}