tests/test_holdings_properties.py
Rama main · réplica de lectura del repositorio original.
"""Holdings, closed positions and the dashboard totals, against a working-out
of their own over generated histories.
A mutation run found most of these figures unguarded by anything but round
numbers: a holding's cost could subtract its brokerage, the AUD gain could be
`value + cost`, and a closed position's proceeds and profit could take almost
any sign, with every test green.
The rules are the documented ones: cost is what the units still held cost,
with brokerage, the oldest sold first, so the gain is unrealised (#54); the
average price is over buys and DRP allotments without it (decisions.md #50);
an AUD figure uses each row's own rate, else the stored series, and is blank
when a foreign currency has none at all (#5, #6); value uses the latest close
and the latest rate.
"""
from __future__ import annotations
from decimal import Decimal
from hypothesis import given
from app import queries, tenancy
from histories import ZERO, histories, load, worked_holdings
EXACT = Decimal("0.00000001")
def _q(v):
return None if v is None else Decimal(v).quantize(EXACT)
FIELDS = ("units", "cost", "invested", "avg_price", "price", "value", "gain", "gain_pct",
"cost_aud",
"value_aud", "gain_aud", "dividends_cash", "dividends_aud", "avg_price_aud",
"price_aud", "day_pct", "day_change_aud")
@given(history=histories())
def test_every_holding_agrees_with_the_working_out(session_factory, portfolio, history):
s = session_factory()
tenancy.bind(s, *portfolio)
try:
load(s, history)
want = worked_holdings(history)
holdings = queries.all_holdings(s)
assert sorted(h.instrument.ticker for h in holdings) == sorted(want)
for h in holdings:
w = want[h.instrument.ticker]
got = {f: _q(getattr(h, f)) for f in FIELDS}
assert got == {f: _q(w[f]) for f in FIELDS}, h.instrument.ticker
open_, closed = queries.split_positions(holdings)
active = {h.ticker for h in history.holdings if h.active}
assert {h.instrument.ticker for h in open_} == {t for t, w in want.items()
if w["units"] > 0 and t in active}
assert {c.instrument.ticker for c in closed} == set(want) - {
h.instrument.ticker for h in open_}
for c in closed:
w = want[c.instrument.ticker]
assert (c.outlay, _q(c.proceeds)) == (w["invested"], _q(w["proceeds"]))
totals = queries.totals(open_)
held = {t: w for t, w in want.items() if w["units"] > 0 and t in active}
valued = [w for w in held.values()
if w["cost_aud"] is not None and w["value_aud"] is not None]
cost = sum((w["cost_aud"] for w in valued), ZERO)
value = sum((w["value_aud"] for w in valued), ZERO)
assert (_q(totals.cost), _q(totals.value), _q(totals.gain)) == (
_q(cost), _q(value), _q(value - cost))
assert _q(totals.gain_pct) == (_q((value - cost) / cost) if cost else None)
assert sorted(totals.excluded) == sorted(
t for t, w in held.items() if w["cost_aud"] is None or w["value_aud"] is None)
assert _q(totals.dividends) == _q(sum((w["dividends_aud"] for w in held.values()
if w["dividends_aud"] is not None), ZERO))
tickers_with = {h.ticker for h in history.holdings if h.dividends}
assert sorted(totals.dividends_excluded) == sorted(
t for t, w in held.items() if w["dividends_aud"] is None and t in tickers_with)
day = sum((w["day_change_aud"] for w in held.values()
if w["day_change_aud"] is not None), ZERO)
assert _q(totals.day_change) == _q(day)
yesterday = value - day
assert _q(totals.day_pct) == (_q(day / yesterday) if yesterday else None)
finally:
s.rollback()
s.close()