Files
signal-platform/docs/research/residual-normalization.md
dennisthiessenandClaude Opus 5.5 7a9b3ad38b research: volatility-standardized residual momentum screen, failed
Strategy review (2026-09-08) and a frozen, paired signal screen of
residual 12-1 momentum divided by its formation-window residual
volatility. Across 73 non-overlapping windows the normalized signal's IC
is 0.0128 against 0.0145 for the matched control, below the frozen 0.03
floor, so the conditional portfolio replay was not run and nothing in
production changes.

Code-review fixes to the runner, before first commit: a missing OHLC
price stops the run instead of reading as 0; the snapshot connection is
closed explicitly (on Windows it otherwise kept the file locked); the
paired-coverage check raises instead of asserting, so python -O cannot
strip it; and source files are recorded as git blob ids with line
endings normalized, so the report can be checked against history. The
report was regenerated with the fixed runner; its screen section is
byte-identical to the original run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:45:24 +02:00

88 lines
5.3 KiB
Markdown

# Residual normalization experiment
Protocol frozen 2026-09-08 before computing either signal's results. Implements
the first candidate in [the strategy review](strategy-review-2026-09-08.md).
This is offline research; it does not change production selection or settings.
**Question:** does dividing the current SPY-adjusted 12-1 score by its own daily
residual standard deviation improve predictive ordering and portfolio economics?
**Signal:** use the existing 252-bar lookback, skip 21 bars, estimate SPY beta
with centered covariance/variance, and retain the fitted intercept's contribution
exactly as production does. The treatment divides the production sum by sample
standard deviation (`ddof=1`) over the same 231 adjusted daily returns. No
denominator clipping, alternate window, factor addition, or weight sweep.
An exactly market-tracking series can leave machine-rounding residuals; sample
standard deviation <= 64 machine epsilons times `max(1, max(abs(stock_returns)))`
is treated as numerical zero, not divided into the signal.
**Data:** the completed `backtest_snapshots/research.sqlite`, restricted to
tickers absent from `research_rank_only` (506 production-universe registry names).
Nominal as-of start 2016-07-01; end bounded by actual SPY history and availability
of a 30-SPY-session forward endpoint. This snapshot starts SPY in January 2016,
so a fully aligned one-year signal cannot start in July 2016. Report actual
signal and forward-return dates. All history is previously inspected and
survivorship-biased; none is labeled fresh out-of-sample.
Require exactly 231 return pairs over consecutive benchmark sessions in the
formation window, positive finite prices, and nonzero residual variance for
both paired arms. A separate unrestricted production control retains production's
minimum-100-pair behavior. Report the removed population. Forward returns use
the same exact stock endpoint dates 30 SPY sessions apart, without filling absent
prices, without subtracting SPY, and without changing the stored return basis.
**Stage 1 — signal screen:** consider the last SPY session of each ISO week.
Keep a date with at least 20 common eligible stocks only if its as-of session is
at or after the preceding retained forward endpoint. Thus successive intervals
share at most an endpoint, not a return, including around holidays and year ends.
Use those dates for all three arms. Report per-window Spearman IC, top-minus-bottom
quintile forward-return spread, cross-sectional coverage, and paired IC deltas.
Era summaries use windows ending before 2021-01-01 and windows starting on/after
2021-01-01; boundary-crossing windows remain in full-period results only.
The screen passes only if the treatment has full-period mean IC >= 0.03, at
least 12 non-overlapping full-period windows, and strictly positive mean IC in
both eras. Mean IC and signs are checked before rounding. No additional paired
delta threshold is silently added. Report uncertainty whether the screen passes
or fails. For IC intervals use paired moving blocks of four retained windows,
5,000 replicates, NumPy PCG64 seed 20260908. These are exploratory intervals;
non-overlap alone does not establish statistical independence.
**Stage 2 — conditional portfolio comparison:** run only if stage 1 passes.
Replay daily, with unrestricted control, common-coverage control, and normalized
treatment; rank the full available production universe once per ticker/date.
Use unchanged 80/20 momentum/6-month-volatility ordering, top-20% momentum gate,
GTL qualification, capacity 15, 1% fixed-fractional risk, 20% notional cap,
cash constraints, 1.5 ATR initial stop, 3 ATR trail, 30-session hold, no
take-profit, and independent gate-reset histories. Keep the volatility rank
universe and its values identical between the paired arms. Use the snapshot's
non-momentum configuration and assert it agrees with the documented production
floors before running. Keep all non-qualified scans for gate-reset observations.
Use close fills as the historical execution scenario and costs 10 and 20 bps per
side. On a common calendar report paired returns, trade overlap, exposure,
turnover, risk per fill, and winning/losing contributions. Advancement requires
positive paired Sharpe improvement, no lower point-estimate CAGR, no deeper
point-estimate max drawdown, and a positive lower 95% bootstrap bound for Sharpe
improvement at both costs and both 21-/63-session block lengths. Use 5,000
paired replicates and seed 20260908. Bootstrap the realized return streams;
this does not model counterfactual changes in decisions along resampled prices.
A failed screen ends this experiment. A portfolio pass would justify forward
research, not automatic promotion. No historical variation or rule relaxation
will be chosen after seeing this result. Point-in-time membership and actual
execution evidence remain separate requirements for deployment.
**Reproduce stage 1:**
```powershell
.venv\Scripts\python.exe scripts/run_residual_normalization.py
.venv\Scripts\python.exe -m pytest tests/unit/test_residual_normalization.py -q
```
The runner reads SQLite in read-only mode, validates the manifest, records
snapshot/protocol/code hashes and package versions, checks production-numerator
parity on the evaluated cells, and verifies the snapshot hash again before
writing completed results. Errors must not become skipped tickers or partial
successful reports.