Skip to content

feat: factor evaluation contract layer (cdd) + StandardFactorEvaluator + research dashboard + PR-C jump-amount-corr reproduction - #63

Merged
StackOverFlow11 merged 4 commits into
mainfrom
feat/pr-c-jump-amount-corr
Jul 19, 2026
Merged

feat: factor evaluation contract layer (cdd) + StandardFactorEvaluator + research dashboard + PR-C jump-amount-corr reproduction#63
StackOverFlow11 merged 4 commits into
mainfrom
feat/pr-c-jump-amount-corr

Conversation

@StackOverFlow11

Copy link
Copy Markdown
Owner

Summary

4 commits, one stacked feature chain — the contract-driven (cdd) factor-evaluation layer, iterated v0.1→v0.7 and closed end-to-end by reproducing a real research-report factor through it.

  1. feat(factors): factor evaluation contract layerFactorSpec (mandatory Factor classattr, pre-registered expected_ic_sign, PIT contract, intraday block validation) + FactorEvaluator ABC (8 mandatory sections, template method, missing-section raises) + FactorEvalReport (deterministic md/json, secret-redacting renderer) + three-axis verdict (Predictive × Incremental × Tradable → Adopt/Watch/Reject/INSUFFICIENT-DATA; asymmetric gate — hard Rejects precede the sample gate; unknown never convicts) + pre-registered success criteria (EvalConfig.success_criteria, criteria_source stamped) + N_eff-based CIs (PASS gates on the lower CI bound in the expected direction). v0.7: separate min_incremental_abs_icir=0.15 (orthogonalization shrinks the residual scale — reusing the raw 0.30 bar was a category error) + monotonicity as a direction gate (default 0.0 = aligned strictly > 0, not a strength bar; the old 0.8 bar rejected tail-concentrated real factors). Both defaults remain UNVALIDATED-BY-DATA (design §11).
  2. feat(analytics): vectorized factor eval-IR + StandardFactorEvaluator — one vectorized IR build (IC via grouped reductions, equal-count quantile buckets, NW-t, N_eff = N/(1+2Σρ) with Geyer extension, ICIR/mean/spread CIs) consumed by all 8 sections; equivalence vs naive per-period loops test-locked.
  3. feat(analytics): factor evaluation dashboard — research-style one-PNG dashboard (layered quantile NAV + dark-maroon long-short on a secondary axis, per-period & cumulative IC, monotonicity/OOS/decay/purity/cost/coverage panels) with a mandatory FACTOR DEFINITION band sourced from FactorSpec — a factor may not be shown without stating how it is computed. evaluate_with_ir() mirrors the frozen ABC template (report byte-identical, test-locked); figures NOT re-exported from __init__ (no matplotlib on package import).
  4. feat(factors): reproduce jump-amount-corr factor (PR-C) — the Kaiyuan report §6 "price-jump turnover correlation" factor as a first-class Factor (spec sign −1 pre-registered from the report), evaluated on REAL cached CSI500 (2021-07→2026-06, daily, cache-only, stk_mins_live_calls=0), no-book + with-book (value_ep/value_bp/volatility_20). Result: Watch / Watch — predictive PASS (ICIR −0.401, NW-t −14.8, lower CI −0.342 clears 0.30, OOS sign consistent), incremental PASS (orth ICIR −0.290, lower CI 0.229 > 0.15), tradable NOT_ASSESSED + exploratory cap. EXPLORATORY — not a return claim.

Test plan

  • pytest 1238 passed (contract 317 + standard/IR + figures 7 + runner 11 + all prior suites; phase0 demo ic 0.9600 / annual 0.8408 unchanged)
  • ruff clean on the full changed set
  • Frozen contract files byte-identical across all stacked branches
  • Real-run gates: verdict reproduced end-to-end twice (392s/394s cache-only, covered 995/996); dashboard rendered from the integrated path
  • Secret scan: 0 hits (token value / .config.json) in additions, reports, logs, dashboards

…or ABC + report + three-axis verdict + pre-registered CI criteria)

Three independent axis-verdicts (Predictive / Incremental / Tradable), each
PASS/FAIL/INSUFFICIENT_DATA/NOT_ASSESSED, plus a derived deployment label
(Adopt/Watch/Reject/INSUFFICIENT-DATA). A factor that re-expresses a known factor
FAILs the Incremental axis and is Rejected where a scalar raw-IC verdict passed it.

Pre-registered success criteria + confidence intervals (change #3):
- EvalConfig.success_criteria: a frozen VerdictThresholds declared BEFORE evaluate()
  runs is pre-registered by construction. None -> the documented global default.
  The report stamps criteria_source ('declared' | 'default') in the provenance box
  and JSON, so the bar cannot be tuned post-hoc without a visibly different report.
  The per-run object passes exactly the same VerdictThresholds validation.
- The magnitude PASS test (ICIR, incremental ICIR) now gates on the N_eff-based
  LOWER confidence bound in the expected direction, not the naked point, via
  _clears_magnitude. A thin/noisy estimate -> wide CI -> low lower bound -> not a
  PASS. This does NOT duplicate the sample gate: the gate is the 'can we estimate a
  CI at all?' precondition (INSUFFICIENT below it); the lower-CI test is the 'is it
  convincingly above the bar?' PASS test above it. FAIL stays POINT-based and
  known-fact-only; a NaN CI bound never convicts and never PASSes.
- config.py imports VerdictThresholds (analytics.eval.verdict has no back-import;
  no cycle). The standard layer computes the CIs; the contract consumes the bounds.

v0.7 — two threshold refinements surfaced by the PR-C jump-amount-corr repro:
- Incremental axis gets its OWN bar, min_incremental_abs_icir=0.15, SEPARATE from
  the raw predictive min_abs_icir=0.30. Orthogonalizing on the known-factor book
  removes variance, so the residual ICIR lives on a structurally smaller scale;
  reusing the raw 0.30 bar was a category error that silently failed genuinely
  partial-incremental factors. The raw predictive bar is unchanged (a 0.20 ICIR
  clears incremental but still fails predictive).
- Monotonicity becomes a DIRECTION gate, not a strength bar: default
  min_monotonicity_spearman=0.0 means "aligned monotonicity must be strictly > 0"
  (buckets ordered the RIGHT way / not reversed), not "strongly monotone". The old
  0.80 strength bar rejected tail-concentrated real factors whose power sits in one
  extreme (jump-amount-corr: ICIR -0.40, NW-t -14.8, yet Q3-humped). A reversed
  aligned monotonicity still fails. Raise the threshold to demand strength again.
Both defaults remain UNVALIDATED-BY-DATA (design section 11).
…(three-axis + N_eff CIs)

StandardFactorEvaluator fills the 8 mandatory sections by reducing the vectorized
eval-IR built once per run; joint orthogonalized IC (Gram-Schmidt, loop-free over
dates) drives the Incremental axis.

N_eff confidence intervals (change #3):
- stats.py: information_ratio_ci (Lo 2002 Sharpe SE, N->N_eff:
  SE(IR)=sqrt((1+0.5*IR^2)/N_eff)) and mean_ci (SE=std/sqrt(N_eff)) at 95%,
  reusing effective_sample_size. Degenerate inputs -> NaN, never a fabricated CI.
- standard.py: predictive_power attaches the ICIR CI (and IC-mean CI); purity
  attaches the incremental ICIR CI computed on the ORTHOGONALIZED IC series' OWN
  N_eff; return_risk reports the base-spread CI (point-only gating, disclosed).
  The contract's Predictive/Incremental PASS gates on the LOWER bound, so an
  autocorrelated or noisy IC series -> wider CI -> lower bound below the bar ->
  not a PASS, where an i.i.d. series of the same raw length passes.
- pre-registered EvalConfig.success_criteria flows through StandardFactorEvaluator
  end to end and is stamped (criteria_source) in the report.
…nel figure)

A one-PNG visual report for a FactorEvalReport, styled after the Kaiyuan
(开源证券) layered-backtest charts: thin per-quantile NAV curves + a thick
dark-maroon long-short curve on a secondary axis, dense YYYYMMDD date axis,
white canvas, top multi-column legend.

analytics/eval/figures.py (pure rendering, matplotlib Agg, NOT re-exported from
__init__ so importing the eval package never pulls a plotting backend):
- render_factor_dashboard(report, ir, out_path) -> the dashboard.
- DashboardData.from_report(report, ir): normalizes the section payloads +
  three-axis verdict (from the report) and the ic / quantile_returns series
  (from the StandardEvalIR) into one testable value.
- Panels: header + three-axis verdict chips, hero layered quantile NAV +
  gross long-short, per-period & cumulative IC, quantile final NAV, OOS
  train/test sign consistency, IC decay, purity corr-with-book, cost-drag
  sensitivity, coverage. Every panel degrades to "not available" on a
  Skipped/absent section rather than crashing.
- MANDATORY factor-definition band (contract constraint): a factor may not be
  shown without stating how it is computed. Sourced from the report's
  FactorSpec (description + inputs + expected sign + horizon/return basis +
  family + min-history + price basis + the minute block when intraday) -
  reusing existing spec fields, no new mandatory field.

analytics/eval/standard.py: StandardFactorEvaluator.evaluate_with_ir returns
(report, ir) by mirroring the frozen ABC evaluate() template step for step -
the contract evaluate() returns only the report (frozen signature) but the
dashboard needs the IR series; building the IR once here avoids a second pass
and the report stays byte-identical to evaluate()'s (test-locked).

Frozen contract files untouched; the long-short curve is GROSS (cost enters via
the separate cost-drag panel), avoiding the aligned-net cost-sign question.
…dFactorEvaluator run (PR-C)

The runner now emits the research-style dashboard PNG alongside the Markdown +
JSON reports, for both the no-book and with-book runs:
- evaluate_two_runs switches evaluator.evaluate -> evaluate_with_ir so it keeps
  the StandardEvalIR (per-period IC + quantile return series) the dashboard needs;
  the reports are byte-identical to the evaluate() path.
- render_factor_dashboard writes {stem}_{no_book,with_book}_dashboard.png. The
  with-book dashboard shows the purity panel + Incremental PASS; the no-book one
  degrades that panel to "not available" (Incremental NOT_ASSESSED).
- _RunReports carries the two dashboard paths; the CLI prints them.

The dashboard's mandatory factor-definition band is sourced from this factor's
FactorSpec (its description already states the full computation recipe), so no
new spec field was needed.
@StackOverFlow11
StackOverFlow11 merged commit 310ed9b into main Jul 19, 2026
@StackOverFlow11
StackOverFlow11 deleted the feat/pr-c-jump-amount-corr branch July 19, 2026 03:13
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