from __future__ import annotations import logging from dataclasses import dataclass, field from datetime import date from decimal import Decimal from app.db.base import SessionLocal from app.integrations.gateway import ( GatewayAuthError, GatewayError, GatewayNotFoundError, ) from app.integrations.moex import MoexClient, MoexDataError from app.repositories.market_data import MarketDataRepository from app.repositories.securities import SecurityRepository from app.services.request_log import save_request_log logger = logging.getLogger(__name__) @dataclass class MarketDataUpdateResult: on_date: date updated: int = 0 no_trades: int = 0 errors: int = 0 messages: list[str] = field(default_factory=list) @property def ok(self) -> bool: return self.errors == 0 @dataclass(frozen=True) class MarketDataRow: isin: str name: str board: str clean_price: Decimal | None clean_price_source: str | None yield_value: Decimal | None yield_source: str | None duration_days: Decimal | None accrued_interest: Decimal | None face_value: Decimal | None coupon_rate: Decimal | None num_trades: int | None volume: Decimal | None async def update( on_date: date, user: str, *, moex: MoexClient | None = None ) -> MarketDataUpdateResult: """Котировки на дату по всем активным бумагам. Каждая бумага — своя короткая транзакция: ошибка по одной не откатывает и не останавливает остальные. Исключение — ошибки конфигурации шлюза (ключ/путь): они одинаковы для всех бумаг, поэтому обновление прерывается. Транзакция не держится открытой во время HTTP-запросов. """ moex = moex or MoexClient.from_settings() result = MarketDataUpdateResult(on_date=on_date) async with SessionLocal() as session: securities = list(await SecurityRepository(session).list_active()) if not securities: result.messages.append("Нет активных бумаг для обновления.") return result for index, security in enumerate(securities): label = f"{security.isin} ({security.name})" try: quote = moex.get_history(security.secid, on_date) if quote is None: result.no_trades += 1 result.messages.append( f"{label}: торгов за {on_date:%d.%m.%Y} не было." ) continue async with SessionLocal() as session: await MarketDataRepository(session).upsert(security.id, quote, user) await session.commit() result.updated += 1 price = ( f"{quote.clean_price} ({quote.clean_price_source})" if quote.clean_price is not None else "нет цены" ) result.messages.append(f"{label}: {quote.board}, цена {price}.") except (GatewayAuthError, GatewayNotFoundError) as exc: remaining = len(securities) - index result.errors += remaining result.messages.append(f"{label}: {exc}") if remaining > 1: result.messages.append( f"Обновление прервано: остальные {remaining - 1} бумаг " "не обновлялись — сначала исправьте настройки шлюза." ) break except (GatewayError, MoexDataError) as exc: result.errors += 1 result.messages.append(f"{label}: {exc}") except Exception as exc: # noqa: BLE001 logger.exception("Ошибка обновления котировки %s", security.isin) result.errors += 1 result.messages.append(f"{label}: непредвиденная ошибка ({exc}).") finally: await save_request_log(moex.gateway, user) return result async def list_for_date(market_date: date) -> list[MarketDataRow]: async with SessionLocal() as session: rows = await MarketDataRepository(session).list_for_date(market_date) return [ MarketDataRow( isin=security.isin, name=security.name, board=row.board, clean_price=row.clean_price, clean_price_source=row.clean_price_source, yield_value=row.yield_value, yield_source=row.yield_source, duration_days=row.duration_days, accrued_interest=row.accrued_interest, face_value=row.face_value, coupon_rate=row.coupon_rate, num_trades=row.num_trades, volume=row.volume, ) for row, security in rows ]