docs: record factor-learning validation gate session (2026-08-27)

This commit is contained in:
Kunthawat Greethong
2026-08-27 12:19:50 +07:00
parent ae814c341e
commit 59c97b5139
2 changed files with 24 additions and 0 deletions

View File

@@ -193,6 +193,29 @@ Implement dated dividend events and a provenance-validated PIT score provider be
---
## Session 2026-08-27 — Factor-learning validation gate (verified + pushed)
**Branch:** main · **HEAD:** ae814c3 (pushed `9700f6b..ae814c3`)
### Completed this session (verified + pushed)
- **Validation gate** `backend/app/weight_learning.py`: `apply_validation_gate(train_ics, holdout_ics)` marks a factor `validated=True` only when total sample >= MIN_SAMPLE_PERIODS (12), each window >= its minimum, train AND holdout IC both beat baseline (BASELINE_IC=0) and agree in sign, and pooled |t| > MIN_IC_TSTAT (1.0). `FactorLearning` now carries `ic_train`/`ic_holdout`/`validated`/`gate_notes`.
- **No auto-apply enforced**: `apply_weight_update` now keeps `new_weight == old_weight` for any unvalidated factor — weights only move after the gate passes with explicit evidence.
- **Gated learner**: `learn_momentum_gated(...)` builds PIT momentum ICs then splits chronologically into train/holdout and applies the gate. Wired into `GET /api/v1/learning/momentum`, which now surfaces `ic_train`, `ic_holdout`, `validated`, `gate_notes`.
- **Tests**: rewritten suite + gate (16) — full backend **286 passed** (was 280).
### Verified
- Backend full suite **286 passed**; compileall, `git diff --check`, static scan clean.
- Live probe on the current price archive: momentum `validated=false`, `ic_train=0.0711`, `ic_holdout=-0.1143`, `gate_notes=['IC not above baseline (0.0711/-0.1143)']`, `new_weight=None` — the honest outcome: momentum is not validated, weight stays unchanged.
### Honest scope / known limitations
- `learn_momentum_gated` is the only factor with a real PIT series; macro/demographic factors are still unattributable (no vintages) and stay `blocked`.
- The gate is intentionally strict; a validated factor merely *suggests* a weight delta — applying it to production is a separate, explicit step.
### Exact next action
All six planned PIT/enabler milestones are complete and pushed. Remaining (deferred): a real per-stock **dividend-history collector** (siamchart `/stock-info/<SYMBOL>/`) to upgrade symbols from `dps_annual_proxy` to `dated_ledger` (needs live network access). Do not reset or stage unrelated pre-existing working-tree changes.
---
## Session 2026-08-27 — Real forward-test lifecycle (verified + pushed)
**Branch:** main · **HEAD:** 80c6d79 (pushed `6d9c283` + `80c6d79`)

View File

@@ -102,3 +102,4 @@
- Dated dividend cash-flow ledger (2026-08-27, commits `068dff2` + `5b8b6d1`): added `dividend_ledger.py` (per-symbol dated store: ex_date/record_date/pay_date/per_share/source/estimate flag; `credit_dividends` credits per_share×qty when payment due, fail-closed with no entry; `build_dps_ledger` builds per-share estimate rows from siamchart ratios.DPS). `run_backtest` accepts `dividend_ledger` and reports `dividend_method` = `dated_ledger` (real rows) | `dps_annual_proxy` (estimate) | `final_holdings_yield_proxy` (legacy). `/api/v1/backtest` accepts `use_ledger`. Frontend maps method to an honest Thai label + Proxy badge on result card and history rows. Full backend **266 passed** (was 255). Honest scope: DPS rows are estimates (no ex-date history in snapshot yet); real dated cash flows require collecting per-stock dividend history, which upgrades a symbol to `dated_ledger`.
- PIT siamchart vintage store (2026-08-27, commit `887e9c9`): added `siamchart_vintages.py` (append-only hash-chained store of every collected siamchart snapshot; `snapshot_at(as_of)` returns the newest snapshot with `retrieved_at <= as_of` — anti-look-ahead, fail-closed). `PitScoreProvider` accepts `siamchart_store`; when wired the fundamental dimension reads the snapshot knowable at as_of (`pit_grade='pit'`) and `partial_pit` no longer forces true. `/api/v1/backtest use_pit` seeds the first vintage from the current snapshot. Full backend **273 passed** (was 266). Honest scope: snapshot reconstruction forward only; EPS year-keys not tied to calendar years (growth = latest-vs-prior).
- Real forward-test lifecycle (2026-08-27, commits `6d9c283` + `80c6d79`): the cosmetic "forward" (same single-pass backtest, different mode string) is replaced with a durable, frozen-signal paper-portfolio lifecycle. `forward_test.py` `ForwardTestStore` (thread-safe JSON store) with status flow: frozen (signals snapshotted immutable) → executed (fills 50/20/30 at post-freeze prices) → marked (mark-to-market equity series) → matured (net_return). New routes: `GET /api/v1/forward(+<id>)`, `POST /api/v1/forward` (create+execute, `use_pit` freeze), `POST /<id>/mark`, `POST /<id>/mature`; store at `data/forward/runs.json` (survives restarts). UI simulation tab: forward calls `/api/v1/forward`, loads runs, shows status/non-PIT/holdings + Mark/Mature per run. Full backend **280 passed** (was 273). Honest scope: score source at CREATE may be current board (`non_pit=true` tagged); paper-only.
- Factor-learning validation gate (2026-08-27, commit `ae814c3`): closed the P4 "no auto-apply" loop — `weight_learning.py` now splits a chronological IC series into train + holdout via `apply_validation_gate`, and a factor is `validated=True` only when total sample >= 12, each window >= its min, train & holdout IC both beat baseline (BASELINE_IC=0) and agree in sign, and pooled |t| > 1.0. `apply_weight_update` keeps the weight unchanged for any unvalidated factor (no auto-apply); only validated factors move. `learn_momentum_gated` wired into `/api/v1/learning/momentum`, surfacing `ic_train`/`ic_holdout`/`validated`/`gate_notes`. Full backend **286 passed** (was 280). Live probe: momentum validated=false, gate_note "IC not above baseline (0.0711/-0.1143)" — weight unchanged.