"""Ставки фондирования (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)))