DRRK/tests/conftest.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

136 lines
4.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
import json
import sys
from datetime import date
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
FIXTURES = ROOT / "tests" / "fixtures"
FIXTURE_ISIN = "RU000A106169"
FIXTURE_DATE = date(2026, 10, 8)
@pytest.fixture
def fixtures_dir() -> Path:
"""Сохранённые реальные ответы MOEX ISS / ЦБ (байт в байт, см. .gitattributes)."""
return FIXTURES
@pytest.fixture
def session_local(tmp_path: Path, monkeypatch):
"""Временный SQLite со схемой из миграций (с сидами) вместо SessionLocal
во всех модулях сервисов. Postgres не нужен."""
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from sqlalchemy.pool import NullPool
from alembic import command
from alembic.config import Config
from app.core.config import settings
from app.services import (
business_calendar,
funding,
fx_rates,
manual_operations,
market_data,
positions,
quik_import,
request_log,
securities,
)
db_url = f"sqlite+aiosqlite:///{(tmp_path / 'drrk.db').as_posix()}"
monkeypatch.setattr(settings, "DATABASE_URL", db_url)
monkeypatch.setattr(settings, "DATABASE_SCHEMA", "")
cfg = Config()
cfg.set_main_option("script_location", str(ROOT / "alembic"))
command.upgrade(cfg, "head")
factory = async_sessionmaker(
bind=create_async_engine(db_url, poolclass=NullPool),
autoflush=False,
expire_on_commit=False,
)
for module in (
business_calendar,
securities,
market_data,
fx_rates,
funding,
positions,
request_log,
quik_import,
manual_operations,
):
monkeypatch.setattr(module, "SessionLocal", factory)
return factory
@pytest.fixture
def add_trade(session_local):
"""add_trade(ISIN, код операции, дата, количество, портфель="UW") — сделка
напрямую в БД, без файла. Бумага уже в справочнике."""
from decimal import Decimal
from sqlalchemy import select
from app.db.base import run_db
from app.db.models import Operation, Security
from app.repositories.reference import ReferenceRepository
async def _add(
isin: str, code: str, on_date: date, quantity: str, portfolio: str
) -> None:
async with session_local() as session:
reference = ReferenceRepository(session)
security = (
await session.execute(select(Security).where(Security.isin == isin))
).scalar_one()
session.add(
Operation(
security_id=security.id,
portfolio_id=await reference.get_portfolio_id(portfolio),
operation_type_id=(await reference.operation_type_ids())[code],
currency_id=security.currency_id,
trade_date=on_date,
quantity=Decimal(quantity),
amount=Decimal("1000"),
created_by="system",
updated_by="system",
)
)
await session.commit()
def add_trade(
isin: str, code: str, on_date: date, quantity: str, portfolio: str = "UW"
) -> None:
run_db(_add(isin, code, on_date, quantity, portfolio))
return add_trade
@pytest.fixture
def fixture_moex(fixtures_dir: Path):
"""Фейковый MOEX, отвечающий реальными фикстурами РЭО-02 за 08.10.2026."""
from app.integrations.moex import parse_history, parse_security
from tests.unit.fakes import FakeMoex, search_hit
security_payload = json.loads(
(fixtures_dir / "moex_security_RU000A106169.json").read_text("utf-8")
)
history_payload = json.loads(
(fixtures_dir / "moex_history_RU000A106169_2026-10-08.json").read_text("utf-8")
)
return FakeMoex(
search={FIXTURE_ISIN: search_hit(FIXTURE_ISIN, FIXTURE_ISIN)},
securities={FIXTURE_ISIN: parse_security(security_payload)},
history={
FIXTURE_ISIN: parse_history(history_payload, FIXTURE_DATE, ["TQCB", "TQOB"])
},
)