Skip to content

aether_quant.research

Research and audit tools (V6.4.3): one report for any signal, the probability of backtest overfitting, factor baselines and spanning, overfitting and leakage audits, the simulator / Lean gap, research probes, the trial registry.

Research and audit tools (V6.4.3): one report for any signal, the probability of backtest overfitting, factor baselines and spanning, overfitting and leakage audits, the simulator / Lean gap, research probes, the trial registry.

FACTOR_COLUMNS module-attribute

FACTOR_COLUMNS = ('f_mom_12_1', 'f_mom_20', 'f_rev_5', 'f_lowvol', 'f_high52')

ResearchContext dataclass

Everything a research trial runs against: the market panel, the signal panel, the path config and the simulator.

carry instance-attribute

carry: Callable[[str, str], float]

config instance-attribute

config: TargetWeightsConfig

listed instance-attribute

listed: Callable[[str, str], bool]

market instance-attribute

market: MarketPanel

panel instance-attribute

panel: ResearchPanel

rf_dates instance-attribute

rf_dates: list[str]

rf_rates instance-attribute

rf_rates: list[float]

settings class-attribute instance-attribute

settings: WeightSettings = field(default_factory=WeightSettings)

sweep instance-attribute

sweep: dict

sessions_for

sessions_for(spec: SleeveSpec) -> set[str] | None

Sessions to score a sleeve on; None for a crypto-only sleeve, whose calendar is every day.

simulate

simulate(weights: Mapping[str, Mapping[str, float]], start: str, end: str, *, max_drawdown_velocity: float | None = None, max_total_drawdown: float | None = None) -> dict[str, Any]

WideCell dataclass

A research cell with its context and, for the wide panels, its bars and membership.

bars class-attribute instance-attribute

bars: WideBars | None = None

cell instance-attribute

cell: Cell

context instance-attribute

context: ResearchContext

data_dir class-attribute instance-attribute

data_dir: str = ''

features class-attribute instance-attribute

features: dict[str, DataFrame] = field(default_factory=dict)

label class-attribute instance-attribute

label: DataFrame | None = None

member class-attribute instance-attribute

member: DataFrame | None = None

root class-attribute instance-attribute

root: Path | None = None

sectors class-attribute instance-attribute

sectors: dict[str, str] = field(default_factory=dict)

tables class-attribute instance-attribute

tables: dict[str, DataFrame] = field(default_factory=dict)

alpha_proof_report

alpha_proof_report(run_dir: Path, config: dict[str, Any], root: Path, *, trial_sharpes: list[float], deflation: dict[str, Any] | None = None, data_dir: str = 'data') -> dict[str, Any]

Every period's figures and the R-ALPHA1 verdict for one Lean run folder (its frozen config when present). data_dir is the run's data root; a closes or benchmark read from any other root would silently come back empty or wrong.

alpha_regression

alpha_regression(y: Series, x: DataFrame, *, hac_lags: int = 5, annualization: float = 252.0) -> dict[str, Any]

OLS alpha of y on x (inner-joined dates, NaN rows dropped) with a Newey-West t; annualization is the number of y periods a year (session data: 252).

annual_sharpe

annual_sharpe(daily: Series, *, sessions: Iterable[str] | None = None) -> float | None

Given sessions, the series is compounded onto them and annualised at 252: a panel has no weekend rows before its first crypto bar, and sqrt(365) on session rows inflates a Sharpe about 1.2 times. Without them, calendar rows at sqrt(365).

append_trial

append_trial(path: Path | str, *, trial_id: str, track: str, window: str, role: str, annualised_sharpe: float | None, n_obs: int, config_sha: str = '', weights_sha: str = '', engine: str = '', detail: dict[str, Any] | None = None) -> dict[str, Any]

Append one trial's record to the append-only trial ledger.

audit_leakage

audit_leakage(frame: DataFrame, *, feature_columns: Sequence[str], target_column: str = 'target_rank_20d', ticker_column: str = 'ticker', date_column: str = 'date', split_column: str = 'split', horizon: int = 20, class_columns: Sequence[str] = (), model_score_column: str | None = None, min_names: int = 20, seed: int = 7, group_column: str | None = None) -> dict[str, Any]

All four checks; (b) and (c) on the validation split, (d) fit on train and read on validation.

benchmark_daily_returns

benchmark_daily_returns(root: Path, config: dict[str, Any], tickers: Sequence[str] = BENCHMARKS, *, start: str, end: str, data_dir: str = 'data', required: bool = False, lag_equity: bool = False) -> pd.DataFrame

Session-calendar daily returns of the benchmark tickers from their config zips: the calendar is the first ticker's (SPY, US equity sessions); a ticker that also trades on other days (BTC) is priced at its last close on or before each session, so every column spans the same session-to-session interval. Tickers not in the universe are left out, or raise with required, since a verdict must not quietly lose a benchmark. Before a ticker's first bar its return is 0, not NaN: the regression drops any row with a NaN, which would cut a window back to BTC's first bar. lag_equity also reads an equity column a session late, like the crypto and forex ones: a crypto book's daily sample holds the bar dated before it, so SPY and AGG on their own date sit a session out of line with it (#330). It is off by default, so the pre-registered R-ALPHA1 figures are unchanged, and alpha_session_aligned reports the aligned one beside them.

block_bootstrap_mean_ci

block_bootstrap_mean_ci(values: ArrayLike, *, block: int, draws: int = 1000, seed: int = 7, confidence: float = 0.95) -> list[float] | None

Percentile CI of the mean of a serially correlated series, resampling contiguous blocks of block values.

build_context

build_context(root: Path, config: dict[str, Any], *, data_dir: str = 'data') -> ResearchContext

Build a ResearchContext from the repo's data: the market panel, known closes, FRED, CFTC and carry inputs, and listing dates.

build_factor_returns

build_factor_returns(frame: DataFrame, factor_columns: Sequence[str], *, date_column: str = 'date', forward_return_column: str = 'target_return_1d', quantile: float = 0.2, min_names: int = 20) -> pd.DataFrame

Daily equal-weight top-quantile minus bottom-quantile return of each score column; NaN on dates with fewer than min_names scored names. forward_return_column at t is the t -> t+1 return (the simulator's convention).

build_factor_scores

build_factor_scores(frame: DataFrame, *, ticker_column: str = 'ticker', date_column: str = 'date') -> pd.DataFrame

f_mom_12_1 (sum of daily returns from t-252 to t-21), f_mom_20 (momentum_20d), f_rev_5 (-5-day return), f_lowvol (-rolling_volatility_20d), f_high52 (dist_52w_high: higher = nearer the 52-week high) and f_composite (mean of the per-date ranks of mom_12_1, high52, lowvol and rev_5). Same index as frame.

build_registry

build_registry(evaluation_dir: Path) -> dict[str, Any]

Every trial ever scored, collected from the research reports under evaluation_dir, for the deflated Sharpe's N.

calendar_rows

calendar_rows(market: MarketPanel, cell: Cell, warmup_sessions: int) -> list[str]

Every date from warmup_sessions equity sessions before the cell to its end. A crypto panel has calendar days between its sessions.

candidate_ids

candidate_ids(entries: list[dict]) -> set[str]

The ids of the ledger's candidate trials.

collect_trials

collect_trials(payload: Any, phase: str, path: tuple[str, ...] = ()) -> list[dict]

(phase, path, median Sharpe) for every <...>/backtest/{median_net_sharpe} summary in a research payload.

crypto_cell

crypto_cell(root: Path, base: dict[str, Any], cell_name: str, *, freeze_path: Path | None = None, freeze_digest: str | None = None, data_dir: str = CRYPTO_ROOT, settings: WeightSettings = WIDE_SETTINGS, side: str | None = None) -> WideCell

Binance's coins under the Coinbase cost model. Side A coins enter a later cell only from 2023-01, so the part selection read stays out. side restricts the cell to one side's coins, for R-CRYPTO1.

crypto_ic

crypto_ic(signal: DataFrame, known: DataFrame, *, start: str, end: str, min_names: int = 8, horizon: int = 20) -> dict[str, Any]

A crypto signal's rank IC with the next horizon days' return on the same coins, with the non-overlapping t.

daily_gap_breakdown

daily_gap_breakdown(lean: dict[str, float], offline: dict[str, float], *, rebalance_dates: Iterable[str] = (), worst_share: float = 0.05) -> dict[str, Any]

Statistics of the daily gap offline - lean over their common dates.

daily_returns_for

daily_returns_for(context: ResearchContext, weights: Mapping[str, Any], *, start: str, end: str) -> pd.Series

The daily excess returns of a book of weights over [start, end] in this context's simulator.

dedupe

dedupe(trials: list[dict]) -> list[dict]

One trial per distinct (phase, score): the same configuration re-scored in a later stage is not a new trial.

deflated_sharpe_from_inputs

deflated_sharpe_from_inputs(returns: ArrayLike, *, num_trials: int, trial_variance: float | None, annualization: float) -> dict[str, Any]

The deflated Sharpe with N and the trial variance given (evaluation.trial_ledger.deflation_inputs), not derived from a list of trial Sharpes: the variance must come from trials on the window being judged.

deflation_inputs

deflation_inputs(entries: list[dict], legacy_registry: dict[str, Any], window: str) -> dict[str, Any]

{"N", "var", "legacy_var", "same_window_trials", "floored"} for a deflated Sharpe judged on window. A candidate scored several times on a window counts once, by its last score. entries must hold every ledger phase's rows.

equity_cell

equity_cell(root: Path, base: dict[str, Any], cell_name: str, *, freeze_path: Path | None = None, freeze_digest: str | None = None, data_dir: str = WIDE_ROOT, settings: WeightSettings = WIDE_SETTINGS, with_features: bool = True, with_info: bool = True) -> WideCell

The cell's context, features and w-tables. Sectors come from SEC; a name with none is never ranked.

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

fit_hgb_rank

fit_hgb_rank(x_train: ndarray, y_train: ndarray, **params: Any) -> Any

Fit a histogram gradient-boosted regressor to the finite targets (sklearn, imported on call).

fit_ridge_rank

fit_ridge_rank(x_train: ndarray, y_train: ndarray, *, alpha: float = 10.0, features: Sequence[str] | None = None) -> dict[str, Any]

Ridge on centred targets: coef = (X'X + alpha I)^-1 X'(y - mean y). Rows with any NaN are dropped.

fresh_sessions

fresh_sessions(y: Series, sessions: Iterable[str], *, start: str = FRESH_START) -> int

Equity sessions after the data freeze, whatever else the series holds.

gap_report

gap_report(calibration: dict[str, Any], *, variant: str = 'calibrated') -> dict[str, Any]

{window: {"ladder": rung_attribution, "best_rung", "judged": the variant row}} for a lean_calibration.py report.

head_ic

head_ic(metrics: dict[str, Any], split: str, head: str) -> float | None

<split>.<head>_ic_non_overlapping.mean_ic of a training-metrics file (the daily <head>_ic as a fallback).

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

identity_check

identity_check(train: DataFrame, evaluation: DataFrame, target_column: str, *, ticker_column: str = 'ticker', date_column: str = 'date', class_columns: Sequence[str] = (), model_score_column: str | None = None, min_names: int = 20, group_column: str | None = None) -> dict[str, Any]

IC of an asset-identity prior (each ticker's mean train target) and an asset-class prior against the model's. With group_column (V6.4.4) the same figures are also computed on the group-centred target ("group_centred"): the raw priors mostly read the group-size artifact of percentile ranks, which a book ranking within groups cannot trade.

label_shuffle_check

label_shuffle_check(frame: DataFrame, feature_columns: Sequence[str], target_column: str, *, date_column: str = 'date', min_names: int = 20, seed: int = 7, max_abs_t: float = 3.0) -> dict[str, Any]

A shuffled target's IC is sampling noise around 0; |t| above max_abs_t is not.

lag_onto

lag_onto(table: DataFrame, index: Index, *, limit: int = 7) -> pd.DataFrame

A day-indexed table (volume, funding) as the session knows it: the value of day d is visible from d+1, carried forward at most limit rows. Same timing as c3.

learning_curve

learning_curve(fit_and_score: Callable[[DataFrame, DataFrame], float], frame: DataFrame, *, train_years: Sequence[int] = (1, 2, 4, 7), train_end: str = '2021-12-31', eval_split: str = 'validation', date_column: str = 'date', split_column: str = 'split') -> list[dict]

Out-of-sample score for training windows of train_years years ending at train_end; fit_and_score(train, evaluation) fits on the first frame and returns a score (e.g. mean rank IC) on the second.

legacy_trials

legacy_trials(registry: dict[str, Any], exclude_phases: tuple[str, ...] = LEDGER_PHASES) -> list[dict]

The registry's trials not already in a ledger: the registry also holds V6.5.0's ledger, which candidate_ids counts again, so N read 178 for what is 128.

liquid_mask_for_cell

liquid_mask_for_cell(liquid: DataFrame, cell: Cell, project: list[str]) -> pd.DataFrame

In the holdout cell, side A coins count only after the window selection read.

load_ohlcv

load_ohlcv(root: Path, tickers: list[str], rows: list[str], *, data_dir: str) -> dict[str, pd.DataFrame]

open/high/low/volume tables on rows, from the Lean zips.

load_raw_closes

load_raw_closes(root: Path, tickers: list[str], rows: list[str], *, data_dir: str) -> pd.DataFrame

Unadjusted closes of the tickers on the given rows, from reference/raw_closes.

load_research_frame

load_research_frame(dataset_path: str | Path, *, columns: Sequence[str], splits: Sequence[str] | None = None, chunksize: int = 40000) -> pd.DataFrame

The dataset's KEY_COLUMNS plus columns (numeric ones as float32), optionally only some splits.

max_drawdown

max_drawdown(daily: Series) -> float

The largest peak-to-trough fall of the compounded daily returns, as a positive fraction.

measured_annualization

measured_annualization(index: Sequence[str] | Index, *, fallback: float) -> float

Rows a year of this very series: a run with no crypto has no weekend rows, and a fixed 365 would inflate its Sharpe by about 1.2 times. A weekend-free series is 252; one with weekend rows keeps the config's own figure.

nulls_for

nulls_for(spec: SleeveSpec) -> tuple[str, ...]

A ranking can be relabelled across assets; a direction or a timing rule has no cross-section to relabel.

passes_sharpe_clauses

passes_sharpe_clauses(score: Mapping[str, Any], *, half_floor: float = 0.4, pooled_floor: float = 0.5) -> bool

Whether a trial's pooled Sharpe and both halves' Sharpes clear their floors (R-SLEEVE3's Sharpe clauses).

path_config

path_config(base: dict[str, Any]) -> TargetWeightsConfig

The target-weights config research runs on: the project's, with the research overrides.

period_report

period_report(y: Series, benchmarks: DataFrame, *, annualization: float, trial_sharpes: list[float], deflation: dict[str, Any] | None = None) -> dict[str, Any]

deflation (evaluation.trial_ledger.deflation_inputs) replaces the spread of trial_sharpes, which mixes windows and annualisations, when the caller has one.

phase_offsets

phase_offsets(rebalance_every: int, phases: int = PHASES) -> list[int]

The start offsets a rebalance cycle is averaged over, so no trial depends on its starting day.

placebo_p_value

placebo_p_value(observed: float | None, placebos: list[float]) -> float

Share of placebos at or above the observed Sharpe, counting the observation itself (never exactly zero).

placebo_sharpes

placebo_sharpes(spec: SleeveSpec, context: ResearchContext, mode: str, *, start: str, end: str, draws: int, seed: int = 1) -> list[float]

Sharpes of draws placebo runs over the same window, one random rebalance phase each.

predict_scores

predict_scores(model: Any, x: ndarray) -> np.ndarray

Scores for every row; a ridge row with a NaN feature scores NaN (the model has no view on it).

probability_of_backtest_overfitting

probability_of_backtest_overfitting(returns: ArrayLike, *, blocks: int = 16, periods_per_year: int = 252) -> dict[str, Any]

{"pbo", "num_combinations", "num_trials", "lambda_quantiles", "degradation_slope", "degradation_intercept", "prob_oos_loss"}; pbo is None when there are fewer than 2 trials or fewer rows than blocks.

project_tickers

project_tickers(base: dict[str, Any], security_type: str) -> list[str]

The project universe's tickers of one security type.

r_alpha1

r_alpha1(report: dict[str, Any]) -> dict[str, Any]

The descriptive verdict over the per-period figures.

read_info

read_info(root: Path, *, data_dir: str) -> dict[str, pd.DataFrame | None]

What scripts/build_wide_info.py wrote; an absent part reads as None, and its trials report an empty book.

read_ledger

read_ledger(path: Path | str) -> list[dict]

The trial ledger's rows, or [] when the file does not exist.

rung_attribution

rung_attribution(variants: dict[str, dict], order: Sequence[str] = LADDER_ORDER) -> list[dict]

[{"rung", "daily_correlation", "delta_correlation", "annual_return_gap", "delta_gap"}] along the ladder.

score_sleeve

score_sleeve(spec: SleeveSpec, context: ResearchContext, *, start: str, split: str, end: str, signal: DataFrame | None = None) -> dict[str, Any]

Median over the phases of the ex-T-bill Sharpe: pooled and on both sides of split.

selection_gate

selection_gate(scores: list[dict], p_values: Mapping[str, Mapping[str, float]], *, max_survivors: int = 5, max_correlation: float = 0.7, alpha: float = 0.05) -> dict[str, Any]

R-SLEEVE2. A survivor passes both Sharpe clauses and every placebo test; among survivors the higher Sharpe keeps its place against any other with daily correlation above max_correlation; at most max_survivors remain.

sharpe

sharpe(values: ndarray, annualization: float) -> float | None

The annualised Sharpe of the finite values; None with fewer than two or no variance.

sharpe_at_sessions

sharpe_at_sessions(series: Series, sessions: Sequence[str] | Index) -> float | None

The annual Sharpe of a daily series sampled at the given sessions.

sharpe_bootstrap_ci

sharpe_bootstrap_ci(y: Series, *, annualization: float, block: int = 10, draws: int = 2000, seed: int = 7) -> tuple[float | None, float | None]

95% interval of the annualised Sharpe by a stationary block bootstrap (Politis & Romano, mean block block).

signal_ic_t

signal_ic_t(table: DataFrame, label: DataFrame, *, start: str, end: str, horizon: int = 20, min_names: int = 20) -> dict[str, Any]

The signal's rank IC with the label, with the t on non-overlapping dates (a 20-session label overlaps itself).

single_feature_ic

single_feature_ic(frame: DataFrame, feature_columns: Sequence[str], target_column: str, *, date_column: str = 'date', min_names: int = 20, threshold: float = 0.15) -> dict[str, Any]

Each feature's mean IC against the target, flagged when its size suggests it reads the future.

slice_market

slice_market(market: MarketPanel, rows: list[str]) -> MarketPanel

The panel on exactly these dates, and the sessions among them. Positions are found by date, so a hole is impossible.

strategy_daily_returns

strategy_daily_returns(run: dict[str, Any], config: dict[str, Any], root: Path, *, data_dir: str = 'data') -> pd.Series

Daily strategy return (NAV change minus the T-bill sweep's net P&L, over the previous NAV), indexed by the date the return ends on - the same series backtest_statistics.compute_backtest_statistics calls the strategy.

tail_nan_check

tail_nan_check(frame: DataFrame, feature_columns: Sequence[str], *, ticker_column: str = 'ticker', date_column: str = 'date', horizon: int = 20, threshold: float = 0.05) -> dict[str, Any]

Whether each feature goes missing in the last horizon rows of every ticker more often than the threshold, a sign it was built from future data.

to_sessions

to_sessions(daily: Series, sessions: Sequence[str] | Index) -> pd.Series

Compounds a calendar-daily return series onto sessions: each session gets every return dated after the previous session and up to itself (weekend crypto moves land on Monday, as they do in a session-based benchmark).

training_gap_report

training_gap_report(metrics_paths: dict[str, Path], heads: Sequence[str] = RANK_HEADS, splits: Sequence[str] = ('train', 'validation', 'backtest')) -> dict[str, Any]

{model: {head: {split: ic, "train_minus_validation": gap, "retained": validation / train}}} from ml/_training_metrics.json files.

weights_for

weights_for(spec: SleeveSpec, context: ResearchContext, signal: DataFrame, *, start: str, end: str, phase_offset: int) -> dict[str, Any]

The sleeve's weights over [start, end] in this context, from one phase offset.

wide_config_for

wide_config_for(base: dict[str, Any], root: Path, tickers: set[str], *, data_dir: str = WIDE_ROOT) -> dict[str, Any]

The config of the wide research root: the project's, widened to the given tickers and the wide manifest.

window_report

window_report(y: Series, benchmarks: DataFrame, *, start: str, end: str, annualization: float, deflation: dict[str, Any] | None = None) -> dict[str, Any]

Sharpe, alpha, max drawdown and (with deflation) the deflated Sharpe of a strategy series on one window.