"""Shadow matching: which report rows are the API's rows seen twice, and which are news.""" from __future__ import annotations from datetime import date from decimal import Decimal from sqlalchemy import select from factories import make_account, make_event, make_instrument from fintracker.analytics import FINDINGS from fintracker.db import get_sessionmaker from fintracker.ledger.dedupe import match_shadow_events from fintracker.models import AccountKind, AccountRole, Event, EventKind, EventStatus TRADE_DAY = date(2026, 3, 10) async def broker_account() -> int: return await make_account( name="Брокерский", kind=AccountKind.broker, role=AccountRole.investment, balance=None, include_in_net_worth=False, source="tinvest", ) async def run() -> tuple[int, int]: FINDINGS.reset() async with get_sessionmaker()() as session: result = await match_shadow_events(session) await session.commit() return result.matched, result.unmatched async def matched_ids() -> dict[int, int | None]: async with get_sessionmaker()() as session: rows = ( await session.execute(select(Event).where(Event.status == EventStatus.shadow)) ).scalars() return {e.id: (e.meta or {}).get("matched_event_id") for e in rows} async def test_price_within_half_a_percent_is_the_same_trade(app): account = await broker_account() instrument = await make_instrument(ticker="GAZP") api = await make_event( TRADE_DAY, account_id=account, kind=EventKind.buy, instrument_id=instrument, quantity="10", price="100", amount="-1000", ) shadow = await make_event( TRADE_DAY, account_id=account, kind=EventKind.buy, instrument_id=instrument, quantity="10", price="100.4", amount="-1004", status=EventStatus.shadow, ) matched, unmatched = await run() assert (matched, unmatched) == (1, 0) assert await matched_ids() == {shadow: api} assert FINDINGS.items == [] async def test_price_off_by_two_percent_is_not_matched_and_is_reported(app): account = await broker_account() instrument = await make_instrument(ticker="GAZP") await make_event( TRADE_DAY, account_id=account, kind=EventKind.buy, instrument_id=instrument, quantity="10", price="100", amount="-1000", ) shadow = await make_event( TRADE_DAY, account_id=account, kind=EventKind.buy, instrument_id=instrument, quantity="10", price="102", amount="-1020", status=EventStatus.shadow, ) matched, unmatched = await run() assert (matched, unmatched) == (0, 1) assert await matched_ids() == {shadow: None} assert [f.check_name for f in FINDINGS.items] == ["report_only_event"] assert "нет в API" in FINDINGS.items[0].detail async def test_one_confirmed_event_closes_only_one_shadow(app): account = await broker_account() instrument = await make_instrument(ticker="GAZP") await make_event( TRADE_DAY, account_id=account, kind=EventKind.buy, instrument_id=instrument, quantity="10", price="100", amount="-1000", ) first = await make_event( TRADE_DAY, account_id=account, kind=EventKind.buy, instrument_id=instrument, quantity="10", price="100", amount="-1000", status=EventStatus.shadow, ) second = await make_event( TRADE_DAY, account_id=account, kind=EventKind.buy, instrument_id=instrument, quantity="10", price="100", amount="-1000", status=EventStatus.shadow, ) matched, unmatched = await run() assert (matched, unmatched) == (1, 1) marks = await matched_ids() assert sorted(marks) == sorted([first, second]) assert sum(1 for v in marks.values() if v is not None) == 1 assert [f.check_name for f in FINDINGS.items] == ["report_only_event"] async def test_settlement_date_one_business_day_later_still_matches(app): """The report prints T+1 where the API prints T — the relaxed level exists for this.""" account = await broker_account() instrument = await make_instrument(ticker="GAZP") api = await make_event( date(2026, 3, 13), # Friday account_id=account, kind=EventKind.buy, instrument_id=instrument, quantity="10", price="100", amount="-1000", ) shadow = await make_event( date(2026, 3, 16), # the next business day account_id=account, kind=EventKind.buy, instrument_id=instrument, quantity="10", price="100", amount="-1000", status=EventStatus.shadow, ) matched, unmatched = await run() assert (matched, unmatched) == (1, 0) assert await matched_ids() == {shadow: api} async def test_an_exact_date_wins_over_a_next_day_candidate(app): account = await broker_account() instrument = await make_instrument(ticker="GAZP") same_day = await make_event( TRADE_DAY, account_id=account, kind=EventKind.buy, instrument_id=instrument, quantity="10", price="100", amount="-1000", ) await make_event( date(2026, 3, 11), account_id=account, kind=EventKind.buy, instrument_id=instrument, quantity="10", price="100", amount="-1000", ) shadow = await make_event( TRADE_DAY, account_id=account, kind=EventKind.buy, instrument_id=instrument, quantity="10", price="100", amount="-1000", status=EventStatus.shadow, ) await run() assert await matched_ids() == {shadow: same_day} async def test_quantities_must_agree_exactly(app): account = await broker_account() instrument = await make_instrument(ticker="GAZP") await make_event( TRADE_DAY, account_id=account, kind=EventKind.buy, instrument_id=instrument, quantity="10", price="100", amount="-1000", ) await make_event( TRADE_DAY, account_id=account, kind=EventKind.buy, instrument_id=instrument, quantity=Decimal("11"), price="100", amount="-1100", status=EventStatus.shadow, ) matched, unmatched = await run() assert (matched, unmatched) == (0, 1)