Skip to content

feat: I5c MMP minute-factor study (exploratory) - #37

Merged
StackOverFlow11 merged 5 commits into
mainfrom
feat/i5c-mmp-minute-factor
Jun 19, 2026
Merged

feat: I5c MMP minute-factor study (exploratory)#37
StackOverFlow11 merged 5 commits into
mainfrom
feat/i5c-mmp-minute-factor

Conversation

@StackOverFlow11

Copy link
Copy Markdown
Owner

Summary

EXPLORATORY MMP minute-factor study — NOT a performance claim and NOT a broad
factor search.
Implements ONE user-proposed minute factor (Minute Microstructure
Pressure, MMP) from cached 1min OHLCV bars, equal-weight aggregates the PIT-visible
per-minute values (session open → 14:50) into one daily score
intraday_mmp20_ew_0930_1450, and analyzes it through the I5a intraday tail event
model with I5b raw stk_limit execution feasibility ON. No tuning, no robustness
matrix, no learned/IC-weighted alpha.

Per 1min bar t: 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. Daily score
is the equal-weight mean of valid MMP_t (the volume term is already inside
MMP_t — not additionally volume-weighted).

PIT / correctness guarantees

  • 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; first 20 bars of
    each session → NaN.
  • PIT cutoff available_time <= trade_date + decision_time filters per-bar
    timestamps BEFORE daily grouping; post-cutoff / late-available bars can't move the
    score.
  • Invalid denominators (mid<=0, median vol<=0, NaN baseline) → NaN, never inf.
  • Score selection configurable via intraday.score_feature; default ret
    reproduces the I5a/I5b smoke exactly (mmp_ew is selectable-only, not in the
    default feature set).
  • Spearman IC (uses forward returns) is computed in the report layer only,
    read-only — never fed back to factors/alpha. Daily close-to-close,
    BacktestEngine, SimExecution, and the achieved-book ledger are unchanged.
  • I5b raw stk_limit execution feasibility stays ON and disclosed.

Changes

  • data/clean/intraday_aggregate.pycompute_minute_mmp, mmp_ew key + column,
    mmp_valid_minute_counts, DEFAULT_FEATURE_KEYS.
  • qt/config.pyIntradayCfg.score_feature (default ret),
    OutputCfg.intraday_report_title.
  • qt/intraday_tail_framework.py — configurable score, study-aware heading, MMP +
    IC report diagnostics.
  • config/phase_i5c_mmp_minute_factor.yaml, tests/test_i5c_mmp_minute_factor.py.

Test plan

  • pytest tests555 passed (538 baseline + 16 I5c + 1 config-validation param)
  • ruff check . → clean
  • validate-config on all 16 configs → OK
  • phase0 regression ic_mean=0.9600, annual_return=0.8408 (unchanged)
  • Real smoke (SSE50, 2026-03-03→06-12): covered=58/58, stk_mins_live_calls=0,
    limit coverage 174/174, 1 up-limit blocked buy, MMP score coverage 4054/4060
    valid, per-rebalance Spearman IC −0.1154/−0.0608/+0.0427 (mean −0.0445,
    report-only), final NAV 0.957143 — not a performance claim.
  • Secret scan (report + logs): 0 token / .config.json.

Acceptance: tmp/context/session_handoff_20260615/stage_i5c_acceptance.md.

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.
@StackOverFlow11

Copy link
Copy Markdown
Owner Author

Addressed a review blocker (real I5c semantic bug): the MMP daily aggregation promised bar_end ∈ [session_open, decision_time] but only applied the upper bound (available_time <= cutoff) — pre-session bars (before 09:30) leaked into the rolling baseline, so the first in-session bar got a spurious non-NaN MMP.

Fix (5afc861): add the bar_end >= trade_date + session_open lower bound 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 and the first 20 IN-SESSION bars are NaN. The existing four features are untouched → zero I5a/I5b 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 0.035355); prepending pre-session bars leaves a full in-session day's MMP unchanged. (+2 tests, now 18 I5c tests.)
  • Real SSE50 stk_mins has no pre-09:30 continuous bars, so the re-run smoke is byte-identical (NAV 0.957143, coverage 4054/4060, IC table, valid-minute distribution all unchanged) — the fix only corrects the edge case, and the report's [session_open, decision_time] claim now actually holds.
  • Gates: pytest 557 passed, ruff clean, 16 configs validate, phase0 0.9600/0.8408 unchanged, secret scan 0.

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