Skip to content

aether_quant.signals

Signal evaluation, grouped by the job (V6.6.5): forward targets from prices, the IC report with HAC t-statistics and bootstrap intervals, returns by quantile, IC decay, factor spanning and purged folds.

Signal evaluation, grouped by the job (V6.6.5): forward targets from prices, the IC report with HAC t-statistics and bootstrap intervals, returns by quantile, IC decay, factor spanning and purged folds.

evaluate_signal

evaluate_signal(frame: DataFrame, *, score_column: str, date_column: str = 'date', ticker_column: str = 'ticker', split_column: str = 'split', splits: Sequence[str] = ('validation', 'backtest'), horizons: Sequence[int] = (5, 20), target_template: str = 'target_rank_{h}d', class_column: str | None = None, regime_column: str | None = None, min_names_per_date: int = 20, era_days: int = 90, bootstrap_draws: int = 1000, seed: int = 7) -> dict[str, Any]

{split: {"h": ic_summary, "by_class": ..., "by_regime": ..., "decay": {horizon: mean_ic}}}.

factor_spanning

factor_spanning(signal_returns: Series, factor_returns: DataFrame, *, hac_lags: int = 19, periods_per_year: int = 252) -> dict[str, Any]

OLS of the signal's daily return on the factors' (dates present in both, rows with a NaN dropped).

forward_rank_targets

forward_rank_targets(prices: DataFrame, horizons: Sequence[int] = (5, 20), *, split: str = 'validation') -> pd.DataFrame

Long format with date, ticker, split, target_return_1d and, per horizon, target_return_{h}d and target_rank_{h}d (the cross-sectional percentile of the forward return on that date). Rows without a 1-day target drop.

forward_returns

forward_returns(prices: DataFrame, horizon: int) -> pd.DataFrame

price[t + h] / price[t] - 1 (dates x tickers), NaN where either price is missing or non-positive.

ic_decay

ic_decay(scores: DataFrame, prices: DataFrame, horizons: Sequence[int] = (1, 5, 10, 20, 60), *, min_names: int = 3) -> pd.Series

Mean daily Spearman correlation between the scores and each horizon's forward return; NaN when no date has min_names names with both.

ic_summary

ic_summary(ic_by_date: dict[str, float], *, horizon: int, era_days: int = 90, bootstrap_draws: int = 1000, seed: int = 7) -> dict[str, Any]

Statistics of one daily IC series ({date: ic}).

newey_west_t_stat

newey_west_t_stat(values: ArrayLike, lags: int) -> float

HAC (Newey-West, Bartlett weights) t-statistic of the mean of a daily IC series. Consecutive daily ICs of an h-day-horizon signal share most of their forward window, so the plain t-stat overstates significance; the non-overlapping subsample is honest but throws away (h-1)/h of the dates (14 of 261 on the validation split). lags ~ horizon - 1. Returns 0.0 when fewer than 2 finite values or no variance.

purged_embargoed_folds

purged_embargoed_folds(dates: ArrayLike, n_folds: int, horizon_days: int, embargo_days: int = 0) -> list[tuple[np.ndarray, np.ndarray]]

Phase 2 of the 5/10 -> 9/10 roadmap: a standard purged/embargoed K-fold split (Lopez de Prado-style) over dates (any array-like of orderable date values, one entry per dataset row - duplicates expected, since every asset shares each date). Layered inside the existing fixed phase1.windows train split as an additional diagnostic for hyperparameter/early-stopping robustness and a more honest in-sample IC distribution - it does NOT replace the top-level train/validation/backtest wall (that stays the single no-lookahead-across-the-wall boundary; see assign_split()), nor does it change which rows actually get promoted.

For each of n_folds contiguous, equal-width date-range folds (assigned by unique date, not by row, so every asset's same-date rows always fall in the same fold together): the validation fold is exactly that date-range; the training rows are every row from the OTHER folds, minus (a) any row whose own forward-return window (horizon_days ahead) would overlap the validation fold's date range at all ("purge" - a training row labeled using information that reaches into the validation window leaks), and (b) any row falling within embargo_days immediately AFTER the validation fold's end date ("embargo" - guards against lookback-feature leakage, e.g. rolling_volatility_20d/momentum_20d computed just after the validation window still partially "sees" it).

Returns a list of (train_row_indices, validation_row_indices) index arrays into dates itself (positional, 0-based) - same shape convention as sklearn's KFold.split(), so this composes with any existing sklearn-based tooling without adapting a different interface. Returns [] (never raises) when there are fewer unique dates than n_folds, matching this codebase's "guard against thin data" convention throughout train.py (e.g. build_cross_sectional_rank_targets()'s min_universe_size gate).

quantile_returns

quantile_returns(scores: DataFrame, forward_returns_frame: DataFrame, *, n_quantiles: int = 5) -> pd.DataFrame

Per date, the mean forward return of each score quantile (columns 1..n, 1 = lowest scores). Dates with fewer names than quantiles are skipped.

rank_ic_from_arrays

rank_ic_from_arrays(predictions: ndarray, targets: ndarray, dates: ndarray, non_overlapping_stride: int = 1, min_names_per_date: int = 2) -> dict[str, Any]

Per-date Spearman rank correlation ("rank-IC") between a model's raw rank_5d/20d prediction score and the realized cross-sectional rank target (target_rank_5d/20d - already a per-date percentile rank of forward return, see train.py::build_cross_sectional_rank_targets()). This is the standard evaluation metric for a cross-sectional/long-short signal: a mean rank-IC of 0.02-0.05 with a real t-stat is a genuine, monetizable edge in this literature.

Spearman(A, B) = Pearson(rank(A), rank(B)); target_rank is already rank(realized return) rescaled to [0, 1] (a positive affine transform, which Pearson is invariant to), so only the model's own raw prediction needs an explicit per-date rank transform before a plain Pearson correlation against target_rank directly reproduces the true Spearman correlation between prediction and realized return.

non_overlapping_stride (e.g. 5 for the 5d horizon, 20 for the 20d horizon) subsamples to every Nth unique date before computing - consecutive daily rows share most of their forward-return window, so the naive daily IC series is autocorrelated and its plain t-stat overstates significance; the non-overlapping subsample gives a more honest (if noisier, fewer-observation) significance check. Pass 1 (default) for the full, autocorrelated series.

min_names_per_date (V5.3.5, development/Problems.md #102) - default 2 reproduces this function's original behavior exactly (train.py's/ performance/rank_ic_monitor.py's existing callers are unaffected). A date with exactly 2 names is mathematically FORCED to a Pearson correlation of either +1 or -1 (two points always fit a line, tie aside) - a legitimate computation, but statistically uninformative, and it gets averaged into mean_ic with the exact same weight as a date with 100 names. portfolio/rolling_ic_gate.py's offline calibration/replay tools raise this well above 2 specifically because a walk-forward rolling-IC distribution's early, thin-universe dates were found (V5.3.5, real data) to produce spurious +-1 readings that dominate the low end of the distribution.

Dates with fewer than min_names_per_date eligible (non-NaN target) assets, or zero variance on either side (e.g. every asset tied), contribute no IC value for that date (skipped, not a spurious 0.0 - which would misrepresent "undefined" as "no correlation").