- 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
99 lines
3.2 KiB
Python
99 lines
3.2 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import date
|
|
|
|
import pytest
|
|
|
|
from app.jobs import update_market as job
|
|
from app.services.fx_rates import FxRatesUpdateResult
|
|
from app.services.market_data import MarketDataUpdateResult
|
|
|
|
|
|
@pytest.fixture
|
|
def calls(monkeypatch) -> dict:
|
|
"""Подменяет сервисы: запоминает аргументы, отдаёт заданный результат."""
|
|
state: dict = {
|
|
"market": MarketDataUpdateResult(on_date=date(2026, 10, 8), updated=1),
|
|
"fx": FxRatesUpdateResult(date_to=date(2026, 10, 8), saved=1),
|
|
"market_args": None,
|
|
"fx_args": None,
|
|
}
|
|
|
|
async def _market_update(on_date, user): # noqa: ANN001
|
|
state["market_args"] = (on_date, user)
|
|
if isinstance(state["market"], Exception):
|
|
raise state["market"]
|
|
return state["market"]
|
|
|
|
async def _fx_update(up_to, user): # noqa: ANN001
|
|
state["fx_args"] = (up_to, user)
|
|
return state["fx"]
|
|
|
|
monkeypatch.setattr(job.market_data, "update", _market_update)
|
|
monkeypatch.setattr(job.fx_rates, "update", _fx_update)
|
|
return state
|
|
|
|
|
|
def test_explicit_date_runs_both_updates_as_system(calls: dict) -> None:
|
|
code = job.main(["--date", "2026-10-08"])
|
|
|
|
assert code == 0
|
|
assert calls["market_args"] == (date(2026, 10, 8), "system")
|
|
assert calls["fx_args"] == (date(2026, 10, 8), "system")
|
|
|
|
|
|
def test_default_date_is_report_date_by_calendar(calls: dict, monkeypatch) -> None:
|
|
async def _report_date():
|
|
return date(2026, 10, 9)
|
|
|
|
monkeypatch.setattr(job.business_calendar, "report_date", _report_date)
|
|
|
|
assert job.main([]) == 0
|
|
assert calls["market_args"][0] == date(2026, 10, 9)
|
|
|
|
|
|
def test_default_date_without_calendar_skips_weekends_only(
|
|
calls: dict, monkeypatch
|
|
) -> None:
|
|
async def _report_date():
|
|
raise RuntimeError("БД недоступна")
|
|
|
|
monkeypatch.setattr(job.business_calendar, "report_date", _report_date)
|
|
monkeypatch.setattr(job, "previous_business_day", lambda: date(2026, 10, 9))
|
|
|
|
assert job.main([]) == 0
|
|
assert calls["market_args"][0] == date(2026, 10, 9)
|
|
|
|
|
|
def test_no_trades_is_not_an_error(calls: dict) -> None:
|
|
calls["market"] = MarketDataUpdateResult(on_date=date(2026, 10, 8), no_trades=2)
|
|
|
|
assert job.main(["--date", "2026-10-08"]) == 0
|
|
|
|
|
|
def test_security_errors_give_nonzero_exit(calls: dict) -> None:
|
|
calls["market"] = MarketDataUpdateResult(on_date=date(2026, 10, 8), errors=1)
|
|
|
|
assert job.main(["--date", "2026-10-08"]) == 1
|
|
|
|
|
|
def test_fx_error_gives_nonzero_exit(calls: dict) -> None:
|
|
calls["fx"] = FxRatesUpdateResult(date_to=date(2026, 10, 8), error="ЦБ недоступен")
|
|
|
|
assert job.main(["--date", "2026-10-08"]) == 1
|
|
|
|
|
|
def test_crash_in_quotes_still_updates_fx_and_fails(calls: dict) -> None:
|
|
calls["market"] = RuntimeError("БД недоступна")
|
|
|
|
assert job.main(["--date", "2026-10-08"]) == 1
|
|
assert calls["fx_args"] is not None
|
|
|
|
|
|
def test_bad_date_argument_exits_with_2(calls: dict) -> None:
|
|
with pytest.raises(SystemExit) as exc_info:
|
|
job.main(["--date", "08.10.2026"])
|
|
|
|
assert exc_info.value.code == 2
|
|
assert calls["market_args"] is None
|