feat(db): flow_link, metric_cash_flow_broker и price_coverage

Три таблицы одной миграцией, а не тремя: автогенерация из трёх параллельных веток
дала бы три ревизии с общим down_revision, то есть ручную разборку ветвления вместо
экономии.

price_coverage засевается прямо в миграции из price_daily: она отвечает на вопрос «с
какой даты мы УЖЕ спрашивали ISS», и без засева первый же прогон moex перекачал бы
историю всех 77 бумаг целиком.

flow_link_kind снимается на откате явно. DROP TABLE оставляет тип в базе, и следующий
upgrade упал бы на CREATE TYPE — то же, что уже сделано для остальных енумов домена.
This commit is contained in:
Dmitry
2026-09-18 15:05:36 +03:00
parent 400d9922bb
commit e974ea9ffa
5 changed files with 214 additions and 0 deletions
+40
View File
@@ -180,3 +180,43 @@ class LotDisposal(Base):
holding_days: Mapped[int] = mapped_column(Integer)
ldv_eligible: Mapped[bool]
"""Held 3+ years on an exchange-traded instrument (art. 219.1 NK)."""
class FlowLinkKind(enum.StrEnum):
auto = "auto"
"""Produced by `ledger/matching.py`; rebuilt from scratch on every refresh."""
manual = "manual"
"""Confirmed by the user through the API; never touched by the matcher."""
class FlowLink(TimestampMixin, Base):
"""One ZenMoney transfer tied to the broker deposit/withdrawal it actually was (plan §1.6 C).
Without this pairing the same money is counted twice — once as the balance of the ZenMoney
account that mirrors the broker, once as the broker's own cash — and a top-up looks like an
expense in the cash flow. The unique constraints on both sides are what make the link a
1:1 statement: one transfer, one broker event, never a fan-out.
The scoring fields are kept because a link is a *guess*: `amount_delta` and `day_gap` are
what the reviewer needs in `GET /links/unmatched` to tell a good pairing from a lucky one.
"""
__tablename__ = "flow_link"
id: Mapped[int] = mapped_column(primary_key=True)
cash_txn_id: Mapped[int] = mapped_column(
ForeignKey("cash_txn.id", ondelete="CASCADE"), unique=True
)
event_id: Mapped[int] = mapped_column(ForeignKey("event.id", ondelete="CASCADE"), unique=True)
kind: Mapped[FlowLinkKind] = mapped_column(
db_enum(FlowLinkKind, "flow_link_kind"), default=FlowLinkKind.auto
)
confidence: Mapped[Decimal]
"""0..1; 1 means same day and the same kopeck."""
amount_delta: Mapped[Decimal]
"""|ZenMoney amount - broker amount|, in `currency` — both sides share it by construction."""
currency: Mapped[str] = mapped_column(String(3))
day_gap: Mapped[int] = mapped_column(Integer)
"""Business days between the two dates: money does not reach a broker over a weekend."""
note: Mapped[str | None] = mapped_column(Text)
"""Why the pair was accepted (which route identified the broker account), for debugging."""