Rejected thesis
Per-layer weight calibration sweep
Question: For each (layer, horizon) cell in judgment.DEFAULT_HORIZON_WEIGHTS_BACKTEST, what weight maximises portfolio Sharpe? Are the shipped defaults optimal?
Methodology: Three stages:
- Marginal sweep —
scripts/sweep_layer_weights.pyruns 8 layers × 3 horizons × 7 weights = 168 backtests. For each cell, all other weights held at default. Reveals each cell's marginal Sharpe response curve. Misses interactions by construction. - Joint refinement — stack the top marginal winners into candidate weight matrices, test, pick the best.
- Anti-overfit validation — winner re-tested on nasdaq100 and sp600 (in-sample + OOS 2020+).
Setup: weekly rebalance, n_top=10, full production settings. sp500 2014-2026 = primary universe.
Stage 1 — marginal sweep (top winners by ΔSharpe vs baseline)
| Layer | Horizon | Default w | Best w | ΔSharpe |
|---|---|---|---|---|
| sentiment | medium | 0.15 | 0.35 | +0.136 |
| sentiment | long | 0.15 | 0.60 | +0.133 |
| sentiment | short | 0.15 | 0.60 | +0.116 |
| sector | long | 0.65 | 0.20 | +0.100 |
| sector | medium | 0.55 | 0.20 | +0.092 |
| sector | short | 0.45 | 0.20 | +0.086 |
| technical | long | 0.10 | 0.60 | +0.079 |
| turnaround | short | 0.00 | 0.60 | +0.070 |
| technical | short | 0.10 | 0.35 | +0.064 |
| ... | ... | ... | ... | ... |
Sentiment dominates; sector is meaningfully over-weighted at every horizon; news shows zero impact (as expected — news layer is zero in backtest config because yfinance can't backdate it).
Stage 2 — joint candidates DO NOT compose
Naive stacking of marginal argmaxes is catastrophic:
| Candidate | ΔSharpe |
|---|---|
| A: sentiment 0.35 all-horizons | +0.098 |
| B: sector 0.20 all | -0.019 |
| C: A + B (sentiment + sector cut) | -0.338 |
| D: C + technical 0.30 | -0.570 |
| E: D + turnaround/insider bumps | -0.460 |
| F: full argmax stack | -0.746 |
| G: conservative tilt | -0.236 |
Only candidate A (sentiment bump alone) beats baseline. Every other change either is null or actively destroys performance. Cutting sector — which the marginal sweep said should lift Sharpe by +0.10 — actually loses -0.34 when combined with the sentiment bump. The sector and sentiment layers carry overlapping information once both are at higher weights.
Stage 3 — refinement found a better magnitude
Sweeping the sentiment bump magnitude:
| Sentiment weight (all-horizons) | Sharpe | ΔSharpe |
|---|---|---|
| 0.15 (default) | 1.461 | — |
| 0.20 | 1.571 | +0.109 |
| 0.25 | 1.582 | +0.120 |
| 0.30 | 1.570 | +0.109 |
| 0.35 | 1.560 | +0.098 |
| 0.40 | 1.462 | +0.000 |
| 0.50 | 1.522 | +0.060 |
| 0.70 | 1.299 | -0.162 |
Optimum is 0.25, not 0.35. Response is non-monotonic — 0.40 breaks the lift, 0.70 destroys it. Per-horizon variation (0.30/0.35/0.45 etc) does no better than the uniform 0.25 setting.
Anti-overfit validation (sentiment 0.25 vs baseline)
| Cell | ΔSharpe | ΔCAGR | ΔMDD |
|---|---|---|---|
| sp500 12yr (in-sample) | +0.120 | +3.70 % | +4.65 % |
| sp500 OOS 2020+ | +0.122 | +4.60 % | +4.65 % |
| nasdaq100 12yr | +0.008 | -0.83 % | -5.23 % |
| nasdaq100 OOS 2020+ | +0.084 | +2.99 % | -5.23 % |
| sp600 12yr | +0.009 | -0.08 % | -1.70 % |
| sp600 OOS 2020+ | -0.036 | -2.61 % | -1.70 % |
5/6 cells positive. In-sample +0.120 ≈ OOS +0.122 on sp500 — exceptional consistency. Nasdaq100/sp600 marginal-to-flat; no catastrophic regression on any cell.
Decision
Bump DEFAULT_HORIZON_WEIGHTS_BACKTEST sentiment row from
0.15/0.15/0.15 → 0.25/0.25/0.25. Live DEFAULT_HORIZON_WEIGHTS
unchanged — live includes fundamental/news (which work live but not
in backtest) so the calibration doesn't transfer cleanly. Live
recalibration is a separate workstream that needs a different
evaluation framework.
Two important methodology notes
- Marginal sweeps lie about joint optima. Stacking the 24-cell argmaxes produced the WORST Sharpe (-0.746) of any candidate tested. Per-cell optima don't compose because layers carry overlapping information that re-weights perturb non-linearly. Coordinate descent (or full Bayesian opt) is the right tool; marginal scan is a first pass to identify candidates.
- Bootstrap CIs are wide. +0.12 Sharpe is comfortably outside
the cross-section noise of single backtests but still within
the ~±0.30 paired-bootstrap CI for sample-path variability per
the harness docstring. Treat as "probable improvement, not
certain".
scripts/bootstrap_recent_abs.pycould confirm.
Reports written:
- research/layer_weight_sweep_sp500_12yr.json (168-cell raw output)
Meta-finding: The shipped backtest defaults had sentiment too low and sector too high in isolation — but cutting sector breaks the ensemble. The robust win is a single-layer tweak (sentiment ↑) with everything else left alone. Future weight recalibration should use coordinate descent or Bayesian optimization rather than naive argmax stacking.
| 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