142 lines
5.3 KiB
Python
142 lines
5.3 KiB
Python
from datetime import date
|
||
from decimal import Decimal
|
||
|
||
import pytest
|
||
|
||
from synthgen import dictionary
|
||
from synthgen.config import Config
|
||
from synthgen.counterparties import plan as make_plan
|
||
from synthgen.economy import build_ledgers, last_day, months
|
||
from synthgen.group import build_group
|
||
from synthgen.periods import book_start, build_periods
|
||
|
||
D = Decimal
|
||
|
||
|
||
def _build(**kw):
|
||
cfg = Config(group_size=kw.pop("group_size", 3), periods=kw.pop("periods", 4), osv_counterparties_mean=kw.pop("mean", 20), **kw)
|
||
g = build_group(cfg, dictionary.load())
|
||
ps = build_periods(cfg.start, cfg.periods)
|
||
pl = make_plan(cfg, g)
|
||
return cfg, g, ps, pl, build_ledgers(cfg, g, ps, pl)
|
||
|
||
|
||
@pytest.fixture(scope="module")
|
||
def built():
|
||
return _build()
|
||
|
||
|
||
def test_books_balanced(built):
|
||
cfg, g, ps, pl, books = built
|
||
start = book_start(ps)
|
||
for lg in books.values():
|
||
for day in (start, date(2025, 3, 31), date(2025, 12, 31)):
|
||
assert sum(lg.balance(day).values()) == 0
|
||
assert lg.balance(day, "000") == {}
|
||
assert not [p for p in lg.postings if "000" in (p.dt, p.ct) and p.day > start]
|
||
|
||
|
||
def test_bs_identity(built):
|
||
cfg, g, ps, pl, books = built
|
||
for lg in books.values():
|
||
for p in ps:
|
||
day = p.end
|
||
for acct in ("90", "91"):
|
||
assert lg.balance(day, acct) == {}
|
||
if p.quarter == 4:
|
||
assert lg.balance(day, "99") == {}
|
||
bal = lg.balance(day)
|
||
assert sum(v for (a, _), v in bal.items() if a != "000") == 0
|
||
# активы не уходят в минус: денежные средства на конец каждого месяца
|
||
for y, m in months(ps):
|
||
assert lg.balance(last_day(y, m), "51").get(("51", None), D(0)) >= 0
|
||
|
||
|
||
def test_externals_used(built):
|
||
cfg, g, ps, pl, books = built
|
||
for (code, acct), lst in pl.by_file.items():
|
||
if not lst or acct not in ("58", "60", "62", "66", "67", "76"):
|
||
continue
|
||
seen = set()
|
||
for p in books[code].postings:
|
||
for an in (p.dt_an, p.ct_an):
|
||
if an is not None and an.cp:
|
||
seen.add(an.cp)
|
||
used = sum(1 for o in lst if o.inn in seen)
|
||
assert used >= 0.8 * len(lst), (code, acct, used, len(lst))
|
||
|
||
|
||
def test_pareto_shape(built):
|
||
cfg, g, ps, pl, books = built
|
||
tot = {code: sum(c for (a, _), (_, c) in lg.turnover(date(2025, 1, 1), date(2025, 12, 31), "62").items()) for code, lg in books.items()}
|
||
biggest = max(tot, key=tot.get)
|
||
by_cp = {}
|
||
for (a, an), (dt, ct) in books[biggest].turnover(date(2025, 1, 1), date(2025, 12, 31), "62").items():
|
||
if an is not None:
|
||
by_cp[an.cp] = by_cp.get(an.cp, D(0)) + dt + ct
|
||
vals = sorted(by_cp.values(), reverse=True)
|
||
top = max(1, len(vals) // 10)
|
||
assert sum(vals[:top]) >= D("0.5") * sum(vals)
|
||
|
||
|
||
def test_signs_variety(built):
|
||
cfg, g, ps, pl, books = built
|
||
for acct in ("60", "62"):
|
||
signs = set()
|
||
for lg in books.values():
|
||
for v in lg.balance(date(2025, 12, 31), acct).values():
|
||
signs.add(v > 0)
|
||
assert signs == {True, False}, acct
|
||
|
||
|
||
def test_year_rollover():
|
||
cfg, g, ps, pl, books = _build(start="2025-Q3")
|
||
dec = date(2025, 12, 31)
|
||
for lg in books.values():
|
||
assert lg.balance(dec, "90") == {} and lg.balance(dec, "91") == {} and lg.balance(dec, "99") == {}
|
||
assert lg.balance(dec, "84") != lg.balance(date(2024, 12, 31), "84")
|
||
t = lg.turnover(date(2026, 1, 1), date(2026, 1, 1), "90")
|
||
assert t == {} # 2026 начинается с нуля; обороты 90 в январе — не 1 января
|
||
jan = lg.turnover(date(2026, 1, 1), date(2026, 1, 31), "90")
|
||
assert any(dt or ct for dt, ct in jan.values())
|
||
assert max(p.day for lg in books.values() for p in lg.postings) == date(2026, 6, 30)
|
||
|
||
|
||
def test_extra_hook_called_before_close():
|
||
cfg = Config(group_size=2, periods=1, osv_counterparties_mean=5)
|
||
g = build_group(cfg, dictionary.load())
|
||
ps = build_periods(cfg.start, 1)
|
||
seen = []
|
||
def hook(ledgers, year, month):
|
||
seen.append((year, month, len(ledgers["C01"].postings)))
|
||
build_ledgers(cfg, g, ps, make_plan(cfg, g), extra=hook)
|
||
assert [(y, m) for y, m, _ in seen] == [(2025, 1), (2025, 2), (2025, 3)]
|
||
|
||
|
||
def test_deterministic_and_zero_mean():
|
||
a = _build(mean=0)[4]
|
||
b = _build(mean=0)[4]
|
||
assert [p for p in a["C01"].postings] == [p for p in b["C01"].postings]
|
||
assert a["C01"].postings
|
||
|
||
|
||
def test_90_closes_via_909():
|
||
cfg, g, ps, pl, books = _build()
|
||
for lg in books.values():
|
||
day = date(2025, 3, 31)
|
||
assert lg.balance(day, "90") == {}
|
||
t = lg.turnover(date(2025, 1, 1), day, "90.09")
|
||
assert t and all(dt and ct for dt, ct in t.values())
|
||
for p in lg.postings:
|
||
if p.ct == "90.09" or p.dt == "90.09":
|
||
assert p.memo.startswith("закрытие на ")
|
||
assert not [p for p in lg.postings if p.memo == "закрытие на 99" and p.dt == "99" and p.ct.startswith("90.0") and p.ct != "90.09"]
|
||
|
||
|
||
def test_build_ledgers_deterministic_with_counterparties():
|
||
a = _build(mean=30, seed=3)[4]
|
||
b = _build(mean=30, seed=3)[4]
|
||
assert a.keys() == b.keys()
|
||
for code in a:
|
||
assert a[code].postings == b[code].postings
|