Ctrl K
ComunidadGuías…

tests/test_holdings_properties.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""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()