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
@@ -0,0 +1,113 @@
"""связь потоков, брокерский cash flow и покрытие истории цен
Revision ID: 3e2977b9e577
Revises: a99f438e0010
Create Date: 2026-09-18 14:53:12.692145
"""
from __future__ import annotations
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "3e2977b9e577"
down_revision: str | None = "a99f438e0010"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"metric_cash_flow_broker",
sa.Column("scope", sa.String(length=32), nullable=False),
sa.Column("month", sa.Date(), nullable=False),
sa.Column("deposits_rub", sa.Numeric(precision=24, scale=10), nullable=False),
sa.Column("withdrawals_rub", sa.Numeric(precision=24, scale=10), nullable=False),
sa.Column("net_rub", sa.Numeric(precision=24, scale=10), nullable=False),
sa.Column("event_count", sa.Integer(), nullable=False),
sa.Column(
"computed_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.PrimaryKeyConstraint("scope", "month", name=op.f("pk_metric_cash_flow_broker")),
)
op.create_table(
"price_coverage",
sa.Column("instrument_id", sa.Integer(), nullable=False),
sa.Column("source", sa.String(length=16), nullable=False),
sa.Column("history_from", sa.Date(), nullable=False),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.ForeignKeyConstraint(
["instrument_id"],
["instrument.id"],
name=op.f("fk_price_coverage_instrument_id_instrument"),
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("instrument_id", "source", name=op.f("pk_price_coverage")),
)
# Seed from what is already stored: an instrument whose history already starts at the day it
# was first held needs no backfill, and seeding spares the next run a full re-read of every
# paper we have ever priced.
op.execute(
"""
INSERT INTO price_coverage (instrument_id, source, history_from)
SELECT instrument_id, source, MIN(d) FROM price_daily GROUP BY instrument_id, source
"""
)
op.create_table(
"flow_link",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("cash_txn_id", sa.Integer(), nullable=False),
sa.Column("event_id", sa.Integer(), nullable=False),
sa.Column("kind", sa.Enum("auto", "manual", name="flow_link_kind"), nullable=False),
sa.Column("confidence", sa.Numeric(precision=24, scale=10), nullable=False),
sa.Column("amount_delta", sa.Numeric(precision=24, scale=10), nullable=False),
sa.Column("currency", sa.String(length=3), nullable=False),
sa.Column("day_gap", sa.Integer(), nullable=False),
sa.Column("note", sa.Text(), nullable=True),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.ForeignKeyConstraint(
["cash_txn_id"],
["cash_txn.id"],
name=op.f("fk_flow_link_cash_txn_id_cash_txn"),
ondelete="CASCADE",
),
sa.ForeignKeyConstraint(
["event_id"], ["event.id"], name=op.f("fk_flow_link_event_id_event"), ondelete="CASCADE"
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_flow_link")),
sa.UniqueConstraint("cash_txn_id", name=op.f("uq_flow_link_cash_txn_id")),
sa.UniqueConstraint("event_id", name=op.f("uq_flow_link_event_id")),
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("flow_link")
# the table's DROP leaves the type behind, and a later upgrade would fail on CREATE TYPE
sa.Enum(name="flow_link_kind").drop(op.get_bind())
op.drop_table("price_coverage")
op.drop_table("metric_cash_flow_broker")
# ### end Alembic commands ###