Skip to content

feat(factors): reproduce volume-peak interval-kurtosis factor (PR-H, null result → Reject) - #68

Merged
StackOverFlow11 merged 2 commits into
mainfrom
feat/pr-h-peak-interval-kurtosis
Jul 20, 2026
Merged

feat(factors): reproduce volume-peak interval-kurtosis factor (PR-H, null result → Reject)#68
StackOverFlow11 merged 2 commits into
mainfrom
feat/pr-h-peak-interval-kurtosis

Conversation

@StackOverFlow11

Copy link
Copy Markdown
Owner

Summary

Reproduce the volume-peak interval-kurtosis factor — the second factor of Kaiyuan Securities microstructure #27 (loop reproduction 5/10). Same peak taxonomy as the merged PR-F peak-COUNT factor, different statistic: the kurtosis of the distribution of gaps between consecutive same-day volume peaks.

Result: an honest NULL. IC mean +0.000217, ICIR 0.0058, NW-t 0.232, win rate 0.522 (coin flip) → Predictive FAIL (OOS sign not consistent across both holdout subperiods) → Reject / Reject. The report claims RankIC +7.19% / RankICIR 4.63 with 13 consecutive positive years — this is the largest reproduction gap in the loop so far, and it does not survive on our daily CSI500 cell.

Independently verified as a real null, not a defect (review): an independent from-scratch reproduction on a different 35-symbol sample got mean IC +0.0095 — same near-zero order, sign-consistent with the pre-registered +1. Factor coverage is healthy (NaN rate 2.86%, mean cross-section 929/995) and the value distribution is non-degenerate (std 5.93, 8857 distinct values), so this is a genuine 'does not rank-correlate with next-day returns' finding rather than a data-starvation or constant-factor artifact.

Factor peak_interval_kurtosis_20: reuses PR-F's peak identification unchanged; per valid day, gaps between consecutive peaks measured in trading minutes (11:30→13:01 is 1 apart, not 91); pooled over the trailing 20 valid days; factor = Fisher excess, bias-corrected kurtosis (pandas .kurt() convention); NaN if pooled intervals < 20 or zero variance; a day with <2 peaks contributes 0 intervals but stays valid. Both pins are report-silent and disclosed on the spec. Pre-registered sign +1.

Reuse refactor — verified bit-identical. data/clean/intraday_volume_prv.py was refactored (+97/−32) to extract prepare_visible_minute_bars and peak_mask_for_symbol for reuse. Review ran two independent numerical equivalence checks (synthetic incl. bad-data and empty paths; 10 real cached symbols / 4,740 rows) comparing main vs branch compute_volume_peak_count: 0 mismatches, bit-identical; all 26 PR-F tests pass unchanged.

Test plan

  • Full pytest: 1380 passed (baseline 1346 + 34; PR-F tests unchanged) — rerun independently by orchestrator
  • ruff clean; phase0 regression ic 0.9600 / annual 0.8408 unchanged
  • Independent code review: APPROVE-WITH-NOTES (0 CRIT/HIGH; reviewer hand-derived the G2 kurtosis formula and verified both test cases from raw arithmetic, and independently reproduced the null)
  • Secret scan: 0 token values in diff + artifacts; verdicts confirmed Reject/Reject from artifacts
  • Known process gap (MEDIUM, disclosed): the task card's pre-run ~30-symbol prototype sanity record is absent from the branch and artifacts — a repeat of a PR-G LOW. The reviewer's independent reproduction (+0.0095, correct sign) substantively covers the risk this gate exists to catch; the next task card makes a persisted prototype artifact a hard requirement.

Reproduces the SECOND factor of the Kaiyuan market-microstructure series #27
(reportId 4957417, section 6): the kurtosis of the gaps between consecutive
same-day volume peaks, pooled over the trailing 20 valid trading days.

Same MACHINE as PR-F, different STATISTIC. The volume-peak identification is
REUSED from data/clean/intraday_volume_prv.py rather than re-implemented, so the
two factors can never drift apart. That required a behaviour-preserving additive
extraction of two helpers from the existing code:

  - prepare_visible_minute_bars: the PIT truncation + volume guard + slot column
  - peak_mask_for_symbol: the same-slot strictly-prior baseline, eruptive/mild
    classification and the mild-neighbour peak rule

compute_volume_peak_count now calls both; nothing else about it changed. The
extraction is provably inert: the pre-refactor and post-refactor functions were
run against the same real cached minute bars for 12 symbols (7906 factor rows,
2022-2024) and compared with assert_series_equal(check_exact=True) -- identical.
The 26 existing PR-F tests pass unchanged.

Two interpretations the report leaves open are PINNED and disclosed on the spec:

  1. an interval is measured in TRADING MINUTES (the tradable-slot difference
     inside the day's visible bar sequence), so the lunch break costs nothing --
     peaks at 11:29 and 13:02 are 3 apart, not 93. Measuring wall clock would
     inject a fixed ~90-minute spike into every straddling distribution and
     dominate the kurtosis.
  2. kurtosis = Fisher excess, bias-corrected (the pandas .kurt() / scipy
     fisher=True bias=False convention; a normal sample sits at 0, not 3).

A day with fewer than 2 peaks contributes 0 intervals but remains a valid day.
Two honest-missing gates: fewer than 20 pooled intervals, or a zero-variance
pool, yield NaN rather than a fabricated number. Pre-registered sign +1.

Tests cover two hand-computed kurtosis values (intervals [2,2,2,8] -> exactly
4.0; [2,3,4,5,6,2] -> exactly -1.48125), estimator alignment with pandas .kurt()
on random samples, the lunch-break trading-minute semantics, the single-peak and
zero-peak day cases, both NaN gates, PIT invariance to post-14:50 and future
bars, per-symbol isolation, and an anti-drift lock asserting the intervals come
from exactly the peaks PR-F counts.
…PR-H)

Runs the PR-H factor through the FROZEN StandardFactorEvaluator on real cached
CSI500 data via a new run-eval-peak-interval-kurtosis subcommand. The eval cell
is IDENTICAL to PR-C..PR-G (CSI500 PIT membership, 2021-07-01..2026-06-30, daily
rebalance, OOS split 2024-01-01, book value_ep/value_bp/volatility_20, fee
0.001), so this run is directly comparable to its PR-F sibling: same peaks,
different statistic.

Cache-only: the per-symbol minute read goes through IntradayParquetStore, which
has no fetch closure, so stk_mins live calls are provably zero; a symbol with no
cached minute is disclosed as empty rather than fetched. Memory-bounded -- one
symbol's minute history is aggregated to its daily series and discarded before
the next. The evaluator runs twice (no book -> Incremental NOT_ASSESSED; with
book -> Incremental measured).

Real run: 995/996 symbols covered, stk_mins_live_calls=0, 1139746 factor rows,
462.7s.

RESULT IS NEGATIVE and reported as such. Rejected on both axes: rank IC mean
+0.000217, ICIR +0.006 (95% CI -0.051..+0.063), Newey-West t 0.232, win rate
52.2%, N_eff 1190; the expected sign is not consistent across the two holdout
subperiods (+0.000683 / -0.000179). With the book, incremental ICIR +0.046 --
redundant. Quintile monotonicity is +0.6 (test +0.7) but the aligned Q5-Q1
spread is -11.0%/yr, i.e. the ordering is not worth anything net. The report's
full-market monthly figures (RankIC +7.19%, RankICIR 4.63) do NOT reproduce on
this daily CSI500 cell; the report gives no CSI500 sub-domain number for this
factor and none is invented.
@StackOverFlow11
StackOverFlow11 merged commit b6db052 into main Jul 20, 2026
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