arm-ka-fixtures/synthgen/tests/test_intragroup.py

235 lines
9.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import math
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 last_day, months
from synthgen.group import build_group
from synthgen.intragroup import build, pair_balance, plan_pairs
from synthgen.periods import book_start, build_periods
D = Decimal
def _make(**kw):
mean = kw.pop("mean", 10)
cfg = Config(group_size=kw.pop("group_size", 5), periods=kw.pop("periods", 6), osv_counterparties_mean=mean, **kw)
g = build_group(cfg, dictionary.load())
ps = build_periods(cfg.start, cfg.periods)
books, pairs = build(cfg, g, ps, make_plan(cfg, g))
return cfg, g, ps, books, pairs
@pytest.fixture(scope="module")
def built():
return _make(vgo_mismatch_share=D("0.3"))
def test_plan_pairs_deterministic_and_loans():
cfg = Config(group_size=6)
g = build_group(cfg, dictionary.load())
p1, p2 = plan_pairs(cfg, g), plan_pairs(cfg, g)
assert p1 == p2
loans = [p for p in p1 if p.kind == "loan"]
assert [p.b for p in loans] == [m.code for m in g.members[1:]]
assert all(p.mismatch is None for p in p1) and all(p.a == "C01" for p in loans)
def test_pairs_mirror(built):
cfg, g, ps, books, pairs = built
clean = [p for p in pairs if not p.mismatches]
assert clean
for pair in clean:
for per in ps:
ba, bb = pair_balance(books, pair, per.end)
assert ba == -bb, (pair, per.key)
assert any(pair_balance(books, p, ps[0].end)[0] != 0 for p in clean if p.kind == "trade")
assert any(pair_balance(books, p, ps[0].end)[0] != 0 for p in clean if p.kind == "loan")
def _years(ps):
return sorted({p.year for p in ps})
def test_mismatch_share():
cfg, g, ps, books, pairs = _make(group_size=10, vgo_mismatch_share=D("0.1"), mean=5)
n = len(pairs)
for y in _years(ps):
got = sum(1 for p in pairs for m in p.mismatches if m.year == y)
assert got == max(1, math.floor(D("0.1") * n)), y
cfg0, g0, ps0, books0, pairs0 = _make(group_size=10, vgo_mismatch_share=D("0"), mean=5, periods=2)
assert all(not p.mismatches and p.mismatch is None for p in pairs0)
def test_mismatch_mix(built):
cfg, g, ps, books, pairs = built
for y in _years(ps):
mms = [m for p in pairs for m in p.mismatches if m.year == y]
assert len(mms) >= 2
small = [m for m in mms if D(1) <= m.pct <= D(4)]
large = [m for m in mms if D(8) <= m.pct <= D(25)]
assert len(small) == len(mms) // 2
assert len(large) == len(mms) - len(mms) // 2
for m in mms:
assert m.above_threshold == (m.pct > 5)
def test_mismatch_lifecycle(built):
cfg, g, ps, books, pairs = built
checked_removal = 0
for pair in pairs:
for m in pair.mismatches:
for per in ps:
if per.year != m.year or per.key < m.period_key:
continue
ba, bb = pair_balance(books, pair, per.end)
diff = (ba + bb) # зеркальные стороны: сумма = разница по модулю со знаком стороны
assert abs(diff) == m.amount, (pair, per.key)
# у стороны с проводкой модуль сальдо больше
with_entry = ba if m.side == pair.a else bb
other = bb if m.side == pair.a else ba
assert abs(with_entry) - abs(other) == m.amount
# pct — от меньшей стороны на конец периода расхождения (в следующих периодах сальдо торговли иное)
if per.key == m.period_key:
assert abs(m.pct - (m.amount / abs(other) * 100)) < D("0.01")
jan = date(m.year + 1, 1, 31)
if jan <= ps[-1].end:
ba, bb = pair_balance(books, pair, jan)
assert ba == -bb
checked_removal += 1
assert checked_removal > 0
def test_dividends_no_balance(built):
cfg, g, ps, books, pairs = built
codes = {m.code for m in g.members}
for lg in books.values():
for per in ps:
assert lg.balance(per.end, "76.03") == {}
head = books["C01"]
t = head.turnover(date(2025, 4, 1), date(2025, 6, 30), "76.03")
assert t and all(dt > 0 and ct > 0 for dt, ct in t.values())
for code in codes - {"C01"}:
t = books[code].turnover(date(2025, 4, 1), date(2025, 6, 30), "76.03")
assert t and any(ct > 0 for dt, ct in t.values())
def test_shares(built):
cfg, g, ps, books, pairs = built
start = book_start(ps)
head = books["C01"]
h58 = {an.cp: v for (a, an), v in head.balance(start, "58.01").items()}
assert set(h58) == {m.code for m in g.members[1:]}
for m in g.members[1:]:
c80 = {an.cp: v for (a, an), v in books[m.code].balance(start, "80.09").items()}
assert c80[head.company] == -h58[m.code]
if m.minority is None:
assert set(c80) == {"C01"} and m.head_share == D("1.00")
else:
assert set(c80) == {"C01", m.minority.inn}
assert c80[m.minority.inn] < 0
# миноритарий не попал к головной
assert not any(an.cp in {m.minority.inn for m in g.members if m.minority} for (_, an) in head.balance(start, "58") if an)
assert any(m.minority for m in g.members)
def test_books_still_balanced(built):
cfg, g, ps, books, pairs = built
start = book_start(ps)
for lg in books.values():
for day in (start, date(2025, 3, 31), date(2025, 12, 31), ps[-1].end):
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]
for per in ps:
for acct in ("90", "91"):
assert lg.balance(per.end, acct) == {}
for y, mth in months(ps):
assert lg.balance(last_day(y, mth), "51").get(("51", None), D(0)) >= 0, (lg.company, y, mth)
def test_group_analytics_not_mixed(built):
cfg, g, ps, books, pairs = built
codes = {m.code for m in g.members}
ext = {o.inn for o in g.externals}
seen_group = seen_ext = False
for lg in books.values():
for acct, an in lg.balance(ps[-1].end):
if an is None or not an.cp:
continue
if acct.startswith(("60", "62")):
assert an.cp in codes or an.cp in ext, (acct, an)
seen_group |= an.cp in codes
seen_ext |= an.cp in ext
assert not (an.cp in codes and an.cp in ext)
assert seen_group and seen_ext
for lg in books.values():
for p in lg.postings:
for acct, an in ((p.dt, p.dt_an), (p.ct, p.ct_an)):
if an is not None and an.cp in codes:
assert acct.split(".")[0] in {"58", "60", "62", "66", "67", "76", "80", "91"}, acct
def test_loan_mismatch_on_principal(built):
cfg, g, ps, books, pairs = built
loans = [p for p in pairs if p.kind == "loan" and p.mismatches]
assert loans
for p in loans:
for m in p.mismatches:
memos = {x.memo for x in books[p.a].postings + books[p.b].postings if x.memo == "транш в пути"}
assert memos == {"транш в пути"}
ba = _sum_loan(books[p.a], m, "58.03", p.b)
assert ba != 0
for per in ps: # проценты не копятся: 76.09 на конец любого периода = 0
assert books[p.a].balance(per.end, "76.09") == {} and books[p.b].balance(per.end, "76.09") == {}
def _sum_loan(lg, m, acct, cp):
return sum(v for (a, an), v in lg.balance(date(m.year, 12, 31), acct).items() if an is not None and an.cp == cp)
def test_seller_cap(built):
cfg, g, ps, books, pairs = built
codes = {m.code for m in g.members}
for code, lg in books.items():
rev = lg.meta["model"].rev_year
for year in {p.year for p in ps}:
tot = sum(
(x.amount for x in lg.postings if x.memo == "реализация (ВГО)" and x.day.year == year),
D(0),
)
assert tot <= D("0.4") * D("1.2") * rev * D("1.1") + D("0.01"), (code, year)
assert any(x.memo == "реализация (ВГО)" for lg in books.values() for x in lg.postings)
def test_build_deterministic():
r1 = _make(vgo_mismatch_share=D("0.3"), periods=4)
r2 = _make(vgo_mismatch_share=D("0.3"), periods=4)
assert r1[4] == r2[4]
for code in r1[3]:
assert r1[3][code].postings == r2[3][code].postings
def test_mismatch_odd_count():
cfg = Config(group_size=5)
n = len(plan_pairs(cfg, build_group(cfg, dictionary.load())))
share = D("3.5") / n # k = floor(3.5) = 3: одна малая, две крупные
cfg, g, ps, books, pairs = _make(group_size=5, vgo_mismatch_share=share, periods=2)
for y in _years(ps):
mms = [m for p in pairs for m in p.mismatches if m.year == y]
assert len(mms) == 3
assert sum(1 for m in mms if m.pct < 5) == 1
assert sum(1 for m in mms if m.pct > 5) == 2
def test_edge_group_two():
cfg, g, ps, books, pairs = _make(group_size=2, periods=4, vgo_mismatch_share=D("0.5"))
assert [p.kind for p in pairs if p.kind == "loan"] == ["loan"]
for lg in books.values():
assert sum(lg.balance(ps[-1].end).values()) == 0
assert any(p.mismatches for p in pairs)