Skip to content

feat(alpha): P3-2 walk-forward IC-weighted alpha (RollingICWeightAlpha) - #11

Merged
StackOverFlow11 merged 1 commit into
mainfrom
p3-ic-weighted-alpha
Jun 10, 2026
Merged

feat(alpha): P3-2 walk-forward IC-weighted alpha (RollingICWeightAlpha)#11
StackOverFlow11 merged 1 commit into
mainfrom
p3-ic-weighted-alpha

Conversation

@StackOverFlow11

Copy link
Copy Markdown
Owner

Summary

P3-2 — walk-forward IC-weighted alpha. Adds the first LEARNED factor combination (alpha/ic_weight.py::RollingICWeightAlpha, alpha.model: ic_weighted) without touching the factor / portfolio / runtime boundaries. EqualWeightAlpha stays the default and the regression baseline. No parameter search; NOT a performance claim.

Implementation

  • Model: at each scored date d, factor weights = mean per-factor cross-sectional rank IC over a trailing window of realized observations; L1-normalized, sign-preserving (a negative-IC factor gets a negative weight). Window modes: rolling (trailing window trading days — conservative default, 60d / min_periods 20) or expanding.
  • Wiring: _build_alpha dispatches by alpha.model (now a validated Literal); the IC horizon is tied to the first configured forward-return period, so training and the reported IC share one definition. alpha_summary + the full per-date effective-weights log land on Phase0Result / Phase2Result; "Alpha model" is a required report section; combo-score labels name the active alpha.
  • Config: config/phase3_real_ic_weighted.yaml is identical to phase3_real_multifactor.yaml except alpha.model — the two real baselines are directly comparable.

Lookahead boundary (the point of this PR)

  • Walk-forward training: a (factor[t], fwd_h[t]) pair enters date d's weights only once REALIZEDt + h <= d in trading-day positions (the h-day forward return of factor date t realizes at t+h). The full IC series is stored, but consumption is strictly the realized-at-d slice.
  • Locked by tests: a perturbation test rewrites every not-yet-realized forward return and asserts the weights at d are bit-identical; a boundary test pins the exact t + h cutoff (min_periods straddling 6-vs-7 available rows).
  • Layer boundary unchanged: forward returns are computed at the alpha boundary (_alpha_forward_returns) and handed ONLY to alpha.fit. The factor layer never sees them (invariant feat: Phase 0 MVP + Phase 1 bias-boundary (PIT · ann_date · tradability · neutralization) #1).

Fallback contract

Any factor with < min_periods valid realized ICs in the window (or degenerate all-zero/NaN ICs) → that date falls back to the equal-weight mean (bitwise identical to EqualWeightAlpha), is logged, and the report disclosed: training coverage, fallback count, and the per-rebalance effective weights table with fallback rows flagged.

Real-data results (manual, not in CI; SSE50 2023-07 ~ 2024-06, same window/factors/neutralization)

Run annual maxDD sharpe turnover
single-factor momentum (P2 baseline) −10.19%
multi-factor equal-weight (regression rerun, unchanged) −9.05% −16.17% −0.59 0.85
multi-factor ic_weighted (this PR) −3.57% −12.93% −0.19 0.92
  • Training coverage 201/221 scored dates (20 equal-weight fallbacks, all before the 60-day window filled; first rebalance 2023-07-31 is a flagged fallback row).
  • ⚠️ Honest read: beating equal weight on ONE year is not evidence of skill — the per-rebalance weights flip sign repeatedly (momentum_20: −0.58 → +0.36; roe: −0.77 → +0.27), which is exactly what small-sample IC instability looks like. The report says so; out-of-sample / IC-stability checks are the roadmap's next step.
  • momentum_20 IC 0.0083 unchanged across all runs (same factor, same window).

Quality gates

Gate Result
pytest -p no:cacheprovider 269 passed (was 249: +12 alpha unit, +6 wiring, +2 report)
ruff check . clean
validate-config ×5 (incl. phase3_real_ic_weighted) OK
run-phase0 demo (equal-weight default) ic 0.96 / annual 0.84 unchanged (test-locked)
phase3 equal-weight real rerun −9.05% / 0.0083 unchanged
phase3 ic_weighted real run results above
Hygiene no token, artifacts git-ignored, no conflict markers

Tests (network-free)

test_ic_weight_alpha.py (12): future-perturbation invariance, exact t+h cutoff, equal-weight fallback equivalence, single-factor ±1 degeneration, L1+sign stability, degenerate-IC fallback, rolling vs expanding, fit-requires-forward-returns, dated-cross-section contract, immutability, weights/fallback log. test_ic_alpha_pipeline.py (6): config dispatch + unknown-model ConfigError, equal-weight demo numbers locked, ic_weighted demo e2e disclosure + no-secret, ic-vs-equal divergence on opposed-rank synthetic factors. test_phase2_baseline.py (+2): report shows model/weights/fallback for both models.

Docs

RUNBOOK (P3-2 section), TEST_REPORT (269 + breakdown), BIAS_AUDIT (walk-forward alpha boundary added to the 未来函数 section, regenerated from the renderer), CLAUDE.md / AGENTS.md progress.

Adds the first LEARNED factor combination while keeping the lookahead
boundary auditable. EqualWeightAlpha stays the default and the regression
baseline; alpha.model: ic_weighted opts into walk-forward rolling-IC weights.
No parameter search; not a performance claim.

Lookahead boundary (locked by tests):
- Training is walk-forward: at each date d, a (factor[t], fwd_h[t]) pair is
  admissible only once REALIZED -- t + h <= d in trading-day positions (the
  h-day forward return of factor date t realizes at t+h). A perturbation
  test proves unrealized forward returns cannot change any date's weights;
  a boundary test locks the exact t+h cutoff via min_periods.
- Forward returns are computed at the alpha boundary and handed ONLY to
  alpha.fit; the factor layer never sees them (invariant #1 unchanged).

Model:
- weights = mean per-factor cross-sectional rank IC over the trailing
  window of realized observations (rolling, conservative default; expanding
  optional), L1-normalized and SIGN-PRESERVING (negative-IC factor gets a
  negative weight).
- Fallback: any factor with < min_periods valid realized ICs (or degenerate
  ICs) -> that date uses the EQUAL-WEIGHT mean (bitwise EqualWeightAlpha)
  and is logged; the report discloses coverage + per-rebalance effective
  weights (fallback rows flagged).

Wiring:
- _build_alpha dispatch (alpha.model now a validated Literal); horizon tied
  to the first forward-return period; alpha_summary/alpha_weights on
  Phase0Result/Phase2Result; 'Alpha model' is a REQUIRED report section;
  combo-score labels name the active alpha.
- config/phase3_real_ic_weighted.yaml: identical to phase3_real_multifactor
  except alpha.model (directly comparable).

Real runs (not in CI):
- Equal-weight phase3 regression rerun: annual -9.05% / IC 0.0083 unchanged.
- ic_weighted: annual -3.57%, maxDD -12.93%, training coverage 201/221
  (20 fallbacks, all before the window filled). Beating equal weight here is
  NOT a performance claim: one-year window + per-period weight sign flips
  (e.g. momentum_20 -0.58 -> +0.36) show small-sample instability, disclosed.

Tests: 269 passed (was 249): +12 alpha unit (incl. perturbation + cutoff
boundary), +6 pipeline wiring, +2 report disclosure. Demo equal-weight
numbers locked unchanged (ic 0.96 / annual 0.84).
@StackOverFlow11
StackOverFlow11 merged commit 61df36d into main Jun 10, 2026
StackOverFlow11 added a commit that referenced this pull request Jun 10, 2026
…er review

Doc-only sync in CLAUDE.md + AGENTS.md (both files, same drift):
- merged-PR lists: add #10 (progress docs) and #11 (P3-2).
- P3-2 entry: 'branch, awaiting acceptance' -> 'PR #11 merged to main'.
- P3-3 entry: 'branch, awaiting acceptance' -> 'PR #12 OPEN, review HIGHs+LOW
  fixed, awaiting acceptance/merge'.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant