Skip to content

feat(factors): reproduce ridge minute-return factor (PR-K, sign transfers but Reject on monotonicity + redundancy) - #71

Merged
StackOverFlow11 merged 1 commit into
mainfrom
feat/pr-k-ridge-minute-return
Jul 20, 2026
Merged

feat(factors): reproduce ridge minute-return factor (PR-K, sign transfers but Reject on monotonicity + redundancy)#71
StackOverFlow11 merged 1 commit into
mainfrom
feat/pr-k-ridge-minute-return

Conversation

@StackOverFlow11

Copy link
Copy Markdown
Owner

Summary

Reproduce the ridge minute-return factor — the fifth factor of Kaiyuan Securities microstructure #27 (loop reproduction 8/10) and the only negative-sign factor in that report. This completes a fourth statistic family (RETURN) on top of the count / timing / price-level families already reproduced from the same report.

Result: Reject / Reject — but the sign transferred. IC −0.0312, ICIR −0.381 (aligned CI lower bound 0.320 > the 0.30 bar), NW-t −13.31, win rate 0.652, OOS sign-consistent (train −0.0278 / test −0.0346). Prototype (persisted pre-run, same seed-20260720 30 names as PR-I/PR-J, symbol list byte-identical): mean RankIC −0.0314, PASS. So the report's behavioural claim — ridge minutes represent retail follow-on trading whose accumulated return reflects over-reaction and therefore predicts negatively — reproduces at the IC level.

It fails on exactly two things:

  1. Quantile monotonicity direction (raw +0.2 → aligned −0.2). The bucket means are Q1 0.000341 / Q2 0.000346 / Q3 0.000232 / Q4 0.000248 / Q5 0.000467 — a smile, not a reversal, with Q5 a lone outlier (final NAV 1.482 vs 1.19–1.36). The negative rank IC is real but lives in the middle of the distribution, not as a monotone tilt across sorted buckets. The reversal persists out-of-sample (test aligned −0.3), so it is not an in-sample artifact.
  2. Incremental axis — orthogonalized ICIR −0.141, just under the 0.15 bar: redundant with the value/low-vol book. (Both prior PASS factors cleared this comfortably at +0.343 and +0.289.)

A clean experimental control. PR-K sits almost exactly on PR-J's machine profile — turnover 2.108 vs 2.101, half-life 8.06 vs 7.99, cross-section 472.8 vs 475.2, ridge day-validity 51.2% vs 51.5% (same mask, same scarcity gate) — which isolates the difference to the statistic, not the pipeline.

Tradability, third data point: net long-short is negative at every cost scenario (−0.001983 / −0.004092 / −0.008309), the worst of the three. Gross long-short is only +0.000125/period against turnover 2.108 — costs swamp the edge roughly 16:1 at base fee (cumulative −90.9%). aligned_spread_* is unreliable here because this factor is sign=−1 and triggers the frozen layer's cost-sign defect (sign*(gross−cost) = −gross+cost); the frozen layer was correctly NOT patched, and the warning is carried in the module docstring, config header, CLI output and run log — every place the implementer controls.

Factor ridge_minute_return_20: reuses the merged ridge mask with zero changes to the shared taxonomy (git diff main -- data/clean/intraday_volume_prv.py is empty); minute return close_t/close_{t−1}−1 with within-day lag on raw closes; daily aggregate = simple sum of selected ridge returns (pinned and test-discriminated against Π(1+r)−1, which differs >4× on the hand cases); day valid = ≥100 classifiable AND ≥10 ridge-with-return; factor = sum (not mean) of daily sums over the trailing 20 valid days; NaN below 10 valid days. Pre-registered sign −1.

Disclosed interpretation edge (task-card gap, surfaced by the implementer): the within-day lag was pinned without a 60s-adjacency requirement, so the 13:01 bar carries a ~91-minute across-lunch return summed alongside 1-minute returns. Disclosed on the spec and locked in a test; the same-slot baseline mitigates systematic selection (13:01 is compared against its own history). First parameter to vary if this factor is ever revisited.

Test plan

  • Full pytest: 1527 passed (baseline 1473 + 54; the four prior factors' tests untouched, and their hand values re-asserted inside PR-K's own file) — rerun independently by orchestrator
  • ruff clean; phase0 regression ic 0.9600 / annual 0.8408 unchanged
  • Independent review: APPROVE, no CRITICAL/HIGH. Leak hunt clean (leakage test verified to have teeth — disabling the cutoff moves the value >50% and flips its sign, while the same late data is inert under the normal cutoff); within-day lag verified to never cross a day boundary; SUM-vs-compounding discriminator hand-recomputed and confirmed non-vacuous; frozen layer confirmed untouched.
  • Secret scan: 0 token values across diff, artifacts, logs, and the tmp/ prototype records

Reproduce the FIFTH factor of the Kaiyuan market-microstructure series #27
(量岭分钟收益) and run it through the frozen StandardFactorEvaluator on the
same CSI500 cell as PR-C..PR-J. Opens a FOURTH statistic family (a RETURN,
after a count / a timing moment / two price levels) and is the report's only
NEGATIVE peak/ridge/valley factor, so it also tests whether the SIGN
transfers along with the family.

The minute taxonomy is REUSED verbatim from data/clean/intraday_volume_prv.py
(unmodified by this change): a ridge is an eruptive minute that is not an
isolated peak. The four already-merged factors stay bit-identical, locked by
hand-value tests here.

Pinned choices, none of which the report specifies (all disclosed on the spec
and in the config header):
  - minute return close_t/close_{t-1} - 1 with a WITHIN-DAY lag, so each day's
    first visible bar carries no return and no return crosses a day boundary;
    exact 60s adjacency is deliberately not required, so the post-lunch bar
    returns against the last bar before the gap;
  - RAW unadjusted closes (the adjustment factor is constant within a day and
    cancels in the ratio; the within-day lag excludes the one bar that could
    straddle an ex-date);
  - the daily aggregate is a SIMPLE SUM, not a compounded product: ridge
    minutes are non-contiguous, so a holding-period reading does not apply;
  - a day is valid iff >=100 classifiable bars and >=10 ridge bars carrying a
    valid return (counted after the guard), NaN below 10 valid days.

The runner surfaces net_long_short_by_cost explicitly: this factor's sign of
-1 triggers a known frozen-layer defect in aligned_spread_*, which computes
sign * (gross - cost) and so adds costs back. The frozen layer is not patched.

Adds: data/clean/intraday_ridge_return.py, RidgeMinuteReturnFactor,
qt/eval_ridge_minute_return.py, config/phase_k_ridge_minute_return.yaml, a
run-eval-ridge-minute-return subcommand, and two test files (+54 tests).
@StackOverFlow11
StackOverFlow11 merged commit 12427cd 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