This repository implements ATOMS (Adaptive Tournament Model Selection) and benchmark algorithms, in the following paper:
Capponi, A., Huang, C., Sidaoui, J. A., Wang, K., and Zou, J. (2025). The Nonstationarity-Complexity Tradeoff in Return Prediction. Available at SSRN: https://ssrn.com/abstract=5980654
- ATOMS: adaptive model selection via (i) adaptive validation-window selection and (ii) a tournament procedure.
- Fixed-window baselines:
-
Fixed-val(
$\ell$ ): select the model with the lowest average validation loss over the last$\ell$ periods. - Fixed-CV: select a model using cross-validation on a fixed historical window.
-
Fixed-val(
This implementation focuses on model selection (ATOMS and baselines). It does not include the full large-scale training pipeline in the paper.
python -m venv .venv
source .venv/bin/activate # macOS/Linux
# .venv\Scripts\activate # Windows PowerShell
python -m pip install -U pip
python -m pip install -e .Notebook walkthrough: example/demo.ipynb.
The demo generates a synthetic nonstationary dataset, trains a small set of candidate models,
runs ATOMS and baselines, and reports performance summaries.
ATOMS operates on per-observation validation losses organized by time period.
The inputs include:
-
val_losses: list of lengthn_models.- Each entry is an array of validation losses concatenated by period in chronological order.
- Single response: shape
(n_obs,) - Multiple responses (e.g., 17 industry portfolios): shape
(n_obs, n_responses)(selection is done separately for each response)
-
val_sizes: list/array(n_0, n_1, ..., n_{T-1}), wheren_tis the number of validation observations
in periodt(same concatenation order asval_losses).
from atoms import ATOMS
atoms = ATOMS(delta=0.1, M=1.0, seed=0)
best_idx = atoms.select(val_losses, val_sizes) # int (single response) or (n_responses,) arrayHere, best_idx is a 0-based index into the candidate model list, corresponding to the selected model. In multi-response settings, it outputs one selected index per response.
from atoms import fixed_val_select
best_idx = fixed_val_select(val_losses, val_sizes, L=10)from atoms import fixed_cv_select
best_idx = fixed_cv_select(
specs,
X_by_period,
y_by_period,
t=t,
cv_window_periods=36,
n_splits=5,
)Here, specs is a list of CandidateSpec objects (see below), t is the testing period, and X_by_period and y_by_period store the data in period form: X_by_period[s] is the feature matrix with shape y_by_period[s] is the corresponding response array with shape
A candidate “model” is represented by a CandidateSpec:
from atoms import CandidateSpec
from sklearn.linear_model import Ridge
spec = CandidateSpec(
name="Ridge (10 periods)",
estimator_factory=lambda: Ridge(alpha=1.0),
train_window=10, # number of periods used for training
)The paper reports two
-
$R^2$ with zero benchmark:
-
$R^2$ with demeaned denominator:
where
They can be computed via
from atoms import oos_r2, oos_r2_over_periods
r2_zero = oos_r2(y_true, y_pred, demean=False)
r2 = oos_r2(y_true, y_pred, demean=True)The repository also includes the monthly refit Markov-switching forecast used for the
new algorithm. It can run either on a dated dataframe or directly on the period-based
synthetic data structure used in example/demo.ipynb:
from atoms import run_regime_switch_on_periods
pred_df = run_regime_switch_on_periods(
X_by_period,
y_by_period,
start_month="2000-01",
min_train_months=12,
k_regimes=2,
)This returns a dataframe with y_true, y_pred, forecast_month, and regime
probability columns for the out-of-sample months.
You can compute
period_sizes = [n_0, n_1, ..., n_{T-1}] # sample size per period in the concatenation
windows = {
"Full": (0, T - 1),
"Late sample": (20, T - 1),
}
r2_by_window = oos_r2_over_periods(
y_true,
y_pred,
period_sizes,
windows,
demean=False,
)src/atoms/selection.py— ATOMS: adaptive window selection + tournamentsrc/atoms/baselines.py— Fixed-val and Fixed-CV baselinessrc/atoms/metrics.py— OOS R^2 metricssrc/atoms/specs.py—CandidateSpecdefinitionsrc/atoms/synthetic.py— Synthetic nonstationary data generatorsexamples/demo_synthetic.ipynb— step-by-step demo notebook
@article{CHS25,
title={The Nonstationarity-Complexity Tradeoff in Return Prediction},
author={Capponi, Agostino and Huang, Chengpiao and Sidaoui, J.~Antonio and Wang, Kaizheng and Zou, Jiacheng},
journal={Available at SSRN 5980654},
year={2025}
}