Skip to content

feat(factors): close D5 C5 — four-leg reconciliation green across all eleven factors - #113

Merged
StackOverFlow11 merged 11 commits into
mainfrom
feat/factor-refactor-d5-c5-audit-v2
Jul 30, 2026
Merged

feat(factors): close D5 C5 — four-leg reconciliation green across all eleven factors#113
StackOverFlow11 merged 11 commits into
mainfrom
feat/factor-refactor-d5-c5-audit-v2

Conversation

@StackOverFlow11

Copy link
Copy Markdown
Owner

D5 C5(四腿全量对账)收口。11 因子 × 3 模式 = 33 格全绿,审计报告填完,C5 通过 ⇒ 满足 C6 的前置条件之一。

结果

结果
eval(11 因子 × 2 book mode) 22/22 rc=0
panels 11/11 rc=0,unclassified 全 0
anchors 11/11 rc=0,failed 全 0
reports 11/11 rc=0,零类外叶子
cache-only stk_mins_live_calls 非零命中 0

做了什么

  • F2 扩类(bars-only 臂):600623.SH 那根整数成交量 bar 压在 same-slot μ+σ 阈值上、pandas rolling 浮点累加路径依赖加载起点(thr 差 ~4e-13)⇒ vol > thr 翻转。窗口/symbol 未动,按因子设相对界(kurtosis ≤5e-3 / relative_vwap ≤1e-5,实测 2.904e-3 / 1.530e-6),cap 25。
  • F3frozen_finite_new_nan 进 warmup 方向集(限 pooled + 早区);新类 warmup_sparse_valid_day_tail
  • F4:float-dust 谓词(abs ≤1e-12)前移到 _in_warmup 分支之前(按机制识别,不按位置)⇒ 月计数天然单调。实测对已绿因子无副作用,被否决的方案②未被启用
  • F5 修复:后缀传进写出层直接写 _bookclose,删掉事后 os.replace。此前 close 模式先用共享 stem 写再移动,decision→close 顺序跑完会销毁 decision 的 with_book 三件套
  • NIT-1:panels summary 加逐类 max_rel(headline 在任何分桶之前更新,与 unclassified=0 并排会诱导"容差没牙"的误读)。

过程中查出的三个证据基窟窿

这些比修复本身更重要——它们说明"跑绿"曾经只覆盖"我们看过的那部分"。

  1. 交接文档的结果总表把 peak_ridge_amount_ratio_20 的 panels 记成通过,而日志是 unclassified=30 / ok=False 它是 F3 同族的第三个因子。⇒ F2/F3/F4 的边界参数原本推自一个残缺证据基。据此对 11 因子 × 3 模式做了表 vs 日志全量对照(33 格,只此 1 格记错)。
  2. 整轮 C5 全量跑用的是 PR test(eval): D5 C5 prep — warmup cell ceiling + migration map + audit skeleton #109 之前的判据——warmup_ceiling35de346(07-28 02:08)引入,而幸存的 8 条 07-28 日志行没有一条带这个字段。⇒ bounded warmup 的 cell ceiling 一次都没上过场,而审计骨架把它写成生效中的闸门。本 PR 在当前 HEAD 上重跑,ceiling 真正上场并通过。
  3. reports 腿的 11 个 1 全部是 F5 的 FileNotFoundError,不是判定结果。 ⇒ 该腿此前只被真正观测过一个因子。本 PR 是它对其余 10 个因子的首次观测——两类此前无人见过的漂移因此第一次露面(诊断 sink 的纯新增 section;D2 迁移导致的 spec.description provenance 改写),两者都不是数值差异,均已登记。

评审揪出的两处 overclaim(都已改准)

HIGH-1:「白名单是该密度谓词的枚举」是假的。 谓词在窗口内允许 1,250 对,类实际覆盖 18 对——69 倍差

处置是改措辞、不改谓词(改谓词会把闸放宽约 69 倍)。该类现在如实标为**「枚举类 + 必要条件守卫」而非机制类**——这个区别是操作性的:读"机制类"的人会以为可以照机制推导着扩。证伪门槛从"低于中位"(按定义约一半的票满足,近乎恒真)收紧到低于 p25,仍 18/18 通过。600906.SH 的不对称证据射程也写准了:只有否定方向(稠密 ⇒ 不进簇)成立,且它在对照因子上只高于中位 1 天,是单点证据

HIGH-2:§四的核心推论是恒真。 「若引擎回归藏在别的指标里,C−B 会在那些行非零」——评审用 mutation 直接证否:把 ic_meanA/B/C 三本书里同时 ×1.5C−B 仍精确为 0.0,因为那四个指标对书构造性免疫,引擎回归会让三者一起动、在差里对消。

已替换为真有信息量的版本:artifact 层 C−B 非零共 174 个叶子,全部落在 sections[3] + book_view + verdict reasons ⇒ 书视图的爆炸半径被限制住,174/174 通过;并明写真正检出引擎回归的是 no_book / bookclose 两格 strict 对冻结基线的逐叶子闸(存在且通过)。这条被证否的推理连同其 mutation 证据留在报告里,没有悄悄换掉——与 compare_postmerge.py 那次空对账的处理一致。

贴边的边界(新增 §三之四)

反棘轮把边界钉死了,读者得知道有多紧:jump float_tail 101/101(0 格)、sparse-tail 窗口右端 2021-11-15(0 天)、cap 19/20(1 格)、p25 密度守卫 35 天 vs 门槛 36(1 天)

其中 p25 那道门红了就是 STOP-AND-REPORT,不许把门槛放回中位——反棘轮明令禁止,而它红的时候恰恰最诱人(中位门会放行,于是"改一个数就绿了")。

方法论:本轮出现 5 次"证据/清单打不中它要支持的那个主张"

全部如实留档,没有一次是被测试抓住的:

  1. 白名单参数化测试读被测常量本身 ⇒ 删一只票只是少一个 case,两次都绿(本仓第 10 例"不可能失败的测试")。
  2. .pyc 按 (mtime, size) 校验 ⇒ 等长改写在同一秒内会让"还原"后的源码被变异后的字节码顶替。据此把先前 13 条 mutation 全部重跑——只修 harness 不重跑等于把已污染的证据留在案上。
  3. 位移测试为错误的理由通过(尾部多出的下标就足以满足断言)。
  4. MD 前缀漏了 to_section 加的两个计算属性;且其覆盖测试两侧同源故对该元组无牙(现已钉到四个 coverage 类 @property 名字的并集)。
  5. 「p25 严格更紧」在案的 mutation 打不中它——两条证据在中位门下也会红。已换成真正的证人(000031.SZ:低于中位、高于 p25,p25 下红、中位下放行;带内共 151 只票)。

同一主张写四遍、改准三遍漏一遍的情况出现两次;已把权威处收敛到参数所在的 docstring,其余改为指针(两份 Markdown 与 Python docstring 无共享 prose 机制,这个限制如实写明)。

重跑射程(实测,非论证)

扰动 qt/factor_eval_reconcile.py 后重算 5 个因子的 code_hash5/5 逐字不变负对照扰动 factors/compute/minute/primitives.py(在 shared set 内)→ 5/5 全变 ⇒ 探针非空。harness 在 code_hash 射程之外、store 不失效。

Gates(lead 独立重跑,未采信 subagent 自报)

pytest -p no:warnings 2606 passed · ruff clean · phase0 锚 ic 0.9600 / annual 0.8408 · validate-config 32/32 · exec_baseline_freeze --verify 77/77 @ 45c14aa · secret scan 0 · 陈旧措辞 grep 0 · 审计报告 TBD 0 · 编目对 main 纯追加(0 删除行) · 冻结基线 今日 0 个文件被写

已登记、本 PR 不改

_make_logger 是 truncate 的(重跑会静默销毁同名日志的唯一副本);jump 的 reports 腿对值改动实际不设闸(593 个 diff 里 518 个走既有更正承载登记 #11with_book(decision) 是 report-only),值级依据应看 no_book/bookclose 两个 strict 格与 panels 腿。

…after

The close book mode used to write the with-book reports under the SHARED
`{stem}_exec_with_book.*` names and `os.replace` them to `_bookclose`
afterwards. Running the two book modes in the order decision -> close
therefore destroyed the decision run's three with-book artifacts: the close
run overwrote them, then moved them aside. All eleven factors lost their
`exec_with_book.*` in the D5 C5 full run, and the reconcile's reports leg
died on a bare FileNotFoundError naming a file that had existed minutes
earlier.

The suffix now travels INTO the write-out layer
(`run_exec_basis_evaluation(..., with_book_suffix=)`, default "" so every
existing caller keeps its byte-identical paths), so each book mode only ever
writes its own file names and the run order stops mattering.

Two tests, at the two levels the behaviour spans: the writer's own
(two real runs into one report dir, first run's bytes must survive) and the
runner's (decision then close, with the stub applying the suffix at write
time exactly as the real writer does, so a wrong suffix writes wrong names).
…oncile

Three judgement gaps the D5 C5 full reconciliation exposed. Each one is
bounded on every axis it can be bounded on, and each bound has a reverse test
that fails one step outside it.

F2 - `threshold_flip_contamination` gains a BARS-ONLY arm: the same
600623.SH critical-bar flip already registered for the cross-sectional
factors, on factors that have no per-date OLS to spread it. Only the directly
affected symbol qualifies; the bound is RELATIVE and per factor
(peak_interval_kurtosis 5e-3, valley_relative_vwap 1e-5) because the two live
on unrelated value scales - measured |diff| 1.07e-2 vs 1.53e-6, so one
absolute bound cannot serve both. At most 25 cells; an unregistered factor
never gets the class.

F3 - two more surfaces of the anchor truncation. (1) frozen-finite ->
new-NaN joins the warmup direction set for VALID-DAY POOLED factors inside
the early region only: their emission is gated by a count of valid days, so
the two geometries can accumulate a different number of them at the anchor
edge. A bounded factor has no such gate and keeps failing. (2) A new class
`warmup_sparse_valid_day_tail` for the sparse names that carry that early
difference past the early window: window [2021-11-01, 2021-11-12], a symbol
whitelist, at most 20 cells, pooled factors only. Every (factor, symbol) pair
in the class sits below that factor's own median emission density - including
600906.SH, which is below it for peak_ridge and above it for ridge, and
appears only in the former's cluster.

F4 - the ABSOLUTE float-dust predicate moves ahead of every region branch, so
a cell is classified by its mechanism rather than by where it lands.
Machine-precision dust is uniform across the grid; letting the region decide
first made a handful of dust cells count as warmup cells, and their monthly
counts then failed the pooled non-increasing gate on noise
(intraday_amp_cut: 2021-08/09/10 = 8/3/9 cells, every one |diff| <= 1e-12).
Measured on the same served panels, before -> after: amp_cut's months
{07:8198, 08:11, 09:3, 10:9} -> {07:8198} and its float tail 758 -> 798 of
1000; vpq 883 -> 925 of 1000; jump, which sits exactly ON its cap of 101,
gains nothing. The relative arm stays where it was.

Also prints the per-class max relative difference in the panels summary
(review NIT-1): the headline max is taken before any bucketing, so a 9.03e-01
that lies entirely inside a registered class was being printed next to
`unclassified=0` and reading like an ungated tolerance. One-sided cells
report inf, never 0.0.
Names every missing decision-mode artifact and the command that produces
them, instead of dying on whichever file it opened first; a half-written
_bookclose pair is its own readable error.
The F4 argument is that the three disputed months are indistinguishable from
every other month, so it has to be backed by the distribution rather than by
the three counts alone: 798 cells over all 59 months, 3-28 per month (median
12), 100% of them on the absolute arm, with the disputed 11/3/9 inside that
spread. Also records that the two representative factors were re-run through
the real harness, not only the offline classifier.
…eport

The C5 first-run results existed only in artifacts/logs/, which is gitignored
and lives on one machine — and `_make_logger` opens in truncate mode, so the
phase-B rerun will overwrite every one of those files. This repo has already
lost a full run's log once to a /tmp cleanup. The report is the durable
carrier; the logs are not.

Adds the per-factor x three-mode pre-fix table with each leg's counters, the
unclassified count and the ok flag, and states the provenance of every row:
eight transcribed from surviving log text, two from a verbatim capture taken
before my own validation reruns overwrote them (cross-checked against an
offline re-derivation on the same served panels under main's classifier), and
one from catalogue section 7.6. The offline re-derivation reproduces the table
cell for cell on the nine non-amp_marginal factors, so it stands as an
evidence base without having to trust the logs.

Corrects section 1.2's characterisation of the bounded warmup cell ceiling. It
is (w-1) x |symbols|, a STRUCTURAL upper bound that cannot be exceeded unless
the date-boundary predicate itself is broken — a backstop that only fires once
another guard has already failed, not a magnitude or density gate. The
measured 91.5% occupancy is not "8.5% of headroom"; it is the fraction of
symbols with a differing warmup cell, which is structurally near one. And it
does NOT close C4a review LOW-1: the class still has no magnitude bound at
all, with a measured max rel of 1.996 (a sign flip) inside it.

Registers three holes in the evidence base: the first C5 run executed on a
classifier predating PR #109 so the ceiling never ran (impact settled — all
three bounded factors clear it on current HEAD); the reports leg produced zero
judgements for all eleven factors because every rc=1 was F5, making phase B
its first observation and leaving the frozen baseline as the A/B/C
decomposition's only reference object; and `_make_logger`'s truncate mode as a
known operational risk, same failure family as D5a's "one run destroys the
only frozen baseline" — not fixed here, that is a behaviour change.
…tors

The window and whitelist were derived from TWO factors. The third,
peak_ridge_amount_ratio_20, was failing at the time with unclassified=30 and
had been recorded as passing in the handoff's results table — so it never
entered the evidence base. This is not "widening the class to cover it": the
old parameters were fitted to an incomplete sample, and these are derived from
all three. How the omission happened is recorded in the catalogue: the reports
leg was rc=1 for all eleven factors at once (F5 destroying its inputs), one
whole column was uniformly dead, and attention went to which factors carried
an F1-F4 label rather than back to the logs for that row.

Membership is now stated as the MECHANISM it claims — the symbol's valid-day
emission density on that factor is below that factor's own median — with the
whitelist as that criterion's enumeration in this window. The claim was
falsified rather than assumed: all 18 in-class (factor, symbol) pairs sit
below their factor's median, 0 violate it, and no whitelisted name is dense on
every affected factor. The sharpest confirmation is an asymmetry the mechanism
predicts: 600906.SH is below its median on peak_ridge and above it on ridge,
and appears in the former's cluster and not the latter's. Control group: the
dense pooled factor volume_peak_count_20 has zero cells on those same names.
Two committed tests re-run the check whenever the frozen panels are on disk.

The catalogue records an ANTI-RATCHET constraint: this class must not be
widened again. Keep relaxing and unclassified can always be driven to zero, at
which point the class describes the residual instead of a mechanism and "an
uncatalogued difference is a failure" has been repealed by degrees. A future
factor needing it is evidence of exactly that -> stop and report.

Boundaries: window [2021-11-01, 2021-11-15], nine names, cap unchanged at 20
(measured 13/13/19). Reverse tests re-cut to the new edge from both sides, and
the whitelist test now reads an INDEPENDENTLY written expectation - deriving
the parametrization from the constant under test could not detect a name being
dropped (measured: removing one took the run from 9 passed to 8 passed, green
both times).
…n anyone

Until phase B this leg had produced ZERO judgements for ten of the eleven
factors — every rc=1 was F5 destroying its inputs — so neither drift below had
ever been seen. Same shape as the peak_ridge case: the registrations were
fitted to a sample of one, not widened after the fact.

A. The diagnostics-sink coverage disclosures arrive as an appended section.
REGISTERED_EXTRA_SECTIONS held only vpq's, because vpq was the only factor
whose reports leg had ever run. Re-verified per factor and per grid rather
than assumed: every leaf under the appended index is old=None with no value
rewrite; the verdict labels are unchanged; and there is NO INDEX DISPLACEMENT
— sections 8 -> 9 with the first eight names pairwise identical and the
disclosure appended last. That third check matters because leaves are compared
position by position, so a section inserted mid-list would shift every later
one and "the rest match" would be an illusion. It is detected rather than
assumed, and a test pins the mechanism.

B. spec.description value rewrites, registered as per-factor EXACT (old, new)
pairs. This is a DIFFERENT class from the section 7 spec drift, which covers
added keys; this is an existing key whose value changed. No predicate: a
blanket "description rewrites are fine" rule would wave through the day a
factor's stated meaning really changes, and spec.description is the field a
reader uses to learn what the factor computes. Cause is the D2 taxonomy move,
and D2 was measured bitwise identical (PR #89, 14/14 at max_rel_diff=0.0), so
this is provenance, not semantics — no version bump, no correction carrier, by
the same criterion as F1.

Both carry anti-ratchet notes: a further member is evidence the registration
describes residuals rather than a mechanism -> stop and report.

Two defects found while doing it, both by observation rather than by a test:
the displacement test passed for the WRONG REASON (a trailing extra index
satisfied its weaker assertion; registering name changes left it green), now
asserting the displaced sections[k].name directly; and the Markdown prefixes,
derived from dataclasses.fields alone, missed the two COMPUTED properties
to_section adds on top of asdict, leaving three rendered lines unregistered
with the JSON side already green. That tuple is now author-once in
qt.factor_eval_disclosures and a test compares the prefixes against the keys a
real section payload actually carries.

Reports leg: 11/11 rc=0, zero unregistered leaves. Scope of the rerun was
established mechanically, not asserted: an AST diff against the phase-B commit
shows the changed functions are exactly _classify_value_change,
diff_report_json and run_reports_mode, with every panels/anchors entry point
byte-identical.
…sults

Sections 3/4/5 now carry the real numbers instead of TBD: per-factor results
for all three legs (33 cells, every one rc=0, unclassified and failed both 0),
the with_book decomposition, and the eleven-row verdict table.

The decomposition is PER METRIC, as required — an aggregate "with_book moved
because the book view changed" would let an engine regression hide behind an
intended change. Split out, it says something stronger: A and B carry
identical deltas on IC / ICIR / NW-t / N_eff (a book does not enter those), and
C-B is non-zero on exactly 11 of 57 metric rows, every one of them the
incremental ICIR — the only metric a book can reach. Verdicts and all three
axis labels are unchanged for 11/11 factors in every grid.

Also records jump_amount_corr_20's float tail sitting at 101/101 with ZERO
headroom: the cap was calibrated on that factor's own observed count, so one
more cell fails the leg, and the failure would read as "cap exceeded" rather
than pointing at whatever moved.

Header no longer claims to be a skeleton.
HIGH-1. The sparse-tail class was described as a MECHANISM whose whitelist is
the mechanism's enumeration. Measured, the density predicate admits 420 + 404
+ 426 = 1,250 (factor, symbol) pairs inside the window against the 18 the
class covers — roughly 69x. So the predicate is necessary and nowhere near
sufficient: it CONSTRAINS the member set without DETERMINING it, and the
whitelist is the enumeration of OBSERVED cells. Relabelled as an enumeration
class with a necessary-condition guard, in all three places that said
otherwise. The label is operational, not cosmetic: "mechanism class" invites
the next person to extend it by derivation, which is exactly what must not
happen, and the anti-ratchet note now says so explicitly.

The guard moves from the median to the 25th percentile. A median gate passes
half the universe by construction, so it could hardly fail; all 18 in-class
pairs sit below their factor's p25 (worst percentile rank 21.7%, 15 of 18
below 3%), so tightening costs nothing and turns an almost-unfailable check
into one that can fail. Mutation evidence both ways: a dense-everywhere name
is still caught under p25, and loosening back to the median still passes,
which is what shows p25 is strictly tighter on this data.

The 600906.SH asymmetry now carries its reach: it is a CONDITIONAL prediction
whose NEGATIVE direction alone holds (dense => absent, corroborated by the
dense control factor's zero cells), while sparse => present is false by that
same 69x — and it is one data point whose dense side clears its median by a
single day.

HIGH-2. The with_book section claimed "if an engine regression hid in other
metrics, C-B would be non-zero there". That inference is VACUOUS and the
review demonstrated it: scaling ic_mean by 1.5 in all three books leaves C-B
at exactly 0.0, because A/B/C hold identical values on those metrics and a
regression moves all three and cancels. The "11 of 57 rows" framing was also a
statement about the table's own row selection; at artifact level C-B is
non-zero on 174 leaves. Replaced with what the data does support — those
174/174 land in sections[3] plus book_view plus verdict reasons, so the book
view's blast radius is bounded — and the report now says plainly that engine
regressions are caught by the strict no_book/bookclose gates against the
frozen baseline, not by C-B. The refuted inference stays ON THE RECORD with
its mutation, as the compare_postmerge empty-reconcile was recorded: the audit
conclusion is unchanged, what was wrong is the attribution.

LOW-1. The Markdown-prefix test could not fail for the tuple it named: both
sides read DERIVED_PAYLOAD_PROPERTIES, so deleting an entry left it green
(measured). Its docstring now states that reach, and a new test anchors the
tuple to the union of @Property names declared on the coverage classes —
which does go red on that exact edit.

LOW-2. The report flagged only jump's 101/101. Added the sparse-tail window's
right edge (drawn AT its last cell: zero days of headroom) and its cell cap at
19/20, alongside the looser bounds, so a reader can see which boundaries are
one step from firing.

NIT-1: the appended-section re-verification denominator is 12 grids (4 factors
x 3), not 8. NIT-2: registered, not fixed — jump's reports leg and the
decision-book grid do not gate VALUE changes (518 of 593 diffs ride the
registered correction carrier; the decision grid is report-only), while
structural additions and removals still gate.

Scope: confirmed empirically rather than argued — perturbing this harness
module leaves all five sampled factor code_hash keys unchanged, so the store
stays valid and only the reports leg needed re-running (5 affected factors
re-run, rc=0, zero unregistered).
The delta review found the old wording surviving in three places the previous
pass missed — and the worst one asserted a MEDIAN gate fifteen lines above the
code that reads p25, so a reader had two contradicting sentences on one screen.
That is the exact defect this rework existed to remove, reappearing because
the same claim was written in four places and only three were corrected.

Rather than correct a fourth copy, the claim is now AUTHORED ONCE, on
SPARSE_VALID_DAY_TAIL_SYMBOLS: the module docstring and the test banner point
at it instead of restating it. The two Markdown documents still have to
restate — no mechanism shares prose between a Python docstring and a .md file
— so they carry an explicit pointer to where the authoritative wording lives.
The catalogue's dangling back-reference (it pointed at a heading this rework
had already renamed, and quoted the retired claim while doing so) now
describes the old title instead of quoting it: copying it verbatim would keep
any scan from reaching zero and would let a skimming reader mistake it for the
live claim.

The "p25 is strictly tighter" mutation record is replaced, because neither
entry on file actually tested that claim: "loosen back to the median -> still
passes" only shows both gates clear the REGISTERED members, and the injected
dense name 000009.SZ fails under the median gate too. The witness that
discriminates is a name from the 151-symbol band that sits below the median
but at or above p25 — 000031.SZ, 38/38/38 against p25 36 and medians 41/41/40
— which FAILS under p25 and PASSES under the median. Both the new evidence and
the old evidence's miss are recorded: this is the same defect as the rest of
the round, a piece of evidence that did not support the claim it was filed
under.

The p25 guard's own headroom joins the near-edge table: its threshold is 36
emitting days and the tightest member has 35, so ONE day. It is flagged the
way the other near-edge bounds are, with the note that a red here is
stop-and-report and must not be answered by moving the threshold back to the
median — the move anti-ratchet forbids, and the one that looks most attractive
precisely when the guard fires.

Section 6's numbering skipped 8.
The p25 guard's row was inserted without updating the two counts below it —
the heading still said 'the first three' above four bullets, and the closing
sentence said 'these three'. Same shape as the section-6 numbering skip: an
insertion whose counts did not follow. In a PR whose subject is claims
matching what is actually there, shipping a table that says three and lists
four is the defect it exists to remove.
@StackOverFlow11
StackOverFlow11 merged commit a5bbbba into main Jul 30, 2026
@StackOverFlow11
StackOverFlow11 deleted the feat/factor-refactor-d5-c5-audit-v2 branch July 30, 2026 21:01
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