Skip to content

feat(phase2): small-scale real-data reproducibility baseline (SSE50, auditable report) - #2

Merged
StackOverFlow11 merged 3 commits into
mainfrom
p2-real-baseline
Jun 9, 2026
Merged

feat(phase2): small-scale real-data reproducibility baseline (SSE50, auditable report)#2
StackOverFlow11 merged 3 commits into
mainfrom
p2-real-baseline

Conversation

@StackOverFlow11

@StackOverFlow11 StackOverFlow11 commented Jun 9, 2026

Copy link
Copy Markdown
Owner

Summary

Phase 2-1: a small-scale, real-data (tushare) reproducibility baseline. It runs the existing P0/P1 spine end-to-end on the real path over a small universe (上证50 / SSE50) and a ~1-year window, finishing in ~11 min, and emits a rich, auditable diagnostic report. No new factor, no parameter search, no live trading — the deliverable is a reproducible, self-auditing report, not a strategy result.

New surface:

  • qt/cli.pyrun-phase2-baseline --config <path> subcommand (refuses the demo source; real-data only).
  • qt/phase2_baseline.py — orchestration that reuses the pipeline step helpers verbatim, plus read-only collectors (PIT membership summary, ann_date coverage, tradability hit funnel, holdings reconstruction).
  • qt/reports.pyrender_phase2_baseline + a required-section contract.
  • universe/index_universe.py — read-only membership_snapshots() accessor.
  • qt/pipeline.py_make_logger gains an optional name (phase0/phase2 log split).
  • config/phase2_real_baseline.yaml — SSE50 (000016.SH), 2023-07..2024-06, momentum_20, all filters + neutralization on.

Report contents (artifacts/reports/phase2_real_baseline.md, git-ignored)

Data window · PIT membership (loaded vs in-window) · ann_date as-of financial coverage · tradability filter-hit funnel · rebalance dates · per-period holdings · per-period turnover/cost · IC / quantile returns · performance summary · all P2 downgrades.

Real run (tushare; not in CI)

  • 68 distinct constituents / 25 loaded snapshots / 60 distinct names in-window / 11 settled rebalances; ~11 min.
  • ann_date roe coverage 100% (a data-quality diagnostic, not the alpha factor).
  • Tradability funnel self-consistent: 550 member-days → 549 tradable (1 at-limit), sum(hits) == candidates - tradable.
  • IC ≈ 0.008; annual return −17.6%, Sharpe −0.96. This is a losing momentum baseline on large-caps — disclosed in the report as a plumbing/reproducibility validation, NOT a performance claim.

Correctness guards

  • No look-ahead: holdings are reconstructed read-only (universe → scores → constructor.build), the same chain the driver runs; the factor stage never sees forward returns.
  • Demo vs real not confused: run-phase2-baseline raises before any network call on a non-tushare source.
  • Secret hygiene: the tushare token / secret file path is never read into the report; verified by test.
  • Settled vs candidate cadence (review fix): diagnostics key off nav_table.index (settled dates), so holdings match the NAV table 1:1; the terminal rebalance with no forward period (BT-003) is disclosed as skipped, not held.
  • PIT lookback honesty (review fix): the ~370-day pre-start lookback snapshots are reported as "loaded" separately from the "in-window" membership summary.

Review loop

A code-review pass flagged 1 HIGH (non-exclusive filter-hit counts) + 2 MEDIUM; a human audit pass flagged the settled-vs-candidate cadence (HIGH) and the lookback/in-window membership (MEDIUM). All were fixed and locked by tests before this PR (see commit 8de99a4).

Test plan

  • pytest182 passed (P0=93 / P1=75 / P2-1=14), all network-free.
  • ruff check . — clean.
  • validate-configexample.yaml, example_tushare.yaml, phase2_real_baseline.yaml all OK.
  • run-phase0 --config config/example.yaml — OK.
  • Real run-phase2-baseline (manual, tushare) — 11 settled holdings == 11 NAV rows; report regenerated from final code.
  • Hygiene — no tushare token, conflict markers, or artifacts//*.parquet tracked.

Add a `run-phase2-baseline` run mode that runs the existing P0/P1 spine
end-to-end on the real tushare path over a small universe (SSE50) + short
window (~11 min) and emits a rich diagnostic report. No new factor, no
parameter search: it validates the real-data plumbing and is reproducible.

- qt/phase2_baseline.py: orchestration reusing the pipeline step helpers
  verbatim + read-only collectors (universe/PIT summary, tradability hit
  funnel, holdings reconstruction, ann_date coverage). Refuses the demo
  source before any network call (real-data only).
- qt/reports.py: render_phase2_baseline + required-section contract.
- qt/cli.py: run-phase2-baseline subcommand.
- universe/index_universe.py: read-only membership_snapshots() accessor.
- qt/pipeline.py: _make_logger gains an optional name (phase0/phase2 split).
- config/phase2_real_baseline.yaml: SSE50 (000016.SH), 2023-07..2024-06.
- tests/test_phase2_baseline.py: 12 network-free tests (collectors, demo
  guard, report-field contract, no-secret-leak, tradability first-match).

Tradability hits use first-match-wins exclusive buckets mirroring
apply_tradable_filters (sum(hits) == candidates - tradable). Holdings are
reconstructed read-only (no forward returns at the factor stage). The
tushare token is never read into the report; the report artifact stays
git-ignored under artifacts/.
…ndow membership

Two report/audit-cadence fixes found in review (report correctness, not strategy):

1. The diagnostics (ann_date coverage, tradability hits, per-period holdings)
   keyed off driver.rebalance_dates() (candidate dates), but BacktestDriver.run()
   SKIPS a terminal rebalance with no forward holding period (BT-003). The report
   therefore listed holdings for a date that had no NAV/turnover row (phantom
   period). Now all three diagnostics key off nav_table.index (the settled dates),
   so holdings match the NAV table 1:1; candidate dates and the skipped terminal
   date are disclosed separately. Regression test drives the real BacktestDriver
   over a synthetic panel whose last rebalance is the last trading day and asserts
   holdings.date == nav_table.index (terminal date absent).

2. summarize_universe folded the ~370-day pre-start lookback snapshots into the
   "over window" summary (first snapshot 2022-06-30 for a 2023-07 window). It now
   reports LOADED snapshots (incl. lookback, for transparency) separately from the
   IN-WINDOW membership (snapshots dated within [start,end] plus the as-of anchor
   at start); distinct-names / size / churn are over the in-window view. Test
   proves a name living only in a superseded pre-window snapshot is excluded.

Real re-run: 11 settled holdings == 11 NAV rows; in-window distinct names 60 (vs
25 loaded snapshots); tradability funnel 550 member-days -> 549 tradable.
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