[verified] Dated dividend cash-flow ledger replacing final-holdings proxy
Replace the single final-holdings yield proxy with a per-symbol dated dividend ledger for the backtest engine: - backend/app/dividend_ledger.py: DividendLedger store (ex_date, record_date, pay_date, per_share, source, estimate flag) with validation and persistence; credit_dividends credits per_share * qty once a payment is due (on/after ex-date and pay date); build_dps_ledger builds estimate rows from siamchart ratios.DPS (per-share, price-independent) as a step up from the yield-percentage proxy. - backend/app/backtest.py: run_backtest accepts dividend_ledger; when set, dividend_income comes from the ledger and dividend_method reports 'dated_ledger' (real rows) or 'dps_annual_proxy' (estimate). No ledger -> legacy final_holdings_yield_proxy preserved and labelled. - backend/app/__init__.py: /api/v1/backtest accepts use_ledger, wiring the DPS-built ledger. - tests: ledger store/credit (9) + backtest ledger integration (2 new) — full backend suite 266 passed. Live probe: use_ledger flips dividend_method to dps_annual_proxy with per-share income (4151.0) vs proxy (5041.96). 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 when present.
This commit is contained in:
@@ -189,6 +189,43 @@ class RunBacktestTest(unittest.TestCase):
|
||||
)
|
||||
self.assertIs(res.leakage_guard, True)
|
||||
|
||||
@patch("app.backtest.load_price_snapshot", return_value=_flat_series())
|
||||
def test_ledger_replaces_proxy_and_marks_dated_ledger(self, _load):
|
||||
# A ledger with a dated real payment replaces the final-holdings proxy.
|
||||
from app.dividend_ledger import DividendLedger
|
||||
import tempfile
|
||||
ledger = DividendLedger()
|
||||
# A pays 2.0/share on 2026-01-31 (within the run window).
|
||||
ledger.add("A", {
|
||||
"ex_date": "2026-01-20", "pay_date": "2026-01-31",
|
||||
"per_share": 2.0,
|
||||
})
|
||||
res = backtest.run_backtest(
|
||||
"2026-01-01", "2026-02-01", capital=100_000,
|
||||
score_fn=lambda s, a: {"A": {"combined": 1.0, "is_dividend": True,
|
||||
"dividend_yield": 0.0}},
|
||||
symbols=["A"], dividend_ledger=ledger,
|
||||
)
|
||||
self.assertEqual(res.dividend_method, "dated_ledger")
|
||||
# A is the only dividend name: bucket1 (50%) buys 50,000/10.0 =
|
||||
# 5,000 shares of A on 2026-01-01, at 2.0/share = 10,000 dividend.
|
||||
self.assertEqual(res.dividend_income, 2.0 * 5_000.0)
|
||||
|
||||
@patch("app.backtest.load_price_snapshot", return_value=_flat_series())
|
||||
def test_ledger_estimate_marks_dps_proxy(self, _load):
|
||||
from app.dividend_ledger import DividendLedger
|
||||
ledger = DividendLedger()
|
||||
ledger.add("A", {"per_share": 1.0, "source": "dps_annual_proxy"})
|
||||
res = backtest.run_backtest(
|
||||
"2026-01-01", "2026-02-01", capital=100_000,
|
||||
score_fn=lambda s, a: {"A": {"combined": 1.0, "is_dividend": True,
|
||||
"dividend_yield": 0.0}},
|
||||
symbols=["A"], dividend_ledger=ledger,
|
||||
)
|
||||
self.assertEqual(res.dividend_method, "dps_annual_proxy")
|
||||
# bucket1 (50%) buys 5,000 shares of A -> 1.0 * 5,000 = 5,000
|
||||
self.assertEqual(res.dividend_income, 1.0 * 5_000.0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user