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
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").