"""Ставки фондирования портфелей (D27). Расчёт стоимости фондирования — вместе с финрезультатом.""" from __future__ import annotations from dataclasses import dataclass from datetime import date, datetime from decimal import Decimal from sqlalchemy.ext.asyncio import AsyncSession from app.db.base import SessionLocal from app.repositories.funding_rates import FundingRateRepository from app.repositories.reference import ReferenceRepository # Знаков после запятой в funding_rate.rate (Numeric(12, 6)). _RATE_SCALE = 6 class FundingError(RuntimeError): """Ошибка с понятным пользователю текстом.""" @dataclass(frozen=True) class FundingOptions: portfolios: dict[str, str] # код -> название currencies: list[str] @dataclass(frozen=True) class FundingRateInput: portfolio_code: str currency_code: str effective_from: date rate: Decimal | None @dataclass(frozen=True) class FundingRateRow: currency: str effective_from: date rate: Decimal updated_by: str updated_at: datetime @dataclass(frozen=True) class RateOnDate: currency: str rate: Decimal | None effective_from: date | None async def load_options() -> FundingOptions: async with SessionLocal() as session: reference = ReferenceRepository(session) portfolios = await reference.list_portfolios() currencies = await reference.currency_ids() return FundingOptions( portfolios={p.code: p.name for p in portfolios}, currencies=sorted(currencies), ) async def _portfolio_id(session: AsyncSession, code: str) -> int: portfolio_id = await ReferenceRepository(session).get_portfolio_id(code) if portfolio_id is None: raise FundingError(f"Портфеля {code} нет в справочнике.") return portfolio_id async def _currency_id(session: AsyncSession, code: str) -> int: currency_id = await ReferenceRepository(session).get_currency_id(code) if currency_id is None: raise FundingError(f"Валюты {code} нет в справочнике.") return currency_id def validate_rate(value: FundingRateInput) -> list[str]: errors = [] if value.rate is None: errors.append("Укажите ставку.") elif value.rate < 0: errors.append("Ставка не может быть отрицательной.") elif -value.rate.as_tuple().exponent > _RATE_SCALE: errors.append(f"Ставка — не больше {_RATE_SCALE} знаков после запятой.") return errors async def set_rate(value: FundingRateInput, user: str) -> None: """Ставка с даты. Повторный ввод на ту же дату (портфель, валюта) перезаписывает ставку. TODO(BIQ-38115): права на ввод ставок — Q16, пока может любой пользователь. """ errors = validate_rate(value) if errors: raise FundingError(" ".join(errors)) async with SessionLocal() as session: portfolio_id = await _portfolio_id(session, value.portfolio_code) currency_id = await _currency_id(session, value.currency_code) await FundingRateRepository(session).upsert( portfolio_id=portfolio_id, currency_id=currency_id, effective_from=value.effective_from, rate=value.rate, user=user, ) await session.commit() async def rate_on( portfolio_code: str, currency_code: str, on_date: date ) -> Decimal | None: """Ставка портфеля в валюте, действующая на дату; до первой записи — None. TODO(BIQ-38115): индивидуальные ставки по бумагам (РЭО-02 = ключевая + 2,42) — Q8, пока только ставка портфеля. Какую дату брать для продажи — Q19, решается в расчёте. """ async with SessionLocal() as session: portfolio_id = await _portfolio_id(session, portfolio_code) currency_id = await _currency_id(session, currency_code) row = await FundingRateRepository(session).effective_on( portfolio_id, currency_id, on_date ) return row.rate if row is not None else None async def rates_on(portfolio_code: str, on_date: date) -> list[RateOnDate]: """Действующие на дату ставки портфеля по всем валютам справочника.""" async with SessionLocal() as session: portfolio_id = await _portfolio_id(session, portfolio_code) currencies = await ReferenceRepository(session).currency_ids() repository = FundingRateRepository(session) result = [] for code in sorted(currencies): row = await repository.effective_on(portfolio_id, currencies[code], on_date) result.append( RateOnDate( currency=code, rate=row.rate if row is not None else None, effective_from=row.effective_from if row is not None else None, ) ) return result async def rate_history(portfolio_code: str) -> list[FundingRateRow]: """История ставок портфеля, новые сверху.""" async with SessionLocal() as session: portfolio_id = await _portfolio_id(session, portfolio_code) rows = await FundingRateRepository(session).list_for_portfolio(portfolio_id) return [ FundingRateRow( currency=currency, effective_from=row.effective_from, rate=row.rate, updated_by=row.updated_by, updated_at=row.updated_at, ) for row, currency in rows ]