DRRK/app/services/market_data.py

135 lines
4.9 KiB
Python

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
]