feat(moex): история бумаг с листинга и цены индексов для бенчмарков

Окно бэкфилла начинается с history_from доски, а не с первой покупки: график цены показывает историю бумаги целиком. Для активных бенчмарков source=moex синк создаёт instrument и качает индекс с его доски (IMOEX и RGBITR на SNDX, MCFTR на RTSI). Миграция засевает IMOEX, MCFTR и RGBITR; тесты чистят таблицы перед каждым тестом, чтобы сид не попадал в первый из них.
This commit is contained in:
Dmitry
2026-09-20 10:46:24 +03:00
parent 05affeea29
commit d2df86ce33
5 changed files with 317 additions and 23 deletions
@@ -0,0 +1,49 @@
"""benchmark: the MOEX indices the portfolio is compared against
The list is data, but there is no screen to add to it, and an empty `benchmark` table leaves
`/analytics/benchmarks` and the overlay on the price chart with nothing to draw. IMOEX and
MCFTR are the pair the comparison is built around (a price index next to its dividend-
reinvested twin, so the gap between them is visible); RGBITR is there for a bond-heavy
portfolio and is not shown unasked. A row that is already there is left as the user set it.
Revision ID: f3a91c7d5e28
Revises: e8b21f6a90c3
Create Date: 2026-09-20 18:00:00.000000
"""
from __future__ import annotations
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "f3a91c7d5e28"
down_revision: str | None = "e8b21f6a90c3"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
BENCHMARKS = [
("IMOEX", "Индекс МосБиржи", "price", True),
("MCFTR", "Индекс МосБиржи полной доходности «брутто»", "total_return", True),
(
"RGBITR",
"Индекс МосБиржи государственных облигаций (полной доходности)",
"total_return",
False,
),
]
def upgrade() -> None:
insert = sa.text(
"INSERT INTO benchmark (code, name, kind, source, currency, is_default, is_active) "
"VALUES (:code, :name, CAST(:kind AS benchmark_kind), 'moex', 'RUB', :is_default, true) "
"ON CONFLICT (code) DO NOTHING"
)
for code, name, kind, is_default in BENCHMARKS:
op.execute(insert.bindparams(code=code, name=name, kind=kind, is_default=is_default))
def downgrade() -> None:
op.execute(sa.text("DELETE FROM benchmark WHERE code IN ('IMOEX', 'MCFTR', 'RGBITR')"))