Rejected thesis
Turnaround layer: portfolio backtest (4 universe / weight cells)
Question: Does the new turnaround layer added to judgment.py
(default weights 0.10 medium / 0.20 long) actually move the
portfolio? Run side-by-side walk-forward backtests with the
turnaround weight ON vs OFF, all other layers identical.
Code:
- research/turnarounds.py::historical_panel — vectorised
point-in-time score panel (date × symbol) consumed by the harness.
- backtest/harness.py::_build_panels adds a "turnaround" panel
keyed turnaround_v1 in the score-cache.
- backtest/harness.py::_judge_from_panels accepts turnaround_row
and forwards it to judgment.judge().
- scripts/compare_turnaround_overlay.py runs the A/B comparison
using horizon_weights override (turnaround row zeroed = OFF;
default = ON).
Setup: weekly rebalance, n_top=10, default cost/sizing/sector
config, 30-day EWM panel smoothing — i.e. production settings.
Default weights = DEFAULT_HORIZON_WEIGHTS_BACKTEST with the
turnaround row at 0.10 (medium) / 0.20 (long). Heavy weights =
0.40 (medium) / 0.80 (long), i.e. 4× default.
Headline numbers:
| Universe | Window | Weight | Sharpe | CAGR | Vol | MDD | ΔSharpe |
|---|---|---|---|---|---|---|---|
| nasdaq100 | 2014→2026 | OFF | 1.41 | +45.49 % | 29.92 % | -37.92 % | — |
| nasdaq100 | 2014→2026 | ON | 1.41 | +45.81 % | 29.92 % | -37.73 % | +0.007 |
| sp500 | 2014→2026 | OFF | 1.46 | +51.18 % | 31.83 % | -43.83 % | — |
| sp500 | 2014→2026 | ON | 1.46 | +51.12 % | 31.81 % | -44.54 % | -0.001 |
| sp500 | 2014→2026 | ON 4× | 1.54 | +53.43 % | 31.05 % | -40.71 % | +0.076 |
| sp500 | 2020→2026 | OFF | 1.76 | +80.51 % | 37.63 % | -43.83 % | — |
| sp500 | 2020→2026 | ON | 1.76 | +80.19 % | 37.57 % | -44.54 % | -0.002 |
| sp600 | 2014→2026 | OFF | 0.86 | +28.72 % | 37.39 % | -51.53 % | — |
| sp600 | 2014→2026 | ON | 0.87 | +28.88 % | 37.54 % | -51.77 % | +0.001 |
| sp600 | 2014→2026 | ON 4× | 0.94 | +32.41 % | 37.07 % | -53.62 % | +0.081 |
Three findings:
-
At the shipped default weight (0.10 / 0.20), the turnaround layer is essentially NEUTRAL at the portfolio level across all four (universe × window) combinations. Sharpe lifts ±0.01, CAGR lifts ±0.3 %, MDD ±0.7 %. The layer does not break anything but does not add measurable portfolio alpha either.
-
At 4× weight (0.40 / 0.80), the layer DOES carry signal:
- sp500 12yr: +0.076 Sharpe, +2.25 pp CAGR, -3.12 pp MDD
- sp600 12yr: +0.081 Sharpe, +3.69 pp CAGR (drawdown worse +2.09 pp) Both universes show the same Sharpe magnitude (~+0.08) — the signal is real and scales with weight.
-
Why default is too small: with n_top=10 weekly, the portfolio is highly concentrated. The default turnaround weight fractionally re-ranks names but doesn't break a turnaround candidate into the top-10 in any given week. At 4× weight, turnaround candidates start displacing other top-N names; the validated single-name edge (+11–59 % over 250d on D≥30/V≥1.3 to D≥50/V≥2 cells) finally expresses at the portfolio level.
Bootstrap caveat: Sharpe lift +0.08 is within the bootstrap CI
width (~±0.30 per the docstring reference). The DIRECTION is
consistent across both universes (which is encouraging), but a
paired-bootstrap (scripts/bootstrap_recent_abs.py) would be needed
to confirm the lift survives sample-path noise.
Verdict:
- The layer carries real but modest signal at appropriately heavier
weights. The shipped default (0.10 / 0.20) under-expresses it.
- Recommended next step: re-calibrate defaults to 0.20 / 0.40
(mid-point between current and heavy) and re-test with a
paired-bootstrap. Worth one more A/B cycle before declaring a
new shipped default.
- The /turnarounds page value is independent of the layer
weight — it shows the specific names the cohort fires on. That's
where the signal is most visible (single-name level), even if the
aggregate portfolio Sharpe barely moves at default weight.
Meta-finding: This is the second time a validated single-name
signal has under-expressed at the portfolio level due to top-N
concentration (the first being accumulation_validate, marginal at
+0.4-0.9 pp/250d but never showed up in portfolio backtests). The
pattern is consistent — multi-quarter single-name alpha gets diluted
in a concentrated portfolio. To express it, either:
(a) raise the layer weight to the point where it routinely
changes top-N membership (the 4× test);
(b) reduce portfolio concentration (e.g. n_top=20+); or
(c) productize the signal as a separate sub-portfolio /
watch-list (the /turnarounds page).
| Verdict | Rejected |
|---|
Every result here is reproducible from the script named above. Reports are in the repository.
- Volatility squeeze
- Pre-FOMC drift
- Uptrend pullback
- Momentum continuation
- Industry-relative momentum (at 4-week lookback)
- Earnings-reaction reversal (anti-PEAD)
- Beat-persistence pre-earnings drift
- Sector ETF mean-reversion
- 52-week-high breakout
- Volume accumulation (above-200DMA +...
- Cross-sector momentum
- Analyst-upgrade cluster