feat: I5d MMP minute-factor grouped backtest — standalone feasibility (incl. I5c plumbing) - #39
Merged
Merged
Conversation
Add the exploratory Minute Microstructure Pressure (MMP) factor to the I3 minute -> daily aggregation. Per 1min bar t (single symbol/day session, sorted by bar_end): mid=(high+low)/2; S=(close-mid)/mid; V=sqrt(volume/median(vol[t-20:t])); B=|close-open|/(high-low+eps); R=(high-low)/(mean(hl[t-20:t])+eps); MMP_t=S*V*B*R, eps=1e-6. - compute_minute_mmp: rolling baselines use ONLY the prior 20 bars t-20..t-1 (rolling(20).shift(1)) — never bar t, never a later bar, never the prior day's tail; the first 20 bars of each session are NaN. Invalid denominators (mid<=0, median vol<=0, NaN baseline) yield NaN, never inf. - new feature key 'mmp_ew' -> column intraday_mmp20_ew_0930_1450 = the EQUAL -WEIGHT mean of valid MMP_t visible at the cutoff (the volume term is already inside MMP_t; the daily aggregation is NOT additionally volume-weighted). No valid minute -> NaN. - mmp_ew is SELECTABLE-ONLY: DEFAULT_FEATURE_KEYS keeps the original four, so a no-args asof_daily_features call has the same columns/cost as before. - mmp_valid_minute_counts: report-only per-(date,symbol) valid-minute count under the same PIT cutoff, reusing compute_minute_mmp (one MMP source of truth). PIT unchanged: available_time <= trade_date + decision_time filters per-bar timestamps BEFORE daily grouping. Data-layer only: no returns / Tushare / cache / portfolio / execution.
Add IntradayCfg.score_feature (Literal mirroring INTRADAY_FEATURE_KEYS; default 'ret' reproduces the I5a/I5b smoke score, 'mmp_ew' selects the MMP factor) and OutputCfg.intraday_report_title so a reused intraday runner can name the actual study instead of a stale phase label (same precedent as subset_report_title). All existing configs validate unchanged and keep the 'ret' default; an unknown score_feature fails readably at validation.
Make the intraday tail runner consume intraday.score_feature: _score_panel requests ONLY the selected feature from asof_daily_features and uses its returned column EXACTLY (no hardcoded prefix matching), so default 'ret' reproduces I5b and 'mmp_ew' selects intraday_mmp20_ew_0930_1450. _report_heading is study-aware (I5c for mmp_ew / I5b when price-limit on / else I5a) with an intraday_report_title override. Add report-only factor diagnostics: MMP formula + PIT disclosure, daily score coverage, valid-MMP-minute distribution, and per-settled-rebalance cross-sectional score stats + Spearman IC of the decision-date score vs the same period's exec-to-exec return (read in the report layer only, never fed back to factors/alpha). I5b raw stk_limit feasibility stays ON and disclosed. Add config/phase_i5c_mmp_minute_factor.yaml: same SSE50 SH/SZ window as I5b with score_feature=mmp_ew, price_limit_check=true, its own report name + title.
16 I5c tests: exact MMP_t formula on a controlled 22-bar day; rolling baselines exclude bar t (t-20..t-1); first 20 bars NaN; daily score is equal-weight (asserts != volume-weighted); NaN-not-inf denominators; daily NaN when no valid minute; PIT (post-cutoff bars + delayed availability don't move the score); future bars don't change earlier MMP_t; multi-day no prior-tail carryover; multi-symbol isolation; old configs default to 'ret'; I5c config selects mmp_ew + titles the study; invalid score_feature fails; score_feature Literal mirrors feature keys; _score_panel selects MMP without prefix matching; heading names I5c. Update the I5b heading test for the new _report_heading signature.
…ime] window The MMP daily score promised aggregation over bars with bar_end in [session_open, decision_time] but only applied the upper bound (available_time <= cutoff); pre-session bars (before session_open) leaked into the rolling baseline, so the first in-session bar wrongly got a non-NaN MMP and the report's [session_open, decision_time] claim did not hold. Add the lower bound bar_end >= trade_date + session_open on the MMP path only (_mmp_ew_daily + mmp_valid_minute_counts) so pre-session bars enter neither the rolling baseline nor the daily mean; the first 20 IN-SESSION bars are NaN. The existing four features (ret/realized_vol/vwap/last30m_ret) are untouched — only mmp_ew gains the lower bound — so I5a/I5b behavior cannot drift. Regression: 20 pre-session bars (09:00..09:19) + one 09:31 bar with session_open 09:30 -> daily MMP NaN / 0 valid minutes (was a spurious value); prepending pre-session bars leaves a full in-session day's MMP unchanged. Real SSE50 stk_mins has no pre-09:30 continuous bars, so the smoke is unaffected.
…Model The execution-price matrix + fills are a pure function of (bars, anchor_dates, symbols, cfg). When several fresh models share the same bars/cfg (one per quantile group in I5d), pass the precomputed (prices, fills) so the heavy matrix is built once and reused while each model keeps its own fresh mutable feasibility diagnostics. Default None reproduces the byte-identical I5a/I5b in-place build (regression tests unchanged).
Rank the I5c MMP daily score (intraday_mmp20_ew_0930_1450) on each monthly rebalance date into analytics.quantiles equal-count rank buckets (Q1 = lowest, QN = highest) and run each group as its own long-only equal-weight portfolio through the SAME BacktestEngine + IntradayTailEventModel + SimExecution (fee_rate) with I5b raw stk_limit execution feasibility ON. - intraday_groups.py: assign_quantile_buckets (equal-count rank buckets, deterministic ties, too-few-names leaves high groups empty), GroupScores, EqualWeightAll (whole-bucket equal weight, ignores top_n). - intraday_group_backtest.py: run-phase-i5d-intraday-groups runner. Cache-only, anchor-date-sliced minute load (only rebalance/exit days -> memory-conscious, zero stk_mins live calls), one shared immutable exec-price matrix across N fresh per-group models, per-group metrics, QN-Q1 synthetic spread + group monotonicity (report-only). - intraday_group_figures.py / intraday_group_report.py: Agg PNG figures (NAV curves / Q5-Q1 spread / grouped metric bars) + markdown report. - cli.py: run-phase-i5d-intraday-groups subcommand. - config/phase_i5d_mmp_quintile_5y.yaml: CSI500, 2021-06-01..2026-05-31, mmp_ew, price_limit_check on, quantiles=5, fee_rate=0.001. require_cache_coverage=false drops minute-uncovered constituents (disclosed; window not shortened). Factor math, daily close-to-close, the event model and execution feasibility are unchanged; only the grouping + per-group orchestration is new. EXPLORATORY, not a performance claim, no tuning.
19 tests over the goal's five groups: equal-count bucket assignment (Q1 low / QN high, NaN/inf excluded, deterministic ties, too-few-names), EqualWeightAll ignores top_n, GroupScores selects one group, fee_rate reaches SimExecution; PIT invariants (grouping uses only the <=14:50 MMP score, post-cutoff and exit bars cannot change the assignment); grouped execution feasibility (raw stk_limit blocks, missing bar blocks without daily-close fallback, per-group counts do not double-count); report H1 names I5d (not stale) and mentions fee_rate=0.001 / analytics.quantiles=5; figures written and non-empty.
This was referenced Jun 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
I5d — MMP minute-factor → daily-aggregate → grouped backtest (standalone feasibility)
This is a STANDALONE, EXPLORATORY feasibility branch — NOT a performance claim and NOT a broad architecture refactor. It tests one closed loop: a minute-level signal (MMP) → a PIT-safe daily aggregated factor → a grouped intraday-tail backtest. It is reviewable against
mainon its own.Why this branch includes I5c code
I5d (the quintile grouped backtest) depends on the I5c MMP daily-score plumbing (the MMP minute formula + EW daily aggregation, the configurable
intraday.score_feature, and the I5c config/tests). Because I5c (PR #37) is still open againstmain, this standalone branch bundles the minimum I5c plumbing so the feasibility loop builds and runs without being stacked on PR #37. The I5c dependency is disclosed, not hidden.Contents (8 commits, main..HEAD)
score_feature+ report title, MMP score selection + report/diagnostics, I5c formula/PIT/config tests, and the[session_open, decision_time]window fix.IntradayTailEventModel, the I5d grouped runner + grouping/figures/report/CLI/config, and the I5d tests.What I5d does
Ranks the daily MMP score
intraday_mmp20_ew_0930_1450on each monthly rebalance date intoanalytics.quantiles=5equal-count rank buckets (Q1 = lowest, Q5 = highest) and runs each group as its own long-only equal-weight portfolio through the SAMEBacktestEngine+IntradayTailEventModel+SimExecution(fee_rate=0.001)with I5b rawstk_limitexecution feasibility ON (14:50 decision / 14:51 execution, exec-to-exec returns, never close-to-close). Factor math, the event model, and the execution feasibility are unchanged; only the grouping + per-group orchestration is new.Accepted exploratory result (one window — not a performance claim)
CSI500 (
000905.SH), 2021-06-01 → 2026-05-31, 59 monthly rebalances.covered=892/995— 103 minute-uncovered constituents are dropped and the coverage bias is disclosed; the 5-year window is NOT shortened.stk_mins_live_calls=0(cache-only). Final NAV increases monotonically Q1→Q5 (0.9822 / 1.0423 / 1.0495 / 1.1275 / 1.1577; monotonicity Spearman 1.0000). The Q5−Q1 spread is a synthetic long-only leg difference, NOT a dollar-neutral executable book. One overlapping window / one universe / no robustness / no tuning — far too little to infer factor quality.Note on scope
Efficient minute-factor research will likely need a more thorough architecture upgrade later; that refactor is intentionally out of scope here.
Gates
pytest577 passed;ruffclean; all configs validate;run-phase0ic_mean=0.9600 / annual_return=0.8408unchanged. Report/logs/figures secret-scanned: 0 token leaks.