aether_quant.training¶
Training without the training stack: splits, purges, purged folds, yearly windows, ranked inputs, the walk-forward linear model, signal recipes, and what the models could know.
Training without the training stack: splits, purges, purged folds, yearly windows, ranked inputs, the walk-forward linear model, signal recipes, and what the models could know.
annual_windows ¶
annual_windows(dates: list[str], *, first_test_year: int = FIRST_TEST_YEAR, purge_days: int = PURGE_DAYS) -> list[dict]
Expanding windows: each test year is scored by a model trained on everything up to purge_days before it, since a
20-day label of a row closer than that is still open on the test year's first day.
assign_split ¶
assign_split(date_value: Timestamp, windows: dict[str, Any]) -> str | None
Which window a date falls in (training, validation or backtest), or None outside them.
assign_split_series ¶
assign_split_series(dates: Series, windows: dict[str, Any]) -> pd.Series
assign_split() for a whole column at once (V5.8.0: the per-row apply ran once per dataset row); same inclusive bounds and precedence, None outside every window.
build_table ¶
build_table(recipe: Mapping[str, Any], tables: Mapping[str, DataFrame], member: DataFrame | None = None, *, sectors: Mapping[str, str] | None = None) -> pd.DataFrame
One trial's table from a recipe in its round file: {"op": "single" | "rank_mean" | "residualise", "parts": [names], "signs": {name: -1.0}, "minimum": n, "exposures": [names]}. A missing part is an error: a trial must not quietly become a different trial.
centred_label ¶
centred_label(frame: DataFrame, target: str = TARGET) -> pd.Series
The target minus its mean over the same date and class, so class size does not shift the label.
fama_macbeth_ridge ¶
fama_macbeth_ridge(features: Mapping[str, DataFrame], label: DataFrame, *, signs: Mapping[str, float], kappa: int = 36, ridge: float = 1.0, horizon: int = 21, purge_sessions: int = 21, every: int = 21, start: str) -> pd.DataFrame
Session x ticker scores from start. The score at D is the features at D times the mean of the last kappa monthly slopes whose label had
fully ended by D - purge_sessions. A mean slope whose sign disagrees with signs counts as zero, so noise can't flip a published premium, and
with every slope at zero the row is NaN. With fewer than MIN_SLOPE_MONTHS slopes it uses the signs as equal weights.
feature_group_ic ¶
feature_group_ic(cell: WideCell, *, horizons: tuple[int, ...] = HORIZONS, ic_fn: Callable[..., Any] | None = None) -> dict[str, Any]
{group: {horizon: mean IC, non-overlapping t, IC by year}} on the cell's own dates. Refuses a cell outside the free ones: this is a report, and no freeze stands behind it.
fundamental_coverage ¶
fundamental_coverage(features: Mapping[str, DataFrame], member: DataFrame) -> dict[str, Any]
{field: {year: share of member rows with a value}}, on member rows only. The early years of training windows read this.
group_table ¶
group_table(features: Mapping[str, DataFrame], group: str, member: DataFrame | None = None) -> pd.DataFrame | None
The group's fixed composite, from whichever of its features this cell holds. None if it holds none.
monthly_slope ¶
monthly_slope(ranks: ndarray, label: ndarray, *, ridge: float) -> np.ndarray | None
One date's ridge slope of the label on the ranked features, on names with a label and at least one feature.
purge_label_overlap ¶
purge_label_overlap(splits: list[Any], horizon_rows: int) -> list[Any]
V5.7.0: relabel "purged" every row whose forward label window reaches into a LATER split. A row labelled
horizon_rows bars ahead (the 20-day return / rank targets) carries information from the next split's prices, so the
last horizon_rows rows of train (and of validation) leak the validation (and backtest) period into the data the model
is fitted, early-stopped and selected on. splits is one asset's per-row split labels in date order (None / NaN rows
are outside every window and stay as they are); purged rows are kept in the frame as feature context but match no
split filter. horizon_rows <= 0 returns the labels unchanged.
purge_label_overlap_calendar ¶
purge_label_overlap_calendar(splits: list[Any], dates: list[Any], calendar_days: int) -> list[Any]
purge_label_overlap in calendar days. A pooled rank mixes classes: 20 crypto bars are 20 days but 20 equity sessions are
about 28, so a crypto row kept by a 20-row purge can have a rank built from equity returns past the boundary.
calendar_days <= 0 returns the labels unchanged.
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).
rank_inputs ¶
rank_inputs(frame: DataFrame, features: list[str]) -> pd.DataFrame
Within-(date, class) percentile rank minus one half; a missing value reads as the middle of its group.
residualise_signal ¶
residualise_signal(table: DataFrame, exposures: Mapping[str, DataFrame], sectors: Mapping[str, str]) -> pd.DataFrame
Per date, what is left of the signal's rank after an OLS on the ranked exposures and sector dummies. A name missing an exposure gets NaN; a date with too few names is all NaN, never an unadjusted signal.