Files
signal-platform/docs/research/residual-normalization-findings.md
T
dennisthiessenandClaude Opus 5.5 231372fb3a
Deploy / lint (push) Successful in 14s
Deploy / test (push) Successful in 2m0s
Deploy / deploy (push) Successful in 48s
fix(backtest): address the second review of the breakeven and book-config work
- The breakeven study grades at the shadow book's capacity AND risk per
  trade, and labels each with where it came from. A missing setting row
  now reads "shadow_book default" instead of crediting the setting; the
  committed report said shadow_book_capacity though the snapshot has no
  such row.
- _production_book_config reads strategies through the same
  _portfolio_monitor_strategies() hook as the monitor.
- breakeven_gate_reset is rejected when there is no post-stop episode or
  cooldown for it to start, and the study skips its diagnostic arm for a
  book without one, so a production config change cannot abort the run.
- identical_to_control is renamed ties_control_on_graded_metrics and needs
  real values: be_2r ties on every graded metric but still changed 2 trades.
- _BookConfig drops an unused field and hashes by identity; the min-R:R
  sweep swaps its gate in with dataclasses.replace instead of passing it.
- New tests cover the parity guard's success path, the no-config fallback
  and the gate-reset check.

The breakeven report is regenerated: every row, diagnostic row and the
parity check are identical; only the labels, the risk fields and the
renamed flag changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 21:41:44 +02:00

81 lines
4.7 KiB
Markdown

# Residual normalization — findings
**2026-09-08: closed negative at the signal screen. Keep production unchanged.**
The proposed normalization did not meet the rule frozen in
[the experiment protocol](residual-normalization.md), and its point estimates
did not improve on the current momentum signal. No portfolio replay was run:
the protocol requires a passing signal screen before that stage.
The candidate used the production SPY-adjusted 12-1 return sum divided by sample
standard deviation of the same 231 daily adjusted returns. All comparisons used
identical as-of dates and 30-SPY-session forward-return endpoints. No new factor,
lookback, volatility weight, sizing rule, or exit was tested.
| Era | Non-overlapping windows | Current signal, matched coverage | Normalized signal | Difference |
|---|---:|---:|---:|---:|
| Full period | 73 | 0.01446 | 0.01284 | -0.00162 |
| Before 2021 | 30 | 0.00776 | 0.00496 | -0.00280 |
| From 2021 | 42 | 0.02377 | 0.02259 | -0.00118 |
Values are mean **Spearman information coefficients (IC)**: how well signal
ordering agrees with subsequent stock returns. They are neither Sharpe ratios
nor trading returns. Higher is better for this positive-momentum hypothesis.
The candidate needed full-period IC >= 0.03, at least 12 windows, and positive
mean IC in both eras. It passed the count and era-sign conditions but failed the
0.03 threshold. The rule was evaluated before rounding.
The paired normalized-minus-current IC difference has a 95% moving-block interval
of **[-0.00496, +0.00156]**. This spans zero: the study does not establish that
normalization is worse, and it supplies no convincing improvement evidence.
The mean top-minus-bottom-quintile 30-session return spread also fell, from
**0.967% to 0.751%**. That is an unconditional diagnostic, not the P&L of the
long-only, gated, capital-constrained production book.
**Coverage and checks**
- The snapshot completion manifest matches 4,650 registry tickers, 5,081,073
OHLCV rows, and 4,144 research-only names. The experiment used the remaining
506 production-universe registry names, averaging 485.4 paired stocks per
retained window.
- Actual retained as-of dates run from **2017-01-06 to 2026-05-01**; the final
forward endpoint is **2026-06-15**. Later dates cannot necessarily add another
complete non-overlapping 30-session window. SPY history starts in January
2016, so a full one-year signal cannot start in July 2016.
- One retained window crosses the 2021 boundary and belongs to the full-period
result only, explaining why the era counts sum to 72 rather than 73.
- The unrestricted production control had **35,441** retained stock/date cells;
paired control and treatment each had **35,433**. Its mean IC was 0.01447,
essentially the same as paired control. Coverage restriction did not account
for the observed result.
- Numerator parity was checked against the production implementation on each
eligible evaluated cell. The snapshot and frozen source hashes were unchanged
across the run. SQLite was opened read-only.
- Twelve targeted tests passed, including numerical invariance, production
parity, lookahead exclusion, zero-variance handling, missing-data pairing,
calendar overlap, and an end-to-end screen check. Ruff passed for the runner
and its tests. The existing Python runtime was usable outside the filesystem
sandbox; no environment replacement was needed.
- A 2026-10-10 code review added four tests (missing prices, file release,
paired-coverage guard, source ids) with the runner fixes they cover. The
report was regenerated with the fixed runner; its screen results are
byte-identical to the original run.
**Interpretation and decision**
My proposed mechanism was plausible but unsupported by this experiment. Do not
add normalization to production or search denominator variants on this history.
This ends this specific experiment, not all possible residual-momentum research.
The current signal also falls below 0.03 on this particular long-history screen.
That cautions against treating its predictive evidence as universally strong;
it does not by itself measure or invalidate the complete production book. The
unconditional universe, true session spacing, dates, and return objective differ
from earlier setup/portfolio studies. Historical constituents are still projected
backward, this history has already been inspected, and the bootstrap intervals
remain exploratory. No new CAGR, drawdown, cost-stress, or live result is claimed.
**Artifacts:** [machine-readable results](../../reports/residual-normalization-20260908.json),
[generated tables](../../reports/residual-normalization-20260908.md),
[runner](../../scripts/run_residual_normalization.py), and
[tests](../../tests/unit/test_residual_normalization.py).