fix(income): погашения облигаций попадают в календарь выплат

У облигаций из ленты операций не было ни даты погашения, ни номинала, а погашение строилось именно от них. moex_payouts дописывает оба поля из паспорта бумаги (только пустые). Аналитика берёт дату погашения из графика номинала, если поля нет, считает сумму по номиналу накануне, а последний шаг амортизации не учитывает дважды.
This commit is contained in:
Dmitry
2026-09-20 10:46:27 +03:00
parent d2df86ce33
commit 6a15371960
5 changed files with 177 additions and 7 deletions
+35
View File
@@ -117,6 +117,41 @@ def test_bond_entries_cover_coupon_amortisation_and_redemption():
assert all(e.basis is IncomeBasis.schedule for e in entries)
def test_a_bullet_bond_is_redeemed_on_the_day_its_schedule_reaches_zero():
"""The OFZ 26226 case: no `maturity_date` on the instrument, one schedule row — zero."""
facts = BondFacts(
nominal=D(1000),
nominal_schedule=((date(2026, 10, 7), D(0)),),
maturity_date=None,
currency="RUB",
)
entries = bond_entries(1, facts, [], D(5), start=date(2026, 9, 20), end=date(2027, 9, 20))
assert [(e.kind, e.expected_date, e.per_unit, e.amount) for e in entries] == [
("repayment", date(2026, 10, 7), D(1000), D(5000))
]
def test_the_last_amortisation_is_the_redemption_and_is_not_counted_twice():
facts = BondFacts(
nominal=D(1000),
nominal_schedule=(
(date(2026, 10, 1), D(500)),
(date(2027, 1, 1), D(200)),
(date(2027, 4, 1), D(0)),
),
maturity_date=None,
currency="RUB",
)
entries = bond_entries(1, facts, [], D(10), start=date(2026, 9, 20), end=date(2027, 9, 20))
by_kind = [(e.kind, e.expected_date, e.amount) for e in entries]
# the first row of a plan has no earlier nominal to step down from, so it is not a step;
# the step to 200 is one, and the final 200 is repaid once — as the redemption
assert by_kind == [
("amortization", date(2027, 1, 1), D(3000)),
("repayment", date(2027, 4, 1), D(2000)),
]
def test_an_announced_payout_displaces_the_projection_of_the_same_payment():
announced = [
Entry(