from __future__ import annotations from datetime import date, datetime from decimal import Decimal from typing import Any from sqlalchemy import ( BigInteger, Date, DateTime, ForeignKey, Index, String, UniqueConstraint, func, ) from sqlalchemy.orm import Mapped, mapped_column from app.core.config import settings from app.db.base import Base from app.db.types import ( AuditMixin, IdMixin, JsonType, Percent, Price, Quantity, qualified, ) class MarketData(IdMixin, AuditMixin, Base): """Котировка бумаги за дату (строка истории MOEX ISS по выбранному борду). Повторная загрузка за ту же дату — upsert по (security_id, market_date). """ __tablename__ = "market_data" __table_args__ = ( UniqueConstraint( "security_id", "market_date", name="uq_market_data_security_date" ), Index("ix_market_data_market_date", "market_date"), {"schema": settings.DATABASE_SCHEMA} if settings.DATABASE_SCHEMA else {}, ) security_id: Mapped[int] = mapped_column( BigInteger, ForeignKey(qualified("security.id")), nullable=False ) market_date: Mapped[date] = mapped_column(Date, nullable=False) board: Mapped[str] = mapped_column(String(12), nullable=False) clean_price: Mapped[Decimal | None] = mapped_column(Price) clean_price_source: Mapped[str | None] = mapped_column(String(20)) yield_value: Mapped[Decimal | None] = mapped_column(Percent) yield_source: Mapped[str | None] = mapped_column(String(20)) # В днях, как отдаёт MOEX; в годы переводит расчёт. duration_days: Mapped[Decimal | None] = mapped_column(Quantity) accrued_interest: Mapped[Decimal | None] = mapped_column(Price) # Текущий номинал: у амортизируемых бумаг меняется. face_value: Mapped[Decimal | None] = mapped_column(Price) coupon_rate: Mapped[Decimal | None] = mapped_column(Percent) num_trades: Mapped[int | None] = mapped_column(BigInteger) volume: Mapped[Decimal | None] = mapped_column(Quantity) # Исходная строка истории ISS — для разборов «откуда цифра». raw: Mapped[dict[str, Any] | None] = mapped_column(JsonType) loaded_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), nullable=False ) class MarketDataManual(IdMixin, AuditMixin, Base): """Ручные цена, доходность, дюрация бумаги на дату (D6). Отдельно от market_data: загрузка MOEX сюда не пишет, значение MOEX остаётся в market_data. Пустое поле — действует значение MOEX. Повторная правка на ту же дату перезаписывает строку: updated_* — кто и когда, reason — почему. """ __tablename__ = "market_data_manual" __table_args__ = ( UniqueConstraint( "security_id", "market_date", name="uq_market_data_manual_security_date" ), {"schema": settings.DATABASE_SCHEMA} if settings.DATABASE_SCHEMA else {}, ) security_id: Mapped[int] = mapped_column( BigInteger, ForeignKey(qualified("security.id")), nullable=False ) market_date: Mapped[date] = mapped_column(Date, nullable=False) clean_price: Mapped[Decimal | None] = mapped_column(Price) yield_value: Mapped[Decimal | None] = mapped_column(Percent) duration_days: Mapped[Decimal | None] = mapped_column(Quantity) reason: Mapped[str] = mapped_column(String(500), nullable=False)