We pre-register and walk a strategy forward across S&P 500 in 1-year steps, each tested out-of-sample on data the strategy had never touched, with survivorship-bias-free constituents reconstructed as of every anchor. Stitched end to end, the walk realises an annualised Sharpe of 0.45 over 2005 out-of-sample observations. Deflating for the 11 trials the search actually ran gives a probabilistic deflated Sharpe of 36% — the strategy does not survive the multiple-testing correction — most likely an artifact of the search. Every attempt made along the way — 11 registered circuits, pruned candidates included — is counted in the correction, so the headline number reflects the true cost of the search.
The study is sealed before it starts: the universe, the step size and the method are fixed at registration and cannot be changed while the walk is running, and the anchor advances only forward. This removes the two commonest ways a backtest flatters itself — moving the test window until the numbers look good, and changing the rules with hindsight.
At each step the strategy is fitted on the 2 years of history ending at the anchor and then tested on the subsequent out-of-sample window it had never seen. Index membership is reconstructed as of the anchor date from the exchange's dated constituent change-log, so names that were later removed or delisted still compete on the dates they actually traded and there is no survivorship bias. Under the evolving method, each step may try several candidate circuits; one is kept and the rest are pruned. Crucially, every candidate — kept or discarded — is registered and counts toward the trial total below.
The inferential statistic is the Deflated Sharpe Ratio (Bailey and López de Prado, 2014), which asks: given that 11 configurations were tried, what is the probability that the observed Sharpe is genuinely positive rather than the best of many noisy draws? The number of trials is frozen at compile time and taken as the larger of the registered circuit count and the number of logged runs, so it can never understate the search.
| # | Step | Out-of-sample window | N | Sharpe | Deflated |
|---|---|---|---|---|---|
| 1 | Regime aware portfolio · step 1 | 2016-01-04 → 2016-12-30 | 1 | 0.32 | 62% |
| 2 | Regime aware portfolio · step 2 | 2017-01-03 → 2017-12-29 | 4 | 1.89 | 77% |
| 3 | Regime aware portfolio · step 3 | 2018-01-02 → 2018-12-31 | 5 | -0.91 | 2% |
| 4 | Regime aware portfolio · step 4 | 2019-01-02 → 2019-12-31 | 6 | 0.25 | 15% |
| 5 | Regime aware portfolio · step 5 | 2020-01-02 → 2020-12-31 | 7 | 0.08 | 10% |
| 6 | Regime aware portfolio · step 6 | 2021-01-04 → 2021-12-31 | 8 | 1.21 | 40% |
| 7 | Regime aware portfolio · step 7 | 2022-01-03 → 2022-12-30 | 9 | -0.06 | 6% |
| 8 | Regime aware portfolio · step 8 | 2023-01-03 → 2023-12-29 | 10 | 1.03 | 29% |
The search registered 11 circuits in total (pruned candidates included); the correction uses this registered-circuit count, N = 11. The pooled deflated Sharpe is the inferential headline; the per-step deflated track in Table 1 is illustrative, since near-identical variants are correlated and per-step deflation over-penalises.
The circuit routes capital by market regime: a classifier labels each period calm, choppy or stressed and the strategy re-weights — and, crucially, de-risks — accordingly. Those states are the translucent bands behind Figure 1. This is where a regime overlay earns or loses its keep: the table below shows how often the book sat in each state and how much capital it actually put to work there versus held idle in cash. A model that retreats to cash in choppy and stressed periods gives up return for a smoother ride — the trade-off is visible here, not hidden in the headline.
| Regime | Time in state | Capital deployed | Idle cash |
|---|---|---|---|
| Calm | 54.5% | 100.0% | 0.0% |
| Choppy | 21.7% | 75.0% | 25.0% |
| Stressed | 23.8% | 50.0% | 50.0% |
Each step's strategy is a circuit of platform primitives, frozen at registration. The table lists the full component set per step and — where the hypothesis evolved — exactly what changed against the previous step: components added, removed, or re-tuned.
| # | Components (primitives & key parameters) | Evolution vs previous step |
|---|---|---|
| 1 | universe (preset=sp500) · price loader · factor loader · factor value · factor quality · factor momentum · factor growth · factor regime tilt · factor composite · regime gmm (n_regimes=auto) · vix regime · hmm vol · regime ensemble (method=majority) · factor top tier · portfolio backtest (horizon=1y, rebalance=quarterly, selection=reselect) · portfolio forward autopsy · monte carlo (horizon=1y) | — |
| 2 | universe (preset=sp500) · price loader · factor loader · factor value · factor quality · factor momentum · factor growth · factor regime tilt · factor composite · regime gmm (n_regimes=auto) · vix regime · hmm vol · regime ensemble (method=majority) · factor top tier · portfolio backtest (horizon=1y, rebalance=quarterly, selection=reselect) · portfolio forward autopsy · monte carlo (horizon=1y) | unchanged — carried forward |
| 3 | universe (preset=sp500) · price loader · factor loader · factor value · factor quality · factor momentum · factor growth · factor regime tilt · factor composite · regime gmm (n_regimes=auto) · vix regime · hmm vol · regime ensemble (method=majority) · factor top tier · portfolio backtest (horizon=1y, rebalance=quarterly, selection=reselect) · portfolio forward autopsy · monte carlo (horizon=1y) | unchanged — carried forward |
| 4 | universe (preset=sp500) · price loader · factor loader · factor value · factor quality · factor momentum · factor growth · factor regime tilt · factor composite · regime gmm (n_regimes=auto) · vix regime · hmm vol · regime ensemble (method=majority) · factor top tier · portfolio backtest (horizon=1y, rebalance=quarterly, selection=reselect) · portfolio forward autopsy · monte carlo (horizon=1y) | unchanged — carried forward |
| 5 | universe (preset=sp500) · price loader · factor loader · factor value · factor quality · factor momentum · factor growth · factor regime tilt · factor composite · regime gmm (n_regimes=auto) · vix regime · hmm vol · regime ensemble (method=majority) · factor top tier · portfolio backtest (horizon=1y, rebalance=quarterly, selection=reselect) · portfolio forward autopsy · monte carlo (horizon=1y) | unchanged — carried forward |
| 6 | universe (preset=sp500) · price loader · factor loader · factor value · factor quality · factor momentum · factor growth · factor regime tilt · factor composite · regime gmm (n_regimes=auto) · vix regime · hmm vol · regime ensemble (method=majority) · factor top tier · portfolio backtest (horizon=1y, rebalance=quarterly, selection=reselect) · portfolio forward autopsy · monte carlo (horizon=1y) | unchanged — carried forward |
| 7 | universe (preset=sp500) · price loader · factor loader · factor value · factor quality · factor momentum · factor growth · factor regime tilt · factor composite · regime gmm (n_regimes=auto) · vix regime · hmm vol · regime ensemble (method=majority) · factor top tier · portfolio backtest (horizon=1y, rebalance=quarterly, selection=reselect) · portfolio forward autopsy · monte carlo (horizon=1y) | unchanged — carried forward |
| 8 | universe (preset=sp500) · price loader · factor loader · factor value · factor quality · factor momentum · factor growth · factor regime tilt · factor composite · regime gmm (n_regimes=auto) · vix regime · hmm vol · regime ensemble (method=majority) · factor top tier · portfolio backtest (horizon=1y, rebalance=quarterly, selection=reselect) · portfolio forward autopsy · monte carlo (horizon=1y) | unchanged — carried forward |
The integrity of a walk-forward rests on registering each hypothesis before its out-of-sample window is scored. The order below is the order in which the hypotheses were sealed.
| # | Registered hypothesis | Anchor | Registered at |
|---|---|---|---|
| 1 | “S&P 500, rebalanced quarterly across the selected basket, conditioned on the wired regime classifiers, and run out-of-sample from the anchor — with no in-sample parameters to optimize, a walk-forward does not apply; its disposition is the realized forward path versus the benchmark, is expected to generate positive risk-adjusted returns over the forward test window.” | 2016-01-01 | 2026-07-22 |
| 2 | “S&P 500, rebalanced quarterly across the selected basket, conditioned on the wired regime classifiers, and run out-of-sample from the anchor — with no in-sample parameters to optimize, a walk-forward does not apply; its disposition is the realized forward path versus the benchmark, is expected to generate positive risk-adjusted returns over the forward test window.” | 2017-01-01 | 2026-07-22 |
| 3 | “S&P 500, rebalanced quarterly across the selected basket, conditioned on the wired regime classifiers, and run out-of-sample from the anchor — with no in-sample parameters to optimize, a walk-forward does not apply; its disposition is the realized forward path versus the benchmark, is expected to generate positive risk-adjusted returns over the forward test window.” | 2018-01-01 | 2026-07-22 |
| 4 | “S&P 500, rebalanced quarterly across the selected basket, conditioned on the wired regime classifiers, and run out-of-sample from the anchor — with no in-sample parameters to optimize, a walk-forward does not apply; its disposition is the realized forward path versus the benchmark, is expected to generate positive risk-adjusted returns over the forward test window.” | 2019-01-01 | 2026-07-22 |
| 5 | “S&P 500, rebalanced quarterly across the selected basket, conditioned on the wired regime classifiers, and run out-of-sample from the anchor — with no in-sample parameters to optimize, a walk-forward does not apply; its disposition is the realized forward path versus the benchmark, is expected to generate positive risk-adjusted returns over the forward test window.” | 2020-01-01 | 2026-07-22 |
| 6 | “S&P 500, rebalanced quarterly across the selected basket, conditioned on the wired regime classifiers, and run out-of-sample from the anchor — with no in-sample parameters to optimize, a walk-forward does not apply; its disposition is the realized forward path versus the benchmark, is expected to generate positive risk-adjusted returns over the forward test window.” | 2021-01-01 | 2026-07-22 |
| 7 | “S&P 500, rebalanced quarterly across the selected basket, conditioned on the wired regime classifiers, and run out-of-sample from the anchor — with no in-sample parameters to optimize, a walk-forward does not apply; its disposition is the realized forward path versus the benchmark, is expected to generate positive risk-adjusted returns over the forward test window.” | 2022-01-01 | 2026-07-22 |
| 8 | “S&P 500, rebalanced quarterly across the selected basket, conditioned on the wired regime classifiers, and run out-of-sample from the anchor — with no in-sample parameters to optimize, a walk-forward does not apply; its disposition is the realized forward path versus the benchmark, is expected to generate positive risk-adjusted returns over the forward test window.” | 2023-01-01 | 2026-07-22 |
A garden of forking paths (Gelman and Loken, 2013) is not cheating — it is what honest research feels like from the inside. At each step there were several defensible things to try, and trying them is the work. What separates research from a hunt for a flattering backtest is that the paths not taken are still counted. Here they are: an annualised Sharpe of 0.45 over the stitched walk, deflated to 36% once the 11 trials are accounted for. The strategy does not survive the multiple-testing correction — most likely an artifact of the search.
Pooled deflated Sharpe is the inferential number; the per-step DSR track is illustrative (near-identical variants are correlated, so per-step N over-deflates). N is a conservative upper bound on the multiple-testing penalty, frozen at compile time and taken as the larger of the lineage trial count and the explored-runs count so it cannot understate the search. Integrity rests on pre-registration: each step's submitted_at is the registration order, auditable against when its OOS was scored.
Each step is backed by a frozen run report. The study is re-derivable from the ledger below.
| # | Commit | Report | Anchor | OOS window |
|---|---|---|---|---|
| 1 | f5d34a004116 | 256 | 2016-01-01 | 2016-01-04 → 2016-12-30 |
| 2 | 007b403b1f75 | 257 | 2017-01-01 | 2017-01-03 → 2017-12-29 |
| 3 | 3e4d52220a7b | 258 | 2018-01-01 | 2018-01-02 → 2018-12-31 |
| 4 | 2c97fa4cd586 | 259 | 2019-01-01 | 2019-01-02 → 2019-12-31 |
| 5 | 0d446a9e665c | 260 | 2020-01-01 | 2020-01-02 → 2020-12-31 |
| 6 | b544e634397b | 262 | 2021-01-01 | 2021-01-04 → 2021-12-31 |
| 7 | bf99842d97c5 | 264 | 2022-01-01 | 2022-01-03 → 2022-12-30 |
| 8 | 9de35299dcf6 | 265 | 2023-01-01 | 2023-01-03 → 2023-12-29 |
What each step's run actually did beyond its return: capital allocation across lanes and regimes, the portfolio book's rebalancing and cost drag, and how positions were sized. Harvested from the frozen run reports — present where the circuit produced them.
Portfolio book — rebalanced quarterly · 4 constructions · 18 names held · selection: reselect · 0% in cash · turnover 0× · cost drag 0% · regime gate active
Portfolio book — rebalanced quarterly · 5 constructions · 16 names held · selection: reselect · 0% in cash · turnover 0× · cost drag 0% · regime gate active
Portfolio book — rebalanced quarterly · 5 constructions · 14 names held · selection: reselect · 0% in cash · turnover 0× · cost drag 0% · regime gate active
Portfolio book — rebalanced quarterly · 5 constructions · 11 names held · selection: reselect · 0% in cash · turnover 0× · cost drag 0% · regime gate active
Portfolio book — rebalanced quarterly · 4 constructions · 19 names held · selection: reselect · 0% in cash · turnover 0× · cost drag 0% · regime gate active
Portfolio book — rebalanced quarterly · 5 constructions · 19 names held · selection: reselect · 0% in cash · turnover 0× · cost drag 0% · regime gate active
Portfolio book — rebalanced quarterly · 5 constructions · 17 names held · selection: reselect · 0% in cash · turnover 0× · cost drag 0% · regime gate active
Portfolio book — rebalanced quarterly · 5 constructions · 19 names held · selection: reselect · 0% in cash · turnover 0× · cost drag 0% · regime gate active
Every primitive this study wired, with the mathematics it actually computes — the same formulas the execution engine runs, as documented in the platform's per-primitive "Explain the math". Nothing here is illustrative; it is the calculation.
The starting set of tickers — resolved point-in-time so there is no survivorship bias.
Before any math, you need a list of stocks. An index preset (S&P 500, Nasdaq-100, Dow 30) is reconstructed as it stood ON your anchor date by replaying the historical add/drop change-log backwards — so a 2018 backtest sees the 2018 membership, not today's winners.
Start from today's constituents and un-apply every membership change after the anchor t:
\mathcal{U}(t) = \mathcal{U}_{\text{now}} \;\ominus\; \{\text{adds after } t\} \;\oplus\; \{\text{drops after } t\}Bulk OHLCV fetch for the whole universe — point-in-time, no future bars.
Momentum, volatility, trend — every price-based metric needs history. This loads open/high/low/close/volume for all names in parallel, clipped so nothing after the anchor can leak in. The lookback window is derived automatically from the deepest metric you wired.
It loads exactly enough history for the hungriest downstream metric plus a warm-up buffer:
W = \max_k(\text{lookback}_k) + \text{buffer}, \qquad \text{bars} \le \text{anchor } tPoint-in-time fundamentals — never let the user see a number before the SEC did.
Loads ~23 fundamental metrics (valuation, quality, growth) per name, but with one inviolable rule: a financial statement becomes visible only on or after its SEC acceptedDate. A 2020 backtest sees only what was actually filed by 2020 — no look-ahead, ever.
\text{visible}(f, t) \iff \text{acceptedDate}(f) \le tValue — how cheap is the stock, cross-sectionally?
Blends cheapness metrics — P/E, P/B, P/S, EV/EBITDA (lower is better) plus free-cash-flow and earnings yields (higher is better). Each metric is z-scored across the universe, winsorized against outliers, weighted by the importance you set (Off/Low/Med/High), and averaged.
z_{i,k} = \pm\,\frac{x_{i,k} - \bar x_k}{s_k}\text{Value}_i = \frac{\sum_k \omega_k\,z_{i,k}}{\sum_k \omega_k}, \quad \omega \in \{0,\,0.5,\,1,\,2\}Quality — is this a strong, profitable, well-financed business?
Blends profitability (ROE, ROA, ROIC, margins) and balance-sheet strength (debt-to-equity inverted, current ratio, interest coverage). Same z-score, winsorize, importance-weight recipe as every factor family.
\text{Quality}_i = \frac{\sum_k \omega_k\,z_{i,k}}{\sum_k \omega_k}Momentum — recent price strength, no filing required.
Pure price-based: 1/3/6/12-month returns measured at the anchor. Because it needs no financial statement, it is always available — even for names with thin fundamentals.
\text{Mom}_i = \frac{\sum_k \omega_k\, z\!\big(R_{i}^{(k)}\big)}{\sum_k \omega_k}, \quad R^{(k)} = \frac{P_t}{P_{t-k}}-1Growth — is the business getting bigger, by the numbers it has filed?
Revenue growth, EPS growth, free-cash-flow growth and book-value-per-share growth, taken from the latest SEC-accepted filings (so growth is honest as-of the anchor). Same family recipe.
\text{Growth}_i = \frac{\sum_k \omega_k\,z_{i,k}}{\sum_k \omega_k}The radar — read the market and suggest which factors to lean on.
Four market signals vote on a recommended Value/Quality/Momentum/Growth weighting: the trend regime (momentum & growth in bull markets, quality & value in bear), the valuation spread, factor momentum, and cross-sectional dispersion. Their importances normalize to 100 and produce a tilt the Composite applies.
\mathbf{w} = 0.30\,\mathbf{R} + 0.25\,\mathbf{V} + 0.25\,\mathbf{M} + 0.20\,\mathbf{D}\text{calm}\to\text{bullish},\ \ \text{choppy}\to\text{sideways},\ \ \text{stressed}\to\text{bearish}The weighting console — blend Value, Quality, Momentum, Growth into one 0–100 score.
Where the four factor families become a single ranking. Each family score is standardized across the universe, blended with your slider weights (or the radar's suggested tilt), and min-max scaled to 0–100. Winsorizing tames outliers; z-score or percentile normalization is your choice.
z_{i,f} = \frac{x_{i,f} - \bar x_f}{s_f}\quad(\text{clipped at the 1st / 99th percentile})C_i = \sum_f W_f\,z_{i,f}, \qquad \text{score}_i = 100\cdot\frac{C_i - \min_j C_j}{\max_j C_j - \min_j C_j}Gaussian mixture — cluster days into regimes, count chosen by BIC.
Treats each day as a point in (return, volatility) space and fits a Gaussian mixture; the Bayesian Information Criterion picks how many regimes the data actually support. States are vol-sorted and short runs de-noised. The rigorous detector the Per-Regime and Regression optimizers were designed around.
p(\mathbf x) = \sum_{k=1}^{K}\pi_k\,\mathcal N(\mathbf x\mid\boldsymbol\mu_k,\boldsymbol\Sigma_k), \qquad K^* = \arg\min_K \text{BIC}(K)VIX thresholds — the simplest fear gauge.
Reads the VIX (or a wired series) and classifies by two cut-offs: low VIX is calm, high VIX is stressed, in between is choppy. Confidence scales with how far past the boundary you are.
\text{regime} = \begin{cases}\text{calm} & \text{VIX} < c\\[2pt]\text{stressed} & \text{VIX} \ge s\\[2pt]\text{choppy} & \text{otherwise}\end{cases}Hidden Markov model on volatility — regimes defined by turbulence.
Same HMM machinery, but the observed features are realized volatilities (short, long) and the VIX term-structure ratio. States are sorted by volatility: lowest = calm, highest = stressed.
\mathbf v_t = \big(\sigma_t^{\text{short}},\,\sigma_t^{\text{long}},\,\tfrac{\text{VIX}}{\text{VIX3M}}\big), \quad \mathbf v_t\mid(z_t=k)\sim\mathcal N(\boldsymbol\mu_k,\boldsymbol\Sigma_k)Combine several classifiers into one honest verdict.
Blends multiple regime classifiers into a single per-bar label by majority (equal vote) or explicit weights. Equal votes stay equal — a 3-way tie resolves to the middle severity, never to whichever was wired first.
\text{regime}_t = \operatorname{round}\!\Big(\frac{\sum_m \omega_m\,\text{sev}(c_{m,t})}{\sum_m \omega_m}\Big)The cut out of the factor lane — keep the top-ranked names.
Takes the composite-ranked factor set and keeps the best N, carrying the composite score, the four family scores and the point-in-time market cap for each survivor. Feed 10–20 to a direct portfolio, or 30–100 as an optimizer pool.
\{\, i : \operatorname{rank}(C_i) \le N\,\}, \quad C_i = \text{composite score}Replay the portfolio forward — rebalanced, point-in-time, with costs.
Holds the basket and rebalances on schedule, re-selecting and re-optimizing point-in-time at each rebalance (so it only ever uses information available then), and reports the equity curve, Sharpe, drawdown and trade stats — optionally net of cost and risk overlays.
E_t = E_{t-1}\big(1 + \mathbf w_{t}^{\top}\mathbf r_t - \text{costs}_t\big)\text{DD}_t = \frac{E_t}{\max_{\tau\le t}E_\tau} - 1, \qquad \text{MaxDD} = \min_t \text{DD}_tMonte Carlo — simulate thousands of futures for the portfolio.
Estimate the portfolio's drift and covariance, then roll it forward thousands of times under geometric Brownian motion. The spread of outcomes gives the probability of doubling, of a big loss, and the P5–P95 cone — the honest range around any single backtested path.
S_{t+\Delta t} = S_t\,\exp\!\Big[\big(\mu - \tfrac12\sigma^2\big)\Delta t + \sigma\sqrt{\Delta t}\;Z\Big],\quad Z\sim\mathcal N(0,1)P(\text{double}),\ P(\text{+50\%}),\ P(\text{loss}),\ P(\text{−25\%}) = \frac{\#\{\text{paths in region}\}}{\#\text{paths}}This paper was produced end-to-end in QuanterLab: the pre-registration, the sealed walk, the point-in-time data, the statistics and the document you are reading. The platform is in closed beta.
Request access More research RSS