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
+34
View File
@@ -48,6 +48,40 @@ class MetricCashFlowMonthly(Base):
computed_at: Mapped[datetime] = mapped_column(server_default=func.now())
class MetricCashFlowBroker(Base):
"""Money put into and taken out of the brokerage accounts, per scope and month.
The sibling of `metric_cash_flow_monthly`, for the other half of the picture: that one
answers what the household earned and spent, this one what it moved across the portfolio
boundary. The scope key is the same string as everywhere else in the investment metrics
(`all`, `account:<id>`, `portfolio:<id>`).
Deposits and withdrawals are kept apart, both as positive magnitudes, because the screen
shows both bars: a month that took 200 000 ₽ in and 200 000 ₽ out is not the same month
as one that saw no money at all, and a single signed sum cannot tell them apart.
`net_rub = deposits_rub - withdrawals_rub` and equals the month's sum of
`metric_portfolio_value_daily.external_flow_rub` for the same scope.
A month with no flow gets no row at all — the series is sparse on purpose, so the client
can tell "nothing happened" from "zero on balance".
"""
__tablename__ = "metric_cash_flow_broker"
scope: Mapped[str] = mapped_column(String(32), primary_key=True)
month: Mapped[date] = mapped_column(primary_key=True)
"""First day of the month."""
deposits_rub: Mapped[Decimal]
"""Everything that came in, positive: cash deposits and securities transferred in."""
withdrawals_rub: Mapped[Decimal]
"""Everything that went out, also positive."""
net_rub: Mapped[Decimal]
"""deposits - withdrawals; negative in a month that took more out than it put in."""
event_count: Mapped[int] = mapped_column(Integer, default=0)
"""Flow events behind the row — the ones that converted; see `metric_data_quality`."""
computed_at: Mapped[datetime] = mapped_column(server_default=func.now())
class MetricSpendingByCategory(Base):
__tablename__ = "metric_spending_by_category"
__table_args__ = (UniqueConstraint("month", "category_id", postgresql_nulls_not_distinct=True),)