Compare commits
2 Commits
068dff22d7
...
377771be65
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
377771be65 | ||
|
|
5b8b6d12b7 |
@@ -193,6 +193,31 @@ Implement dated dividend events and a provenance-validated PIT score provider be
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Session 2026-08-27 — Dated dividend cash-flow ledger (verified + pushed)
|
||||||
|
|
||||||
|
**Branch:** main · **HEAD:** 5b8b6d1 (pushed `068dff2` + `5b8b6d1`; backend `068dff2`, frontend `5b8b6d1`)
|
||||||
|
|
||||||
|
### Completed this session (verified + pushed)
|
||||||
|
- **Dated dividend ledger** `backend/app/dividend_ledger.py`: per-symbol dated store with `ex_date` / `record_date` / `pay_date` / `per_share` / `source` / `estimate` flag, validation and persistence. `credit_dividends` credits `per_share × qty` once a payment is due (on/after ex-date and pay date); no ledger entry → no credit (fail closed). `build_dps_ledger` builds per-share estimate rows from the siamchart `ratios.DPS` (price-independent, a step up from the yield % × market-value proxy).
|
||||||
|
- **Backtest integration** `backend/app/backtest.py`: `run_backtest` accepts `dividend_ledger`; `dividend_income` comes from the ledger when set and `dividend_method` reports `dated_ledger` (real rows) | `dps_annual_proxy` (estimate) | `final_holdings_yield_proxy` (legacy, kept when no ledger). `BacktestResult` gained a `dividend_method` field surfaced in `to_dict`.
|
||||||
|
- **API** `backend/app/__init__.py`: `POST /api/v1/backtest` accepts `use_ledger`, wiring the DPS-built ledger (works independently and alongside `use_pit`).
|
||||||
|
- **Frontend** `frontend/src/App.vue`: maps `dividend_method` to an honest Thai label (ปันผล (ตามวันจริง) / ประมาณการปันผล/หุ้น (DPS) / ประมาณการปันผล (Proxy)) and shows a **Proxy** badge whenever the dividend is an estimate, on both the result card and saved-run history rows.
|
||||||
|
|
||||||
|
### Verified
|
||||||
|
- Backend full suite **266 passed** (was 255); new: ledger store/credit (9) + backtest ledger integration (2).
|
||||||
|
- compileall, `git diff --check`, static secret/dangerous scan clean.
|
||||||
|
- Live route probe: `use_ledger:true` → `dividend_method=dps_annual_proxy`, `dividend_income=4151.0` (per-share) vs default proxy `5041.96` (yield×value); price P&L unchanged (5802.89).
|
||||||
|
- Frontend build passes (bundle `index-DDD-F_cZ.js`).
|
||||||
|
|
||||||
|
### Honest scope / known limitations
|
||||||
|
- `build_dps_ledger` rows are **estimates** (no ex-date history in the snapshot) — `dividend_method=dps_annual_proxy`, flagged Proxy in UI. A symbol only reports `dated_ledger` once real ex-date/per-share history is collected for it.
|
||||||
|
- Ledger credit is end-of-run on final holdings for this milestone (not mid-window on holdings-at-ex-date); a future pass can accrue per rebalance window as ex-date history arrives.
|
||||||
|
|
||||||
|
### Exact next action
|
||||||
|
Add a real per-stock **dividend-history collector** (siamchart `/stock-info/<SYMBOL>/`) that appends dated payments into the ledger so symbols can upgrade from `dps_annual_proxy` to `dated_ledger`. In parallel, add the **siamchart vintage chain** to un-partial the fundamental dimension. Do not reset or stage unrelated pre-existing working-tree changes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Session 2026-08-27 — PIT factor store + partial PIT score provider (verified + pushed)
|
## Session 2026-08-27 — PIT factor store + partial PIT score provider (verified + pushed)
|
||||||
|
|
||||||
**Branch:** main · **HEAD:** 1f630be (pushed `1b971ac..1f630be`)
|
**Branch:** main · **HEAD:** 1f630be (pushed `1b971ac..1f630be`)
|
||||||
|
|||||||
@@ -99,3 +99,4 @@
|
|||||||
- 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.
|
- 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.
|
- 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.
|
- 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`.
|
||||||
|
|||||||
@@ -149,6 +149,17 @@ function formatNumber(value, digits = 2) {
|
|||||||
return Number(value ?? 0).toFixed(digits)
|
return Number(value ?? 0).toFixed(digits)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// map the backend's dividend_method to an honest Thai label/flag
|
||||||
|
function dividendMethodLabel(method) {
|
||||||
|
if (method === 'dated_ledger') return 'ปันผล (ตามวันจริง)'
|
||||||
|
if (method === 'dps_annual_proxy') return 'ประมาณการปันผล/หุ้น (DPS)'
|
||||||
|
return 'ประมาณการปันผล (Proxy)' // final_holdings_yield_proxy or unknown
|
||||||
|
}
|
||||||
|
function dividendIsEstimate(method) {
|
||||||
|
// only dated_ledger rows are real dated cash flows; everything else is an estimate/proxy
|
||||||
|
return method !== 'dated_ledger'
|
||||||
|
}
|
||||||
|
|
||||||
function formatDate(value) {
|
function formatDate(value) {
|
||||||
if (!value) return '—'
|
if (!value) return '—'
|
||||||
return new Date(value).toLocaleString('en-GB', {
|
return new Date(value).toLocaleString('en-GB', {
|
||||||
@@ -695,12 +706,12 @@ onMounted(async () => { await loadDashboard(); await loadBacktestRuns() })
|
|||||||
<div v-else-if="btResult" class="backtest-results">
|
<div v-else-if="btResult" class="backtest-results">
|
||||||
<div class="bt-kpi-grid">
|
<div class="bt-kpi-grid">
|
||||||
<div class="bt-kpi"><span>กำไรจากราคา</span><strong :class="pnlClass(btResult.price_pnl)">{{ formatNumber(btResult.price_pnl) }} บาท</strong></div>
|
<div class="bt-kpi"><span>กำไรจากราคา</span><strong :class="pnlClass(btResult.price_pnl)">{{ formatNumber(btResult.price_pnl) }} บาท</strong></div>
|
||||||
<div class="bt-kpi"><span>ประมาณการปันผล*</span><strong class="positive-text">{{ formatNumber(btResult.dividend_income) }} บาท</strong></div>
|
<div class="bt-kpi"><span>{{ dividendMethodLabel(btResult.dividend_method) }}<span v-if="dividendIsEstimate(btResult.dividend_method)" class="status-tag warning-tag" style="margin-left:6px">Proxy</span></span><strong class="positive-text">{{ formatNumber(btResult.dividend_income) }} บาท</strong></div>
|
||||||
<div class="bt-kpi"><span>มูลค่าสุดท้าย</span><strong>{{ formatNumber(btResult.final_value) }} บาท</strong></div>
|
<div class="bt-kpi"><span>มูลค่าสุดท้าย</span><strong>{{ formatNumber(btResult.final_value) }} บาท</strong></div>
|
||||||
<div class="bt-kpi"><span>ผลตอบแทนสุทธิ</span><strong :class="pnlClass(btResult.net_return)">{{ (btResult.net_return * 100).toFixed(2) }}%</strong></div>
|
<div class="bt-kpi"><span>ผลตอบแทนสุทธิ</span><strong :class="pnlClass(btResult.net_return)">{{ (btResult.net_return * 100).toFixed(2) }}%</strong></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bt-meta muted-cell">Trades: {{ btResult.trades }} · ช่วง {{ btResult.start }} → {{ btResult.end }}</div>
|
<div class="bt-meta muted-cell">Trades: {{ btResult.trades }} · ช่วง {{ btResult.start }} → {{ btResult.end }}</div>
|
||||||
<div class="bt-meta muted-cell">*ประมาณจาก dividend yield ของพอร์ตสุดท้าย ไม่ใช่กระแสเงินสดปันผลจริง</div>
|
<div class="bt-meta muted-cell">*{{ dividendIsEstimate(btResult.dividend_method) ? 'ประมาณการปันผล ไม่ใช่กระแสเงินสดปันผลตามวันจริง' : 'ปันผลตามวันจริงจาก ledger (ex-date/per-share)' }}</div>
|
||||||
<div v-if="!btResult.leakage_guard" class="bt-meta muted-cell">คำเตือน: ผลนี้ใช้คะแนนปัจจุบันย้อนหลัง จึงเป็น descriptive non-PIT และไม่ใช่หลักฐานประสิทธิภาพกลยุทธ์</div>
|
<div v-if="!btResult.leakage_guard" class="bt-meta muted-cell">คำเตือน: ผลนี้ใช้คะแนนปัจจุบันย้อนหลัง จึงเป็น descriptive non-PIT และไม่ใช่หลักฐานประสิทธิภาพกลยุทธ์</div>
|
||||||
<div v-if="Object.keys(btResult.holdings || {}).length" class="bt-holdings">
|
<div v-if="Object.keys(btResult.holdings || {}).length" class="bt-holdings">
|
||||||
<strong>พอร์ตสุดท้าย:</strong>
|
<strong>พอร์ตสุดท้าย:</strong>
|
||||||
@@ -712,13 +723,13 @@ onMounted(async () => { await loadDashboard(); await loadBacktestRuns() })
|
|||||||
<div v-if="btRuns.length" class="bt-history">
|
<div v-if="btRuns.length" class="bt-history">
|
||||||
<div class="section-kicker">ประวัติการย้อนทดสอบ</div>
|
<div class="section-kicker">ประวัติการย้อนทดสอบ</div>
|
||||||
<table class="source-table">
|
<table class="source-table">
|
||||||
<thead><tr><th>#</th><th>ช่วง</th><th>ทุน</th><th>กำไรราคา</th><th>ประมาณการปันผล (Proxy)</th><th>ผลตอบแทน</th><th>รันเมื่อ</th></tr></thead>
|
<thead><tr><th>#</th><th>ช่วง</th><th>ทุน</th><th>กำไรราคา</th><th>ปันผล</th><th>ผลตอบแทน</th><th>รันเมื่อ</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="r in btRuns.slice().reverse()" :key="r.id">
|
<tr v-for="r in btRuns.slice().reverse()" :key="r.id">
|
||||||
<td>{{ r.id }}</td><td>{{ r.start }} → {{ r.end }} <span v-if="r.leakage_guard === false" class="status-tag warning-tag" title="ใช้คะแนนปัจจุบันย้อนหลัง ไม่ใช่ point-in-time">descriptive non-PIT</span></td>
|
<td>{{ r.id }}</td><td>{{ r.start }} → {{ r.end }} <span v-if="r.leakage_guard === false" class="status-tag warning-tag" title="ใช้คะแนนปัจจุบันย้อนหลัง ไม่ใช่ point-in-time">descriptive non-PIT</span></td>
|
||||||
<td>{{ formatNumber(r.capital) }}</td>
|
<td>{{ formatNumber(r.capital) }}</td>
|
||||||
<td :class="pnlClass(r.price_pnl)">{{ formatNumber(r.price_pnl) }}</td>
|
<td :class="pnlClass(r.price_pnl)">{{ formatNumber(r.price_pnl) }}</td>
|
||||||
<td class="positive-text">{{ formatNumber(r.dividend_income) }}</td>
|
<td class="positive-text">{{ formatNumber(r.dividend_income) }}<span v-if="dividendIsEstimate(r.dividend_method)" class="status-tag warning-tag" title="ประมาณการ ไม่ใช่กระแสเงินสดจริง">Proxy</span></td>
|
||||||
<td :class="pnlClass(r.net_return)">{{ (r.net_return * 100).toFixed(2) }}%</td>
|
<td :class="pnlClass(r.net_return)">{{ (r.net_return * 100).toFixed(2) }}%</td>
|
||||||
<td class="muted-cell">{{ r.ran_at ? formatDate(r.ran_at) : '—' }}</td>
|
<td class="muted-cell">{{ r.ran_at ? formatDate(r.ran_at) : '—' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
Reference in New Issue
Block a user