Skip to content

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.

audit_trial_ids

audit_trial_ids() -> list[str]

The 18 looks, as the trial ledger counts them.

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.