Skip to content

feat(factors): D1 — factor registry, PanelField requires, FactorSpec contract v1.0 - #86

Merged
StackOverFlow11 merged 3 commits into
mainfrom
feat/factor-refactor-d1-registry
Jul 23, 2026
Merged

feat(factors): D1 — factor registry, PanelField requires, FactorSpec contract v1.0#86
StackOverFlow11 merged 3 commits into
mainfrom
feat/factor-refactor-d1-registry

Conversation

@StackOverFlow11

Copy link
Copy Markdown
Owner

Second step (D1) of the factor-layer refactor per v3.2 design (§3.1 / §九 D1). Identity of this step: dispatch swap + declarations — zero factor-math changes.

What (+1751/−69, 19 files, 3 commits)

  • factors/requires.pyPanelField(field, source), endpoint-only (R25: no factor-on-factor refs); construction-time require_known_source (R8) + field-level market_daily validation (R7) via the D0 policy module.
  • factors/spec.py → contract v1.0 (explicit authorized change to the frozen contract): three new mandatory declaration dims (requires / adjustment / overnight_boundary), values coerced to the data.availability_policy enums (single source); D0 §1.1 static check (adjustment=none forbids OHLC requires, one direction only, converse documented legal). Existing enforcement untouched: metaclass double-check, expected_ic_sign pre-registration, property-spec deferred validation (factors/base.py diff empty).
  • factors/registry/register/build/requirements + explicit builtin.py list (no pkgutil); three-timepoint naming checks (define / build / config-parse); exact-before-prefix dispatch preserving the old chain's semantics, coercions and error messages; require_legal_pairing forwarding (deep wiring lands D4). All 18 shipped Factor classes registered (incl. the 11 minute surfaces the old chain never dispatched — registry now covers the full D4 surface).
  • Declarations on all 18 factor classes — the 14 closing-set factors follow the D0 pre-assignment table verbatim (21-row parametrized test with hand-written literals locks it); daily factors derived with per-class docstring evidence; minute_ideal_amplitude lands CROSSED_DISCLOSED-candidate with the missing bias-measurement duty pinned as a tested "MISSING…D2" docstring debt.
  • qt/pipeline.py::_build_factors — body becomes a pure registry.build() loop (if/elif chain retired, −55); _maybe_enrich_*, all 11 eval runners, intraday framework: zero changes (verified per-file empty diffs).
  • qt/config.py — enabled factor names resolve against the registry at parse time (fail-fast enhancement; disabled entries unchecked, mirroring historical behavior).

Zero-math evidence

  • Compute files' removed lines = 4 × the same import line; grep '^[+-].*def compute' over compute diffs = zero hits (review, hunk-by-hunk).
  • phase0 anchor ic 0.9600 / annual 0.8408 unchanged (free acceptance for a dispatch-only step, per v3.2 R10).

Disclosed behavior deltas (intentional / accidental)

  1. Unknown factor name now fails at config-parse (earlier boundary; old build-time path still locked by registry tests).
  2. The old chain eagerly ran int(params.get("window",20)) for every factor; a bogus window on a financial/value factor used to raise accidentally — no longer reproduced. Verified: no config or test depends on it.

Acceptance

  • Adversarial review: APPROVE-WITH-NITS — 0 CRIT / 0 HIGH; the 1 MEDIUM ("AGENTS.md truncated") was adjudicated a diff-base artifact: the branch forked from 703bd3e before docs PR docs: record factor-layer refactor kickoff (v3.2 + review + D0) #85 (which re-synced AGENTS.md) merged; two-dot git diff main showed docs: record factor-layer refactor kickoff (v3.2 + review + D0) #85 inverted. Merge-base diff (main...HEAD) confirms 19 files, AGENTS.md/CLAUDE.md untouched. 1 LOW recorded as a future-facing note (_PRICE_CHANNEL_FIELDS matches field names regardless of source — no conflict among current 18 factors; revisit if a non-market endpoint ever names a column "close").
  • Lead-independent gates: pytest 1879 passed (= 1806 + 73, zero displaced), ruff clean, validate-config 31/31, phase0 anchor verbatim, secret scan on diff 0 hits.

Next (still step D1 per v3.2 §九): end-of-D1 panel freeze (R11) — dump the 14 closing-set factor value panels + manifest into git, before any D2 math migration.

Factor-refactor D1, first slice (design v3.2 §3.1; D0 taxonomy in
docs/factors/refactor_d0_contract.md). EXPLICITLY AUTHORIZED change to the
previously frozen FactorSpec contract (stated outright per the #74
precedent): three declaration dimensions become MANDATORY on every spec —

* requires: endpoint-level PanelField inputs (R25 endpoint-only; R8
  endpoint closure + R7 market_daily field-level validation fire at
  declaration time inside PanelField);
* adjustment / overnight_boundary: value domains are the
  data.availability_policy enums (single source, no second list).

A missing declaration is a readable definition-/construction-time error,
same as the pre-existing mandatory fields; the metaclass double-check and
expected_ic_sign pre-registration are untouched. The D0 §1.1 static check
(adjustment=none must not require a price-channel field) fires at
construction; the converse is deliberately unchecked (VWAP-ratio factors
carry price via sum(amount)/sum(volume) with no OHLC field).

All 18 shipped Factor classes declare per the D0 pre-assignment table
(evidence cited in each spec docstring): the finishing-line 14 land
row-for-row, including minute_ideal_amplitude's CROSSED_DISCLOSED
candidate whose missing PR-L-style deviation measurement is pinned in the
docstring as a D2 obligation; valley_price_quantile additionally requires
market_daily close (the T-1 rev20 leg reads the qfq daily panel — a real
second endpoint beyond its minute fields). Factor math untouched: zero
compute() changes.
…ecks

Factor-refactor D1, second slice. factors/registry is THE one name ->
class mapping (red line #4): register() re-runs the metaclass mandatory-
spec validation (red line #1 — reused, not reimplemented), demands a
concrete Factor, checks a static class-attribute spec against the class
name at definition time, and rejects duplicate keys and mutually-
prefixing prefixes readably. build() reproduces the retired
_build_factors params coercion exactly (same int()/str() calls, same
defaults) and enforces config name == instance name == spec.factor_id —
property specs are checked on the built INSTANCE, never forced back into
class attributes (pit #2). requirements() aggregates deduplicated
PanelField needs for the D4 materializer; require_legal_pairing()
forwards the availability-policy view x basis legality check (deep
wiring is D4 — D1 only guarantees it is callable from the factor layer).

builtin.py is an explicit import + register list (pit #3: no pkgutil
auto-discovery) covering the FULL 18-class surface in the retired
chain's dispatch order, including the 11 minute-derived factors the
chain never dispatched (eval runners construct them directly today).
Collision tests run on fresh FactorRegistry instances so the default
registry is never polluted.
Factor-refactor D1, final slice — the dispatch replacement that IS D1's
identity. _build_factors' if/elif chain (qt/pipeline.py:769) is retired
for registry.build(): same classes, same params coercion, same config
order; the no-enabled-factor and duplicate-name guards stay verbatim in
_build_factors; unknown names and name/params mismatches keep readable
errors (now single-sourced in the registry). The now-unused factor-class
imports are dropped; FinancialFactor / ValueFactor / SUPPORTED_FIELDS
stay — the _maybe_enrich_* isinstance dispatch is D4/D6 scope, untouched.

qt.config.FactorCfg gains the third naming timepoint: every ENABLED
factor entry resolves its name in the registry at parse time, so
validate-config / load_config reject an unknown name before any run
(pydantic ValidationError subclasses ValueError and carries the
registry's message). Disabled entries stay unchecked, mirroring
_build_factors. test_unknown_factor_name_raises moves to the parse-time
boundary accordingly; the direct registry.build unknown-name path is
locked in tests/test_factor_registry.py.

Behavior anchors: run-phase0 demo unchanged (ic 0.9600 / annual 0.8408),
all 31 configs validate, full suite green — factor math untouched by D1.
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