Correct the multi-rebalance backtest accounting so ending wealth is capital + price_pnl + dividend_income with no double counting: - price_pnl now measures equity change excluding dividends (was reusing ending holdings value as 'price profit') - dividend proxy is included in final_value and net_return, exposed as dividend_method=final_holdings_yield_proxy - regression tests: flat price => zero price_pnl; flat + dividend => dividend-only return; rising no-dividend => correct bucket P&L; multi-rebalance accounting identity - UI (result card + saved-run history) labels dividends as ประมาณการปันผล (Proxy) and shows descriptive non-PIT badge when leakage_guard=false Backend 239 tests passed; targeted backtest 11 passed; frontend build, npm audit (0), static scan and diff check passed; fresh independent review deleg_10918fed passed with empty blocker arrays. Backtest remains descriptive non-PIT (leakage_guard=false) with the default current-score scorer.
3.9 KiB
Backtest Accounting Remediation — 2026-08-27
Status
complete — core accounting implementation, disclosure remediation, local verification, and fresh current-diff independent review all passed.
Scope
Correct the general multi-theme backtest result accounting without changing scoring, allocation, rebalance, price retrieval, or unrelated dirty working-tree files.
Verified root cause
backend/app/backtest.py previously calculated:
final_value = ending cash + ending market valueprice_pnl = final_value - ending cash - dividendnet_return = (final_value - initial capital) / initial capital
This treated ending holdings value as price profit and excluded the dividend proxy from both final value and net return.
A deterministic flat-price reproduction with THB 100,000 produced price_pnl=20,000 without dividends and price_pnl=49,000 with a 2% dividend-yield proxy, when both price P&L values must be zero.
Remediation
- Added flat-price regressions before production code changes and observed both fail for the expected accounting error.
- Defined ending equity before dividend as ending cash plus ending holdings market value.
- Defined price P&L as ending equity before dividend minus initial capital.
- Included the estimated dividend in final value and net return.
- Added API disclosure
dividend_method=final_holdings_yield_proxy. - Updated the Vue result panel to label dividends as estimates and show a non-PIT warning when
leakage_guard=false.
Changed files
backend/app/backtest.pybackend/tests/test_backtest.pyfrontend/src/App.vue
Verification evidence
- RED: two focused regressions failed with
price_pnl=20,000andprice_pnl=49,000. - GREEN: flat/no-dividend, flat/proxy-dividend, rising/no-dividend, and multi-rebalance accounting checks passed.
- Backtest module: 11 tests passed.
- Full backend suite: 239 tests passed.
- Python compileall: passed.
- Frontend Vite production build: passed.
- Fresh static server referenced
index-DlxBkioz.js; all three new disclosure strings were present in the served bundle. - Live API probe returned the corrected accounting fields including
dividend_method=final_holdings_yield_proxyandleakage_guard=false. - npm audit, high severity threshold: 0 vulnerabilities.
- Added-line static scan: no hardcoded secrets, shell injection, eval/exec, unsafe pickle, SQL formatting, or debug leftovers.
git diff --check: passed.- Independent review cycle 1 failed closed on one saved-history disclosure blocker. A fresh fix agent added per-history proxy and non-PIT disclosure. After test hardening, fresh final review
deleg_10918fedapproved the current scoped diff withpassed=trueand emptysecurity_concerns,logic_errors, andsuggestions.
Full command evidence: docs/test-evidence/2026-08-27-backtest-accounting-remediation.md.
Remaining risks
- Dividend income remains an annualized proxy based on final holdings and final dividend yield. It is not a dated dividend cash-flow ledger and is disclosed as such.
- The public default backtest still uses current scores historically and remains descriptive non-PIT when
leakage_guard=false. leakage_guard=truemeans a scorer callable was supplied; it does not independently certify release-time provenance.- Existing backtest run history remains process-local memory.
- The in-app preview loaded the current application, but result-state visual capture could not be completed; fresh served-artifact and live API checks are recorded instead.
Data safety and rollback
No database migration, external write, order dispatch, commit, or push occurred. Rollback is limited to the three changed code/test files listed above, but the repository contains unrelated pre-existing modifications that must not be reset wholesale.
Exact next action
Implement a dated dividend cash-flow ledger and provenance-validated PIT scorer rather than extending the proxy.