"""Остатки выпусков в портфелях, шт (D29): Σ BUY − Σ SELL по портфелю и бумаге. TODO(BIQ-38115): операции отбираются по дате сделки, как в Excel; дата сделки или дата расчётов — Q30. Закрытие остатка при погашении — Q29, количество из корректировок (ADJ) — Q28. Учёта партий нет (Q24). """ from __future__ import annotations from dataclasses import dataclass from datetime import date from decimal import Decimal from app.db.base import SessionLocal from app.repositories.operations import OperationRepository from app.repositories.reference import ReferenceRepository _ZERO = Decimal(0) class PositionError(RuntimeError): """Ошибка с понятным пользователю текстом.""" @dataclass(frozen=True) class PositionRow: portfolio: str isin: str name: str currency: str bought: Decimal sold: Decimal balance: Decimal async def list_portfolios() -> dict[str, str]: """Код -> название.""" async with SessionLocal() as session: portfolios = await ReferenceRepository(session).list_portfolios() return {p.code: p.name for p in portfolios} async def positions_on( on_date: date, portfolio_code: str | None = None, *, include_closed: bool = False ) -> list[PositionRow]: """Остатки на конец дня on_date. Без include_closed — только ненулевые (отрицательный остаток тоже показываем: продаж больше покупок).""" async with SessionLocal() as session: portfolio_id = None if portfolio_code is not None: portfolio_id = await ReferenceRepository(session).get_portfolio_id( portfolio_code ) if portfolio_id is None: raise PositionError(f"Портфеля {portfolio_code} нет в справочнике.") rows = await OperationRepository(session).quantities_on(on_date, portfolio_id) result = [] for portfolio, isin, name, currency, bought, sold in rows: bought, sold = bought or _ZERO, sold or _ZERO balance = bought - sold if balance or include_closed: result.append( PositionRow(portfolio, isin, name, currency, bought, sold, balance) ) return result async def balance_on(portfolio_code: str, isin: str, on_date: date) -> Decimal: """Остаток бумаги в портфеле на конец дня; нет сделок — 0.""" rows = await positions_on(on_date, portfolio_code, include_closed=True) return next((row.balance for row in rows if row.isin == isin), _ZERO)