Skip to content

Signals

A signal is a score per name and date, known at that date's close. To evaluate one, build forward targets from prices, then read the daily rank IC: its mean, a HAC t-statistic and a block-bootstrap interval. A mean IC alone hides how noisy it is.

import numpy as np
import pandas as pd
from aether_quant import samples, signals

closes = samples.load_sample_prices().loc["2023-07-13":]
momentum = closes / closes.shift(20) - 1

targets = signals.forward_rank_targets(closes, horizons=(5, 20))
scores = momentum.rename_axis(index="date", columns="ticker").reset_index().melt(id_vars="date", var_name="ticker", value_name="score")
table = targets.merge(scores, on=["date", "ticker"]).dropna(subset=["score", "target_rank_20d"])
report = signals.evaluate_signal(table, score_column="score", splits=("validation",), min_names_per_date=8, bootstrap_draws=200)
h20 = report["splits"]["validation"]["h20"]
print(f"IC {h20['mean_ic']:.3f}, HAC t {h20['hac_t']:.2f}, 95% CI {h20['ci95']}")

decay = signals.ic_decay(momentum, closes, horizons=(1, 5, 20))
quantiles = signals.quantile_returns(momentum, signals.forward_returns(closes, 5), n_quantiles=3)
Function Answers
forward_returns(prices, h) the return from D's close to D+h's close
forward_rank_targets(prices, horizons) the long table evaluate_signal reads
evaluate_signal(frame, ...) IC, HAC t, bootstrap CI and decay, per split and horizon
quantile_returns(scores, fwd) the mean forward return of each score quantile
ic_decay(scores, prices, horizons) the mean IC at each horizon
factor_spanning(signal_returns, factors) is the signal more than known factors (alpha and its HAC t)?
purged_embargoed_folds(dates, ...) cross-validation folds that purge overlapping labels

Before trusting a signal, run the leakage audit (aether_quant.overfitting.audit_leakage). It flags features whose single-feature IC is implausibly high, which usually means future data leaked in. See the signal evaluation tutorial.