from __future__ import annotations from datetime import date from decimal import Decimal from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from app.db.models import MarketData, MarketDataManual, Security from app.integrations.moex import MarketQuote from app.repositories._upsert import insert_for class MarketDataRepository: def __init__(self, session: AsyncSession) -> None: self.session = session async def upsert(self, security_id: int, quote: MarketQuote, user: str) -> None: """Повторная загрузка за ту же дату перезаписывает котировку (created_* сохраняются, updated_*/loaded_at обновляются).""" values = { "board": quote.board, "clean_price": quote.clean_price, "clean_price_source": quote.clean_price_source, "yield_value": quote.yield_value, "yield_source": quote.yield_source, "duration_days": quote.duration_days, "accrued_interest": quote.accrued_interest, "face_value": quote.face_value, "coupon_rate": quote.coupon_percent, "num_trades": quote.num_trades, "volume": quote.volume, "raw": quote.raw, } stmt = insert_for(self.session, MarketData.__table__).values( security_id=security_id, market_date=quote.trade_date, created_by=user, updated_by=user, **values, ) stmt = stmt.on_conflict_do_update( index_elements=["security_id", "market_date"], set_={ **{name: stmt.excluded[name] for name in values}, "loaded_at": func.now(), "updated_at": func.now(), "updated_by": user, }, ) await self.session.execute(stmt) async def list_for_date( self, market_date: date ) -> list[tuple[MarketData, Security]]: result = await self.session.execute( select(MarketData, Security) .join(Security, Security.id == MarketData.security_id) .where(MarketData.market_date == market_date) .order_by(Security.isin) ) return [(row, security) for row, security in result.all()] async def list_manual_for_date( self, market_date: date ) -> list[tuple[MarketDataManual, Security]]: result = await self.session.execute( select(MarketDataManual, Security) .join(Security, Security.id == MarketDataManual.security_id) .where(MarketDataManual.market_date == market_date) .order_by(Security.isin) ) return [(row, security) for row, security in result.all()] async def upsert_manual( self, *, security_id: int, market_date: date, clean_price: Decimal | None, yield_value: Decimal | None, duration_days: Decimal | None, reason: str, user: str, ) -> None: """Правка на ту же дату заменяет все три значения: пустое поле — снова действует значение MOEX.""" values = { "clean_price": clean_price, "yield_value": yield_value, "duration_days": duration_days, "reason": reason, } stmt = insert_for(self.session, MarketDataManual.__table__).values( security_id=security_id, market_date=market_date, created_by=user, updated_by=user, **values, ) stmt = stmt.on_conflict_do_update( index_elements=["security_id", "market_date"], set_={ **{name: stmt.excluded[name] for name in values}, "updated_at": func.now(), "updated_by": user, }, ) await self.session.execute(stmt)