Files
fin-tracker/backend/tests/sources/test_tinvest_broker_price.py
Dmitry 4552efbf5d fix(tinvest): чистка отменённых операций, цена бумаги от брокера, логотипы эмитентов
Отменённые и незавершённые операции удаляются из event по raw_tinvest_operation, а не только из текущего окна синка. Бумага, которую MOEX не котирует (структурные ноты, внебиржевые облигации), получает цену из портфеля брокера. Логотип и цвет бренда копируются в instrument (миграция e8b21f6a90c3).
2026-09-19 21:54:47 +03:00

142 lines
5.0 KiB
Python
Raw Permalink 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.
"""A paper the exchange never quotes is priced by the broker's own mark (T-Invest snapshot)."""
from __future__ import annotations
from datetime import UTC, date, datetime
from decimal import Decimal
from sqlalchemy import select
from factories import make_account, make_instrument, make_price
from fintracker.db import get_sessionmaker
from fintracker.models import AccountKind, AccountRole, AssetClass, Instrument
from fintracker.models.pricing import PriceDaily
from fintracker.sources.tinvest.client import PortfolioSnapshot, PositionLine
from fintracker.sources.tinvest.sync import _store_snapshot
D = Decimal
CAPTURED = datetime(2026, 9, 19, 17, 10, tzinfo=UTC) # 20:10 in Moscow, the same calendar day
DAY = date(2026, 9, 19)
async def with_uid(instrument_id: int, uid: str) -> None:
async with get_sessionmaker()() as session:
row = await session.get(Instrument, instrument_id)
assert row is not None
row.tinvest_uid = uid
await session.commit()
def line(uid: str, *, price: str | None = "12354.6177", qty: str = "2") -> PositionLine:
return PositionLine(
instrument_uid=uid,
figi=None,
quantity=D(qty),
average_price=D("11353.3839"),
current_price=None if price is None else D(price),
currency="RUB",
instrument_type="bond",
)
async def store(account: int, *positions: PositionLine, at: datetime = CAPTURED) -> None:
snapshot = PortfolioSnapshot(
account_id="tinv-1", captured_at=at, positions=list(positions), cash=[], payload={}
)
async with get_sessionmaker()() as session:
await _store_snapshot(session, account, snapshot)
await session.commit()
async def prices(instrument_id: int) -> list[PriceDaily]:
async with get_sessionmaker()() as session:
return list(
(
await session.execute(
select(PriceDaily)
.where(PriceDaily.instrument_id == instrument_id)
.order_by(PriceDaily.d)
)
)
.scalars()
.all()
)
async def broker_account() -> int:
return await make_account(
name="Т", kind=AccountKind.broker, role=AccountRole.investment, balance=None,
source="tinvest", source_id="tinv-1",
) # fmt: skip
async def test_an_unquoted_paper_gets_the_brokers_mark_as_the_days_close(app):
account = await broker_account()
note = await make_instrument(ticker="SIBN6P4", asset_class=AssetClass.bond, board=None)
await with_uid(note, "uid-note")
await store(account, line("uid-note"))
(row,) = await prices(note)
assert (row.d, row.close, row.currency, row.source) == (DAY, D("12354.6177"), "RUB", "tinvest")
async def test_the_mark_is_refreshed_within_the_day(app):
account = await broker_account()
note = await make_instrument(ticker="SIBN6P4", asset_class=AssetClass.bond, board=None)
await with_uid(note, "uid-note")
await store(account, line("uid-note", price="12000"))
await store(account, line("uid-note", price="12100"))
(row,) = await prices(note)
assert row.close == D("12100")
async def test_an_exchange_priced_paper_is_left_to_the_exchange(app):
account = await broker_account()
sber = await make_instrument(ticker="SBER")
await with_uid(sber, "uid-sber")
await make_price(date(2026, 9, 18), instrument_id=sber, close="300")
async with get_sessionmaker()() as session:
row = (
await session.execute(select(PriceDaily).where(PriceDaily.instrument_id == sber))
).scalar_one()
row.source = "moex"
await session.commit()
await store(account, line("uid-sber", price="999"))
assert [(p.d, p.source) for p in await prices(sber)] == [(date(2026, 9, 18), "moex")]
async def test_it_never_overwrites_another_sources_row_for_the_day(app):
account = await broker_account()
note = await make_instrument(ticker="SIBN6P4", asset_class=AssetClass.bond, board=None)
await with_uid(note, "uid-note")
await make_price(DAY, instrument_id=note, close="13000") # a manual entry, say
async with get_sessionmaker()() as session:
row = (
await session.execute(select(PriceDaily).where(PriceDaily.instrument_id == note))
).scalar_one()
row.source = "manual"
await session.commit()
await store(account, line("uid-note", price="12354"))
(row,) = await prices(note)
assert (row.close, row.source) == (D("13000"), "manual")
async def test_cash_and_lines_without_a_mark_are_skipped(app):
account = await broker_account()
rub = await make_instrument(ticker="RUB000UTSTOM", asset_class=AssetClass.currency, board=None)
bare = await make_instrument(ticker="BARE", asset_class=AssetClass.bond, board=None)
await with_uid(rub, "uid-rub")
await with_uid(bare, "uid-bare")
await store(account, line("uid-rub", price="1"), line("uid-bare", price=None))
assert await prices(rub) == []
assert await prices(bare) == []