Files
set50-system/docs/test-evidence/2026-08-27-backtest-accounting-remediation.md
Kunthawat Greethong 1b971ac72d [verified] Fix backtest accounting identity + honest UI disclosure
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.
2026-08-27 09:00:39 +07:00

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.