Files
set50-system/docs/engineering-log.md
2026-08-28 11:42:11 +07:00

110 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Engineering Log — SET50 Alternative Data Platform
## Current status
| Milestone | Status | Evidence | Next action |
|---|---|---|---|
| M0 repo foundation | complete | Flask API, Vue/Vite shell | keep research/paper guardrails |
| M1 BOT Tourism adapter | complete | 18 tests, live BOT fetch, raw/snapshot persistence | validate multiple vintages |
| M2 vintage collector | complete | 25 tests, manifest idempotency, live collector and point-in-time API | collect independent releases |
| M2.3 event-study gate | complete/blocked | 30 tests, pure engine and truthful 409 readiness API | add point-in-time price provider |
| M2.4 price snapshot adapter | complete/blocked | 38 tests, live 9-symbol Yahoo snapshot, revised-history gate | evaluate point-in-time price source |
| M2.5 research runner + durable paper ledger | complete/blocked | 54 tests, immutable blocked report, restart-safe paper path, live API/UI workflow | collect independent releases and point-in-time prices |
| M2.5 integrity hardening | complete | 79 tests, canonical VintageStore replay, report and manifest-entry identity/hash binding, explicit resumable legacy migration, fail-closed manifest/snapshot reads/writes/encoding/I/O, semantic replay-shape validation, exact-schema independent review passed | protect point-in-time gate with a trusted deployment secret if threat model expands |
| M2.6 paper auth policy | complete/blocked | 85 tests, explicit loopback-only demo mode, protected token mode preserved, UI warning and startup guard | keep demo local; use token mode for shared/network access |
| M2.7 PIT price archive contract | complete/blocked | 93 tests, explicit `pit-daily-v1` contract, per-bar known-at validation, market-timezone no-lookahead join, immutable manifest binding; live provider evidence not yet established | obtain a provider archive with release-time evidence and keep revised history false |
| M2.8 price-provider feasibility | complete/blocked | public evidence matrix for SET Historical Data, SETSMART, SMART Marketplace, ICE SET, LSEG Tick History, Databento, and EDI; EDI is the closest conditional price-feed lead but no candidate proves the full PIT price contract | capture forward observations while obtaining one complete provider evidence packet separately |
| M2.9 forward price observations + research modes | complete/blocked | immutable raw/snapshot reuse, append-only observation IDs, semantic series/raw lineage, contract-backed history counts/timing, UTC predecessor ordering, same-raw/normalized mismatch rejection, malformed-manifest rejection, first-capture temporal guard, exploratory descriptive runner, validated PIT gate preserved; 125 backend tests; final independent review `deleg_e5407553` and focused follow-up review `deleg_0e2282c8` passed with empty blocker arrays | collect independent BOT releases and obtain provider PIT evidence; do not promote revised history |
| Tourism deterministic signal | complete | live foreign-arrivals YoY surprise | add occupancy/airport metric |
| Internal paper ledger | complete | atomic local JSON persistence and restart test | shared store before multi-worker deployment |
| Dashboard | complete | Vite build + served source check with live-sign copy | visual browser capture after permission is available |
| General backtest accounting | complete/blocked | flat-price RED/GREEN invariants; 238 backend tests; Vite build; dividend proxy and non-PIT UI disclosures | replace final-holdings yield proxy with dated dividend cash flows; add provenance-validated PIT scorer |
| LLM analysis | deferred | intentionally no LLM dependency in M0 | add after signal lineage is stable |
| Webhook receiver | deferred | contract only, no external receiver | choose after core app is usable |
| MT5 bridge | deferred | not started | paper bridge after webhook decision |
## Guardrails
- Research and paper modes only.
- No live orders, external webhook receiver, broker credentials, or MT5 connection.
- NEVER include API keys, tokens, passwords, secrets, credentials, or connection strings in the summary — replace any that appear with [REDACTED].
- Deterministic signal is authoritative; LLM will remain downstream.
- Fixture and provisional BOT sources are explicitly labelled; neither is investment-ready without validation.
- `target_weight` is recorded in the internal paper ledger; it is not an order.
- Local paper demo mode is explicit, loopback-only, visibly warned, and never a live-order authorization path.
- Shared/network paper writes require protected token mode and a server-side session.
## Verification
- Backend: 93 unittest tests pass, including paper-auth mode, malformed-config, bind-host, PIT archive, immutable-write, and market-timezone no-lookahead regressions.
- Independent M1 review: **PASSED**; no concrete security or logic blockers.
- Reviewer suggestions: set `PAPER_COOKIE_SECURE=1` outside local HTTP; replace in-memory sessions before multi-worker deployment.
- M1 reviewer backlog: add schema-drift, duplicate/reordered-row, and malformed-vintage regression fixtures.
- Frontend: `npm run build` passes with Vite.
- Backend health endpoint returns HTTP 200 JSON.
- Dashboard served HTML contains the current title, Vue mount point and Vite entry.
- Paper ledger POST and readback work through the live API.
- BOT source live fetch parsed 138 monthly periods and persisted raw HTML plus normalized snapshot.
- Data-health reports `source_mode=bot`, `status=provisional`, and `replayable=true`.
- Live vintage replay returned the same theme surprise as the current dashboard summary.
- Vintage collector preserved one live `vintage_id` with `seen_count=4` and point-in-time API excluded it before `published_at`.
- Independent M2 review: **PASSED**; no concrete security or logic blockers.
- Event-study readiness gate correctly returns HTTP 409 with 1/12 independent vintages; no backtest result is fabricated.
- Independent M2.3 review: **PASSED**; no concrete security or logic blockers.
- Price snapshot normalized 9 symbols with adjusted close and provider-derived trading dates; quality is explicitly `revised_vendor_history` and `point_in_time=false`.
- Backtest gate requires both independent vintages and point-in-time prices.
- Independent M2.4 review: **PASSED**; no concrete security or logic blockers.
- Research runner persists blocked/ready reports with frozen input IDs, hashes, configuration and gate reasons.
- Revision-aware readiness counts `(source_id, published_at)` once and runner selects the latest revision only.
- Event-study defaults to next trading-session execution and handles non-trading event dates without skipping the first session.
- Paper ledger persists atomically under ignored `backend/data/paper/ledger.json` when configured.
- Live API `0.5.0` created and replayed the same blocked Tourism research report; UI served the research-run panel and human-readable gate reason.
- Independent M2.5 review: **PASSED**; no concrete security or logic blockers.
- Integrity hardening: normalized snapshots and raw payloads are bound to manifest metadata; malformed boolean flags, stale manifests, and cached-ready replay after tampering are rejected.
- Canonical hash algorithm is explicit: `sha256-json-canonical-v1` using sorted-key compact UTF-8 JSON after excluding only the normalized hash field.
- Integrity scope is local artifact/corruption detection. A hostile machine owner who can rewrite code, manifests, raw files and runtime environment is outside this local research app's threat model.
- Independent integrity-hardening review: **PASSED** under the local threat model via the schema-corrected exact verdict; prior pre-fix findings are recorded as remediated.
- Replay now uses the canonical `VintageStore.load_snapshot()` validation path; tampered normalized snapshots return HTTP 422 instead of being recomputed.
- Research reports carry a canonical content hash; manifest entries carry their own hash and bind the report file/hash/metadata. Tampered reports, manifest hashes/metadata, and malformed report shapes fail closed.
- Research input lineage now records normalized snapshot hashes and hash algorithms alongside raw payload hashes.
- Manifest JSON shape and every `list_runs()`/`latest()` entry are validated before metadata is returned or used for selection.
- Persist validates every existing manifest entry before returning or writing a new report, and refuses malformed entries without leaving an orphan report.
- Direct report loads also bind the manifest entry identity to the requested `run_id`.
- Invalid UTF-8 in persisted manifest/report files is converted to `ResearchRunError` instead of escaping as a decode exception.
- VintageStore rejects non-object snapshots, invalid UTF-8 manifests/snapshots, and raw-payload read failures with `VintageStoreError` instead of leaking attribute/decode/I/O exceptions.
- Tourism replay computation rejects non-list/non-object observation/exposure shapes as controlled semantic failures; malformed exposure replay returns HTTP 422 instead of leaking `AttributeError`.
- Latest BOT collector/readiness check fetched the same `source_id`/`published_at`; `seen_count=16` but independent releases remain `1`, and the backtest gate remains HTTP 409 with `insufficient_vintages`.
- Browser visual capture was blocked by Chrome remote-debugging permission; no permission dialog was clicked.
- Paper auth policy: `PAPER_AUTH_MODE=demo` permits local paper writes without a session token only on loopback; `PAPER_AUTH_MODE=token` remains fail-closed without a configured server token.
- Frontend surfaces the demo warning returned by `/api/v1/auth/paper`; no token is embedded in the bundle.
- Independent paper-auth review initially found malformed non-string token configuration being coerced and an ambiguous disabled-session UI state; fixed with strict token typing, explicit `enabled` status, UI gating, and regressions.
- Fresh final paper-auth review returned schema-valid `passed=true` with empty security-concern and logic-error arrays; non-blocking suggestions remain for request-token type, frontend integration, and startup precedence coverage.
- The separate future PIT-price-contract delegation ended interrupted without a complete recommendation; no implementation was accepted, and revised vendor history remains `point_in_time=false`/blocked.
- The original staged integrity-review result (`deleg_43348a3b`) reported `passed=true`/`findings=[]` but violated the required four-key verdict schema; its corrected follow-up is recorded below.
- Schema-correction review `deleg_a1c721d2` returned exactly `{"passed":true,"security_concerns":[],"logic_errors":[],"suggestions":[]}`; the staged integrity-remediation scope is now approved under the stated local threat model. Interrupted parallel review `deleg_e2d54c30` contributes no evidence.
- PIT price contract: `point_in_time=true` now requires `quality=point_in_time_archive`, `archive_contract=pit-daily-v1`, provider release/evidence metadata, per-series IANA timezone, per-bar `session_date`/`known_at`/OHLC/adjusted-close/volume, and immutable manifest/hash binding. The event-study runner rejects missing or market-locally future-known prices when PIT mode is enabled.
- PIT verification: 93 backend tests, compileall, Vite build, npm audit (0 vulnerabilities), diff checks, and credential-pattern scan passed. The live revised Yahoo snapshot remains `point_in_time=false`; `/api/v1/backtest/tourism?min_events=1` remains HTTP 409 `blocked` with `price_series_not_point_in_time`.
- Final bounded independent PIT review `deleg_c08f6dd8` returned the exact required verdict `{"passed":true,"security_concerns":[],"logic_errors":[],"suggestions":[]}`. Earlier reviewer timeouts were treated as non-approving and contributed no evidence.
- Price-provider feasibility pass recorded in `docs/engineering-log/2026-08-24-price-provider-feasibility.md`: public SET/SETSMART/SMART Marketplace pages establish historical/API availability but not release-time PIT semantics; ICE SET is a strong commercial candidate; LSEG S3 Direct has the strongest public PIT claim but still needs SET-specific coverage and vintage evidence; Databento confirms SET venue presence and PIT corporate-action records but not PIT price-vintage semantics. No provider currently passes `pit-daily-v1` from public evidence.
- No price adapter or configuration promotion was made during the feasibility pass. The next adapter must be gated on a raw sample, provider release metadata, known-at definition, correction/revision example, symbol coverage, and immutable replay evidence. Citation ledger verification for the feasibility document passed with evidence quotes for all 11 cited sources.
- M2.9 forward price observation layer records every retrieval against a stable source/period scope, preserves immutable raw/snapshot files, binds each observation ID to its prior hash and diff, tracks first/last seen times, rejects tampered observation records, and rejects out-of-order writes before creating snapshot/raw artifacts. Repeated unchanged payloads do not create a new snapshot or a new research run; runtime observation timestamps are excluded from the run fingerprint.
- Price integrity remediation requires explicit non-PIT normalized schema/series-to-raw lineage, validates PIT numeric input without leaking `OverflowError`, parses predecessor order in UTC, and enforces observation counts/timing for contract-backed entries. Pre-observation legacy manifests remain readable when they have no observation contract; new contract-backed entries are fully cross-checked.
- Research now has explicit `validated` and `exploratory` modes. Validated mode retains the PIT/known-at fail-closed gate. Exploratory mode may use revised vendor history only with `require_price_known_at=false`, returns `result_scope=non_pit_descriptive_only` and top-level `status=descriptive_only`, and carries limitations; it cannot promote `point_in_time` or produce a validated backtest claim.
- `/api/v1/prices/observations` exposes the local observation audit trail; `/api/v1/prices/health` reports observation count, last observation time, and revision status. The UI's research button explicitly requests exploratory mode and maps machine-readable status/reason codes to human-readable labels.
- Current M2.9 verification: 125 backend tests, compileall, Vite build, npm audit (0 vulnerabilities), diff checks, static credential/dangerous-pattern scan, and current-data Flask health smoke test passed. Provider release semantics remain unproven and the live revised Yahoo snapshot remains `point_in_time=false`.
- Fresh final M2.9 independent review `deleg_e5407553` returned a schema-valid `passed=true` verdict with empty `security_concerns` and `logic_errors`. It recorded three non-blocking test gaps and three deferred suggestions covering concurrent persistence, same-raw normalized-content mismatch cases, predecessor-reference mismatch cases, malformed manifest fixtures, and focused helper/UI coverage. No commit or push has been made.
- M2.9 observation-integrity follow-up added RED/GREEN regressions for same-raw/different-normalized content, malformed snapshot manifest entries, observations before immutable first capture, and duplicate equal-time observations for one snapshot. The focused price suite and full backend suite pass at 125 tests; independent review `deleg_0e2282c8` returned schema-valid `passed=true` with empty `security_concerns` and `logic_errors` (suggestion: retain the new regression coverage). Process-level locking and focused helper/UI coverage remain deferred.
- Audit + fix pass (2026-08-26, plan at `docs/audit-and-plan-2026-08-26.md`): triple-confirmed the declarative `FACTORS`/`THEMES` framework is by-passed by hand-written scoring in `dashboard._theme_surprises`, and that `/api/v1/simulation` recomputed a divergent 3-theme path. Fixed P1 (simulation reuses the canonical board via `default_scores` — live check: sim top pick PTT == top board combined 1.600), P2 (dashboard now emits `source_summary{factor_keys:11, rows:6}`; frontend shows "N ปัจจัย · M แหล่ง"), and P5-partial (removed dead `list_themes`/`Theme`/`build_theme_scores`/`_map_index` + the tests that locked them; added `backend/tests/conftest.py` so pytest needs no `PYTHONPATH`). Deferred P0 (registry-driven re-baseline) and P3/P4 (point-in-time backtest + factor-weight learning) pending explicit scope/baseline sign-off. Full backend suite: **203 tests pass**; Vite build passes. This work is own-engine review gated before commit.
- P0-B + P3 + P4 (commit `8db3d48`, reviews `deleg_fe6f45cd` + `deleg_718218f8` both `passed=true`): the declarative FACTORS/THEMES registry is now the single source of truth (`compute_theme_surprises` reads registry; hand-written per-theme blocks removed; FACTORS carries center/span normalization spec; bank NPL wired into banks). P3 rebuilt `run_backtest` as a real multi-rebalance PIT engine (`leakage_guard`, `momentum_at` true 12-1). P4 added factor-weight learning (Spearman IC -> `apply_weight_update`) + `/api/v1/learning/momentum`. Live result: momentum IC=0.012, t=0.132 over 22 periods (no reliable predictive power in this SET50 window). 226 tests pass.
- Follow-up (A+B): theme surprises now weight-normalized by total |weight| so cross-theme magnitudes are comparable (retail 0.189->0.145). Added append-only `FactorHistory` store (`data/factor_history/<key>.jsonl`) that records every FACTORS value each scheduler run, wired into `refresh_all` (non-fatal), plus `/api/v1/learning/factors` readiness endpoint. Macro/demographic factors start at n=1 and become learnable (P4) as history accumulates. 233 tests pass.
- Backtest accounting remediation (2026-08-27): deterministic flat-price tests exposed that ending holdings value was reported as `price_pnl` and the dividend proxy was excluded from final value/net return. The corrected identity is `final_value = capital + price_pnl + dividend_income`; API now emits `dividend_method=final_holdings_yield_proxy`, and both result/history UI paths label the proxy and warn when `leakage_guard=false`. RED failures reproduced `price_pnl=20,000/49,000`; GREEN verification: 239 backend tests, compileall, Vite build, npm audit 0 high-severity vulnerabilities, served-bundle disclosure check, static scan and diff check passed. Fresh final independent review `deleg_10918fed` passed with empty blocker arrays. Evidence: `docs/engineering-log/2026-08-27-backtest-accounting-remediation.md` and `docs/test-evidence/2026-08-27-backtest-accounting-remediation.md`. The dividend model remains a proxy and the default public backtest remains descriptive non-PIT.
- PIT factor store + partial score provider (2026-08-27, commit `1f630be`): added `factor_vintages.py` (append-only PIT factor store: observed/released/retrieved_at + SHA-256 hash chain; `value_at(as_of)` anti-look-ahead, fail-closed when nothing released by as_of) and `pit_scorer.py` (`PitScoreProvider.score_board(as_of)` + `make_pit_score_fn`). `backtest._resolve_scores` now sets `leakage_guard=True` only when the supplied score_fn asserts `pit_meta.pit=True` (closes "any callable ⇒ PIT"); `/api/v1/backtest` accepts `use_pit`. Empty factor store fail-closes (live probe `leakage_guard=false`). Full backend **255 passed** (was 239). Honest scope: theme dimension PIT from this store forward; no factor history before 2026-08-27 so pre-today backtests stay non-PIT; siamchart fundamental partial (EPS 5-yr series PIT-grade, ratios current → `partial_pit`). Next: dated dividend cash-flow ledger.
- 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.
- Real dated dividend-history collector (2026-08-27, commit `f9973e8`): `siamchart.py` gained `parse_dividend_history`/`fetch_dividend_history` (reads the "ประวัติการปันผล" ex-date + DPS table per stock-info page); `dividend_ledger.py` gained `populate_dated_dividends` (registers dated rows, estimate=False). `POST /api/v1/dividends/update` fetches all snapshot symbols and persists a dated ledger at `data/dividends/ledger.json`; `use_ledger` backtests prefer the dated ledger (`dividend_method=dated_ledger`) and fall back to DPS estimates when unpopulated. Full backend **292 passed** (was 286). Live network fetch: 49/49 symbols, 1410 dated payments; use_ledger → `dated_ledger`. Note: this sandbox HAS outbound network (curl/https to siamchart 200) — contrary to earlier assumption.
- Auto-refresh dated dividend ledger (2026-08-27, commit `03195dc`): `AppDataScheduler` now calls `_maybe_refresh_dated_dividends()` in `refresh_all` — a cooldown-gated (default 6h) fetch of real dated dividend history into `data/dividends/ledger.json`, so the ledger stays fresh without manual `/api/v1/dividends/update` and without hammering the source every tick. `DIVIDEND_REFRESH_COOLDOWN_SECONDS` env toggle. Full backend **294 passed** (was 292).
- Single-container Docker packaging (2026-08-27, commit `f9b8cd1`): added `Dockerfile` (python:3.11-slim + nginx; serves the PREBUILT `frontend/dist` SPA, proxies `/api` → 127.0.0.1:5000, `VOLUME /app/backend/data`, HEALTHCHECK on /api/v1/dashboard/summary, exposes :80), `deploy/nginx.conf`, `deploy/entrypoint.sh` (Flask HOST=0.0.0.0:5000 + nginx foreground), `docker-compose.yml` (port 8080:80, volume `./backend/data`), `.dockerignore`, and `.gitignore` now tracks `frontend/dist/` (prebuilt bundle required by the image; `backend/data/` stays untracked). Backend runtime verified (test_client `/api/v1/dashboard/summary` = 200). No local docker on this machine so the image itself must build on EasyPanel.
- Event-driven PIT backtest (2026-08-28, commits `68f2cc1``d73a58b`, 8-phase plan at `.hermes/plans/2026-08-28_091000-event-driven-pit-backtest.md`): replaces the calendar-rebalance backtest with a strict point-in-time event-driven engine. Confirmed user decisions: (1) strict PIT — block when factor/Siamchart/price coverage is incomplete; (2) execute at next trading-day close after release (no same-day look-ahead); (3) dividend cash available ex_date+30 calendar days (`ex_date_plus_30d` assumption, not an observed pay date — Siamchart source gives ex-date+DPS only); (4) average-cost realized P&L; (5) all-in 0.3% fee per trade, no added tax. Ships: `backtest_readiness.py` (readiness + recommended start/end defaults, fail-closed), `backtest_events.py` (event calendar + next-trading-day mapping), `portfolio_ledger.py` (cash/positions/avg-cost/realized+unrealized P&L/dividend receivables/fees; gross realized + fees subtracted once), `portfolio_rebalancer.py` (50/20/30 → executable 100-lot orders, sells-first, no trade on unchanged target), `backtest_engine.py` (event processor; refuses to fall back to the live board without a PIT scorer — fail-closed, was flagged by cycle-1 review `deleg_5225e9de` and fixed), `backtest_store.py` (durable atomic JSON runs at `data/backtest/runs.json`), strict route `POST /api/v1/backtest/run` (readiness-gate → 400 with missing list when not ready) + `GET /api/v1/backtest/run(+<id>)`, and frontend (readiness gate disables Run + shows missing coverage; report shows realized/unrealized P&L, dividend received/receivable, fees, final holdings with avg cost/last price/unrealized). Offline standalone suite **54 tests** (9+14+15+5+6+4→ readiness 10 after price regression); compileall + diff-check clean; static scan no secrets/eval/debug (4 "secret"-regex hits are the prose "missing token" string, not credentials). Independent review cycles: cycle-1 `deleg_5225e9de` flagged 3 logic errors (no-scorer live-board fallback look-ahead; price_coverage not checking explicit start → false PIT window; coincident-day ex-date entitlement breach) → all fixed; cycle-2 `deleg_b969075c` re-verified the fallback fix + 9/10 invariants and re-confirmed the invariant-4 breach; cycle-3 `deleg_7f56df3b` confirmed the ordering fix; cycle-4 `deleg_55f4e877` final re-review returned `passed=true`, empty `logic_errors`/`security_concerns` (suggestion only: per-symbol price coverage at an explicit start equal to the intersection start — deferred as a known edge-case limitation; the engine/accounting remain verified). Fixes: engine fails closed without a PIT scorer; readiness blocks an explicit start before price history exists; each calendar day runs dividend_entitlement < dividend_payment < rebalance (shares bought on ex-date not entitled). Live probes: `GET /api/v1/backtest/readiness` on real data → ready=false, recommended_end=2026-08-27; `POST /api/v1/backtest/run` on real data (no vintages) → 400 + missing list; happy-path `scripts/probe_event_backtest.py` (synthetic PIT-complete) → accounting_reconciled=True, identity lhs=rhs, leakage_guard=True, 100-lot holdings. Deploy-safe auto-collection (2026-08-28, commit `1fb1e1a`, +4 scheduler tests): `AppDataScheduler.refresh_all` now calls `_record_pit_factor_vintages` (writes each registry factor's PIT vintage via `FactorVintageStore.record`, dedupe on unchanged value; `released_at=retrieved_at=now` — evidence-safe, we do not assume an unknown reporting lag) and `_record_siamchart_vintages` (persists the current `set50_master.json` via `SiamchartVintageStore.persist`, idempotent). Runs inside the Flask process whether on Mac or the deployed container, so strict-PIT vintages accumulate wherever the app runs (persistent volume), not just locally. Live probe: `refresh_all()` 6/6 sources → 11 factor `.jsonl` + 1 Siamchart manifest snapshot written; strict readiness `ready=true` with explicit `end=today` (auto end=yesterday stays blocked until past data is knowable — honest). Per-source cadence + source health (2026-08-28, commit `14b2aef`, +4 scheduler tests): each `_REFRESH_JOBS` entry now carries a `frequency` (monthly for BOT tourism/macro/auto-sales, quarterly for NPL/energy/bank) and a cooldown marker (`data/scheduler/job_<key>.json`) so slow sources are re-fetched at their natural cadence (30d/91d) not every tick; a separate `_DAILY_JOBS` list refreshes Siamchart vintages + the Yahoo price snapshot (`prices.collect_price_snapshot`, rolling ~3y → yesterday) daily. Every refresh appends a per-source health log (`data/scheduler/source_health.json`, ring buffer 500) with an auto-classified failure category (network/timeout/http/parse/structure/auth/other) via `_analyze_error`, exposed through `GET /api/v1/scheduler/sources` and a new frontend "สถานะข้อมูล" panel (`#health`) with a one-click copy of the failure cause. Verification: first `refresh_all` produced 8 results + 8 job markers + 8 health entries (all ok) + 11 factor vintages; a second immediate tick returned `[]` (all jobs in cooldown, proving cadence).