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.8 KiB
Test Evidence — Backtest Accounting Remediation
Date: 2026-08-27 +07
Branch: main
Base HEAD: b362cc3
RED evidence
Command:
PYTHONPATH=. python3 -m pytest -q tests/test_backtest.py::RunBacktestTest::test_flat_price_has_zero_price_pnl tests/test_backtest.py::RunBacktestTest::test_dividend_is_included_in_final_value_and_net_return
Expected failures observed:
2 failed
flat/no-dividend: price_pnl 20000.0 != 0.0
flat/2%-proxy: price_pnl 49000.0 != 0.0
The failures were caused by the production accounting bug, not test syntax or setup.
A second RED cycle for API disclosure failed with:
KeyError: 'dividend_method'
Test hardening later added a rising-price/no-dividend case and an explicit multi-rebalance accounting identity. The first draft incorrectly expected all capital in the non-dividend name and failed 4,000 != 10,000; canonical allocation intentionally assigns non-dividend names to the 20% bucket, so the expected P&L was corrected to 4,000 without changing production code.
GREEN evidence
Focused regressions:
2 passed
Backtest module:
11 passed
Full backend suite:
239 passed
Python compilation:
python3 -m compileall -q backend/app backend/tests
passed
Frontend production build:
vite v6.4.3 building for production
10 modules transformed
built in 352ms
Fresh served artifact verification:
served_chunk=index-DlxBkioz.js
served_ui_strings=present
The fresh served bundle contains ประมาณการปันผล, descriptive non-PIT, and ไม่ใช่หลักฐานประสิทธิภาพกลยุทธ์.
Live API probe through the Vite frontend proxy:
final_value=1,146,565.40
price_pnl=94,271.57
dividend_income=52,293.83
dividend_method=final_holdings_yield_proxy
net_return=0.1466
leakage_guard=false
The returned values satisfy the accounting identity after response rounding.
Dependency audit:
npm audit --omit=dev --audit-level=high
found 0 vulnerabilities
Static added-line scan:
hardcoded secrets: none
shell injection: none
eval/exec: none
unsafe pickle: none
SQL string formatting: none
debug leftovers: none
Diff integrity:
git diff --check
passed
Deterministic accounting invariant
For flat prices and no dividend:
price_pnl = 0
final_value = initial capital
net_return = 0
For flat prices and a final-holdings yield proxy:
price_pnl = 0
final_value = capital + dividend_income
net_return = dividend_income / capital
final_value = capital + price_pnl + dividend_income
Independent review gate
Cycle 1 failed closed with no security finding and one disclosure logic error: the newly executed result card was honest, but the saved-run history table still labelled the proxy as ordinary dividends and displayed leakage_guard=false rows without a visible non-PIT marker. A fresh fix agent changed only frontend/src/App.vue: the history column is now ประมาณการปันผล (Proxy), and each leakage_guard=false row gets a descriptive non-PIT warning badge. Frontend build and git diff --check passed after the fix; the freshly served production chunk index-CzZTebYa.js contains both disclosures. Fresh final review deleg_10918fed inspected the current post-hardening diff and returned schema-valid passed=true with empty security, logic, and suggestion arrays. The earlier failed and stale verdicts are not treated as approval.
Manual/live scope
No live order, webhook, broker, external mutation, commit, or push was performed. The in-app preview loaded the current page, but the result-state interaction did not produce a capturable UI state. Visual result-state capture is therefore not claimed; verification uses the fresh served chunk plus the live API contract.