- funding_rate table and "Фондирование" page: rate from date, history, rate on date (D27)
- positions by issue: BUY - SELL per portfolio, "Остатки" page (D29)
- quotes are loaded only for securities with an open position (D10)
- business calendar from xmlcalendar.ru: yearly file upload, report date
and update job use previous working day (D30)
- xlsx download for positions, quotes and funding rate history
- manual operation type as radio buttons
- QUIK instruments with purchase marks ("МТС 2Р-18_1") match the issue,
file name kept in operation.instrument (D17)
- migrations d3a6f8b1c254, e7b2c9d4a813, f1c4a7e9b352
- project.md: decisions and questions after customer meetings
72 lines
2.8 KiB
Python
72 lines
2.8 KiB
Python
"""Остатки выпусков в портфелях, шт (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)
|