135 lines
4.9 KiB
Python
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
|
|
]
|