DRRK/tests/unit/test_funding.py
Raykov-MS c1c73e96f5 Add funding rates, positions, business calendar and xlsx export
- 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
2026-10-29 11:20:15 +03:00

126 lines
4.3 KiB
Python

"""Ставки фондирования (D27) на временном SQLite."""
from __future__ import annotations
from datetime import date
from decimal import Decimal
import pytest
from app.db.base import run_db
from app.services import funding
from app.services.funding import FundingError, FundingRateInput, validate_rate
USER = "analyst@rshb.ru"
def _set(
effective_from: date,
rate: str,
*,
portfolio: str = "UW",
currency: str = "RUB",
user: str = USER,
) -> None:
value = FundingRateInput(portfolio, currency, effective_from, Decimal(rate))
run_db(funding.set_rate(value, user))
def _on(on_date: date, portfolio: str = "UW", currency: str = "RUB") -> Decimal | None:
return run_db(funding.rate_on(portfolio, currency, on_date))
def test_rate_on_date_acts_until_next_change(session_local) -> None:
_set(date(2026, 1, 1), "21")
_set(date(2026, 7, 28), "18.5")
assert _on(date(2025, 12, 31)) is None # до первой ставки
assert _on(date(2026, 1, 1)) == Decimal("21")
assert _on(date(2026, 7, 27)) == Decimal("21")
assert _on(date(2026, 7, 28)) == Decimal("18.5")
assert _on(date(2026, 10, 8)) == Decimal("18.5")
def test_rates_are_separate_by_portfolio_and_currency(session_local) -> None:
_set(date(2026, 1, 1), "21")
_set(date(2026, 1, 1), "4.2", currency="CNY")
_set(date(2026, 1, 1), "20.5", portfolio="SALES")
assert _on(date(2026, 3, 1)) == Decimal("21")
assert _on(date(2026, 3, 1), currency="CNY") == Decimal("4.2")
assert _on(date(2026, 3, 1), portfolio="SALES") == Decimal("20.5")
assert _on(date(2026, 3, 1), portfolio="SALES", currency="CNY") is None
rates = run_db(funding.rates_on("UW", date(2026, 3, 1)))
assert [(r.currency, r.rate, r.effective_from) for r in rates] == [
("CNY", Decimal("4.2"), date(2026, 1, 1)),
("RUB", Decimal("21"), date(2026, 1, 1)),
("USD", None, None),
]
def test_same_date_is_replaced(session_local) -> None:
_set(date(2026, 1, 1), "21")
_set(date(2026, 1, 1), "21.25", user="head@rshb.ru")
history = run_db(funding.rate_history("UW"))
assert [(r.effective_from, r.rate, r.updated_by) for r in history] == [
(date(2026, 1, 1), Decimal("21.25"), "head@rshb.ru")
]
def test_history_newest_first(session_local) -> None:
_set(date(2026, 1, 1), "21")
_set(date(2026, 1, 1), "4.2", currency="CNY")
_set(date(2026, 7, 28), "18.5")
_set(date(2026, 1, 1), "20.5", portfolio="SALES")
history = run_db(funding.rate_history("UW"))
assert [(r.effective_from, r.currency, r.rate) for r in history] == [
(date(2026, 7, 28), "RUB", Decimal("18.5")),
(date(2026, 1, 1), "CNY", Decimal("4.2")),
(date(2026, 1, 1), "RUB", Decimal("21")),
]
assert {r.updated_by for r in history} == {USER}
def test_future_date_is_allowed(session_local) -> None:
_set(date(2027, 1, 1), "15")
assert _on(date(2026, 12, 31)) is None
assert _on(date(2027, 1, 1)) == Decimal("15")
@pytest.mark.parametrize(
("rate", "errors"),
[
(None, ["Укажите ставку."]),
(Decimal("-0.1"), ["Ставка не может быть отрицательной."]),
(Decimal("16.1234567"), ["Ставка — не больше 6 знаков после запятой."]),
(Decimal("0"), []),
(Decimal("16.123456"), []),
(Decimal("1E+1"), []),
],
)
def test_validate_rate(rate: Decimal | None, errors: list[str]) -> None:
value = FundingRateInput("UW", "RUB", date(2026, 1, 1), rate)
assert validate_rate(value) == errors
def test_invalid_rate_is_not_saved(session_local) -> None:
value = FundingRateInput("UW", "RUB", date(2026, 1, 1), Decimal("-1"))
with pytest.raises(FundingError, match="отрицательной"):
run_db(funding.set_rate(value, USER))
assert run_db(funding.rate_history("UW")) == []
def test_unknown_portfolio_or_currency(session_local) -> None:
with pytest.raises(FundingError, match="Портфеля MM нет"):
_set(date(2026, 1, 1), "21", portfolio="MM")
with pytest.raises(FundingError, match="Валюты EUR нет"):
_set(date(2026, 1, 1), "21", currency="EUR")
with pytest.raises(FundingError, match="Портфеля MM нет"):
run_db(funding.rates_on("MM", date(2026, 1, 1)))