Skip to content

feat(factors): D2 — minute primitives, ops operators, 11-factor migration (bit-identical to the frozen baseline) - #89

Merged
StackOverFlow11 merged 8 commits into
mainfrom
feat/factor-refactor-d2-primitives
Jul 24, 2026
Merged

feat(factors): D2 — minute primitives, ops operators, 11-factor migration (bit-identical to the frozen baseline)#89
StackOverFlow11 merged 8 commits into
mainfrom
feat/factor-refactor-d2-primitives

Conversation

@StackOverFlow11

Copy link
Copy Markdown
Owner

Step D2 of the factor-layer refactor (v3.2 §3.2/§九) — the first math-touching step. Identity: implementation moves, definitions do not — and the migration turned out bit-identical.

What (9 commits)

  • factors/ops/ — 7 time-series operators (only actually-consumed ones); momentum/volatility/liquidity/overnight_mom rewritten on them, locked by naive-oracle equivalence tests.
  • factors/compute/minute/primitives.py (absorbs peak_mask_for_symbol — the taxonomy's single authored definition — plus visible-bar prep, VWAP identities, rolling baselines, valid-day guards, all parameterized per-factor) + 11 one-file-per-factor modules (math + surface class + D1 spec declarations).
  • Shims (R14 granularity) — 10 pure factor modules + intraday_derived.py become AST-locked re-export shims; intraday_aggregate keeps its generic core as real code and re-exports MMP/jump; intraday_schema hosts shared constants (adjudicated: leaving them in aggregate forms a real import cycle, live-tested; documented for D6d).
  • Properties — 11 factor test files migrate by import-target only (assertions untouched; PR-M monkeypatch seams verified interceptable); NET-NEW overnight_boundary declaration test lands here (uniform pre-d rescale semantics; NONE factors bit-invariant incl. via real front_adjust; crossed_disclosed factors must move — positive control).
  • PR-D measurement debt settled — 4.58% of minute_ideal_amp windows contain a true ex-date (numbers independently re-derived by review from the frozen manifest); CROSSED_DISCLOSED upheld; debt-tracking test now pins "settled".
  • Provisional anchor (R10) — full-precision phase0 metrics pinned exact-equality (PROVISIONAL, promoted at D6).

Acceptance evidence

  • Cell-level reconciliation vs the D1 frozen baseline: 14/14 factors, max_rel_diff = 0.0, canonical hashes equal — bit-identical. Comparator itself has 6 teeth tests; frozen panels never touched (panels_d2/ separate).
  • 88 stratified hand-anchor rows, 0 mismatches (plain-arithmetic scripts with a runtime guard asserting no engine modules imported; the script caught 3 of its own convention errors before the engine ever disagreed).
  • Mutation evidence run live: ridge cross-day lag → rc=1/rc=0; shim stray function → rc=1/rc=0; ts_std min_periods → rc=1/rc=0. Review re-ran two of these mutations independently.
  • I5 value anchors all hit: I5b/I5f final_nav=1.019318; I5d quintiles + Spearman +0.9000; I5e quintiles + Spearman −0.5000 (cache-only, zero live calls).
  • Gates: pytest 1955 passed (1902 + 53, zero displaced), ruff clean, 31/31 configs, phase0 anchor verbatim, secret scans 0 (diff + run artifacts + binaries).

Review

Adversarial review: APPROVE-WITH-NITS (0 CRIT/HIGH/MED, 1 LOW). Reviewer went beyond reading: per-function text diffs of migrated math vs git show main: (only docstring/rename deltas), live identity checks proving the old import paths serve the new engine objects (dead-code illusion excluded), a live import-cycle test validating the constants-home adjudication (JC1), two mutations re-run live, and independent re-derivation of the measurement-debt numbers. The 1 LOW (silently skipped guard_boundary anchor class for value_ep/value_bp left no machine-readable trace) is fixed in this PR (guard_boundary_skipped disclosure rows; current JSON predates the fix, the D5 re-run will carry it).

Lead-independent: gates re-run, 3 panels_d2 hashes recomputed ≡ D1 manifest, shim import chains live-verified to factors.compute.minute.*.

Notes carried to D6: shim-period aggregate → factors.minute import direction (disclosed exception to red line #10, cycle-free, sunset D6d); asof_daily_features' mmp_ew hook needs a new home when shims are deleted.

…y factor rolling on them

D2 commit A (design v3.2 §3.2): factors/ops/timeseries.py is the one home of
the per-symbol lag/rolling machinery the daily factors used to inline. Three
conventions locked by tests: strictly per-symbol grouping, full-window
leading-NaN semantics, purity. Only the operators the rewrite consumes exist
(ts_lag / ts_window_return / ts_pct_change / ts_std / ts_mean / ts_sum /
log_positive) — no speculative cs_rank/median, no string-dispatch registry.

momentum / volatility / liquidity / overnight_mom computes now call the ops;
reversal still delegates to momentum. Definitions unchanged: every rewritten
factor is locked bit-identical to an independent naive oracle in
tests/test_factors_ops.py, and the pre-existing factor tests pass unchanged.

pytest 1917 passed (baseline 1902 + 15), ruff clean.
…compute/minute

D2 commits B+C (design v3.2 §3.2 / §6.4 / R14): the single definition point of
every minute factor's math moves to factors/compute/minute — one factor per
file, each file carrying BOTH the compute_* math (rewritten on the shared
primitives layer) and its D1 Factor surface class (spec declarations carried
over unchanged).

- factors/compute/minute/primitives.py: peak_mask_for_symbol +
  prepare_visible_minute_bars move house VERBATIM (they were already
  author-once; R1 — relocation, not repair), plus the shared PIT front half
  (visible_minute_frame), amplitude guard, positive-trade mask, valid-day
  rolling and pooled-trailing-reduce forms, per-symbol split, and the
  VOLUME_PRV_* classification constants. Per-factor definition parameters
  (min_valley_bars=20 / min_ridge_bars=10 / counted-AFTER-the-guard, ...)
  stay explicit parameters — parameterized, never unified.
- data/clean/intraday_schema.py: DATE_LEVEL / DAILY_INDEX_NAMES /
  DEFAULT_DECISION_TIME / DEFAULT_SESSION_OPEN move here (single home both
  the data-layer generic core and factors.compute.minute can import without
  a cycle); intraday_aggregate re-exports them unchanged.
- data/clean/intraday_aggregate.py: MIXED module per R14 — generic core
  (asof_daily_features / resample_intraday_bars / feature keys) keeps real
  code; the MMP and jump factor math migrated to factors.compute.minute.{mmp,
  jump_amount_corr} and is re-exported (qt/intraday_tail_framework.py:38 and
  the eval runners keep working with ZERO changes).
- 10 pure data/clean factor modules + factors/compute/intraday_derived.py are
  now import-only re-export shims (deleted in D6d); locked by an AST purity
  test scoped EXACTLY to the fully-migratable set (R14) plus re-export
  identity tests (shim name IS the new object).
- factors/registry/builtin.py imports the classes/constants from their new
  homes; dispatch semantics unchanged.
- The 11 per-factor property-test files migrate their imports to the new
  modules (性质测试搬迁 — same properties, now proven directly on the new
  engine; the PR-M teeth tests keep their monkeypatch seams, now patching
  factors.compute.minute.peak_ridge_amount_ratio).

Factor definitions and float operation order preserved verbatim; the D1
frozen-baseline cell-by-cell reconciliation follows in this branch.

pytest 1941 passed (1917 + 24 shim tests), ruff clean, validate-config 31/31,
phase0 anchor ic 0.9600 / annual 0.8408 unchanged.
…R-D ex-date measurement debt

D2 commit C - the NET-NEW P10 property the D0 catalogue stamped for this stage.

tests/test_overnight_boundary_declarations.py implements D0 s1.2's reading of
the declaration probe: a UNIFORM basis rescale of the symbol's entire < d
price history (lambda=0.5, a power of two, so 'none' checks assert
bit-equality). Declared none -> day-d value must not move (ridge_minute_return
/ intraday_amp_cut / jump_amount_corr minute-side, momentum through the real
front_adjust chain daily-side); declared crossed_disclosed -> the value MUST
move (valley_price_quantile + minute_ideal_amplitude positive controls, the
s3.5 pair-migration rule). Two calibration notes recorded in the module
docstring: (1) a first draft also rescaled 'amount' and the jump factor's
cross-day pooled correlation rightly moved - that simulated a rescale no real
ex-date performs (amount is traded RMB value); the D0 wording, prices only, is
the faithful probe; (2) the first ridge data set had no ridge at the day's
first bar, so the cross-day-lag mutation could not fire - the data now places
a ridge run at slots (0,1) and the mutation evidence was RE-RUN and hits
(cross-day lag: rc=1; restored: rc=0).

minute_ideal_amplitude's D0-note-1 measurement debt is PAID: measured against
the D1 frozen CSI500 panel + adj_factor cache, 4.58% of finite values (52,876
/ 1,155,283) carry a true ex-date strictly inside their 10-day pooling window;
on a seeded 120-value sample (each pinned value re-derived from raw 1min cache
and matched 120/120), the ranking-key re-basing intervention moves the factor
in 70% of windows (|delta| median 1.07e-05 / p90 1.79e-04 / max 2.91e-03 vs
panel std 6.0e-04; top/bottom-k membership shifts median 2.96% / p90 45.8% of
pooled bars). All three CROSSED_DISCLOSED requirements now met; the numbers
live in the spec docstring, and the D1 debt-tracker test
(test_minute_ideal_amplitude_docstring_tracks_the_d2_measurement_debt) now
pins the SETTLED disclosure - the docstring must carry the measured numbers
and must no longer claim the obligation open (an authorized tracker update,
not a weakened test: the tracked debt was paid).
…tooling

D2 commits F/G tooling (design v3.2 s5 legs 2+4, R11/R12).

qt/panel_reconcile.py rebuilds all 14 closing-factor RAW panels on the current
tree (the panel_freeze recipes route through the D2 shims, so they exercise the
migrated factors/compute/minute math automatically), writes them to the
SEPARATE artifacts/refactor_baseline/panels_d2/ directory (the frozen D1
baseline is never overwritten), and compares cell by cell: identical index,
identical NaN sets, relative diff <= 1e-12 on jointly-finite cells. Anti-empty
-reconciliation provenance: the frozen side is hash-verified against the D1
manifest BEFORE any comparison is trusted, and both sides are read from disk
through independent file reads. tests/test_panel_reconcile_d2.py feeds the
comparator engineered defects (value drift beyond budget, NaN-set change in
both directions, index mismatch, near-zero fabrication) and asserts each one
CONVICTS - a comparator that cannot fail is the compare_postmerge.py failure
mode.

qt/hand_anchors_d2.py + qt/hand_anchor_rows.py hand-recompute stratified
anchor rows (R12: warm-up end / guard boundary / ex-date window / seeded
interior) from the raw cache parquets in plain numpy+pandas arithmetic. The
module NEVER imports factors.* or data.clean.* (runtime guard raises if the
engine sneaks into sys.modules); engine values come from the panels_d2 files
(disk reads). qt/hand_anchors_engine_values.py is the one companion allowed to
import the engine - it fills in the engine side for the four ops-rewritten
daily factors that have no frozen panel (momentum/reversal/liquidity/
overnight_mom).

Two engine-semantics facts the hand check surfaced and now encodes (the
pre-flight spot-check convicted the first drafts, which is the point of an
independent reimplementation): (1) PR-C jump_amount_corr computes on FULL-day
bars - its engine has no 14:50 cutoff (day-level PIT only; PR-C predates the
cutoff convention); (2) PR-E resamples the FULL day to 5min FIRST and then
PIT-filters the DERIVED bars by their own available_time = max(source)+1min,
so a bucket with any post-14:49 constituent is dropped WHOLE. With both
encoded, the 24-row pre-flight spot-check against the frozen panels passes
0-failure (20 exact, worst rel 5.8e-15, all within the 1e-12 float budget).

tests/test_provisional_anchor_d2.py freezes the PROVISIONAL full-precision
phase0 demo anchor (R10 timeline; promoted in D6): ic_mean
0.9600438863169586 / annual_return 0.8407986461861146 / sharpe
10.304260918159038 / plus turnover/cost/vol/maxDD, exact-equality contract,
with the coarse 0.9600/0.8408 reading locked as a corollary.
Three corrections the frozen-panel spot-check convicted (the reason the hand
reimplementation exists): (1) the minute-cache path gains its freq=1min
partition level; (2) PR-C jump reads FULL-day bars (its engine has no 14:50
cutoff - day-level PIT only, a PR-C-predates-the-convention fact now recorded
at the call site); (3) PR-E resamples the FULL day to 5min FIRST and PIT-
filters the DERIVED bar by available_time = max(source)+1min, dropping partial
buckets WHOLE. Post-fix spot-check: 24/24 rows pass (20 exact, worst rel
5.8e-15). Also widens the peak-family hand read windows to 200 calendar days
(baseline-truncation safety), trims the valley-quantile cross-section read to
130 days and amortizes its five anchor rows over three distinct dates (each
date costs a full-universe hand cross-section; disclosed).
momentum/reversal/overnight: warmup + ex-date + 3 random; liquidity (amount
channel, ex-date class not applicable - disclosed, not faked): warmup + 4
random.
The first G run convicted every early-window row (warmup/ex-date anchors in
2021-07/08) while every mid-panel row was exact: the minute cache holds
BACKFILLED bars from before 2021-07-01, and the hand reads were pulling that
richer history in, while the frozen panels were computed on
store.read_range(cfg.data.start=2021-07-01, ...). read_minutes now clips its
lower bound to the plane start. _pick_warmup gains a disclosed fallback for
factors that only emit rows once the accumulation floor is crossed (amp_cut's
combine drops non-finite stat pairs, so its panel has no leading-NaN ramp):
the symbol's first emitted row IS the warm-up end.
A missing guard_boundary row in hand_anchors_d2.json was previously
indistinguishable from a forgotten stratification class (review D2, LOW).
The scan now records an explicit guard_boundary_skipped row with the scan
size, so the JSON is self-describing. Current run artifacts predate this
fix; the D5 re-run will carry the disclosure.
@StackOverFlow11
StackOverFlow11 merged commit 81c760d into main Jul 24, 2026
StackOverFlow11 added a commit that referenced this pull request Jul 24, 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