"""Tests for the ingestion service — honest reporting of empty provider fetches.""" from __future__ import annotations from datetime import date, timedelta import pytest from app.models.ohlcv import OHLCVRecord from app.models.settings import IngestionProgress from app.models.ticker import Ticker from app.providers.protocol import OHLCVData from app.services import ingestion_service as svc from tests.conftest import MockMarketDataProvider, _test_session_factory # type: ignore @pytest.fixture async def session(): async with _test_session_factory() as s: yield s async def _add_ticker(session, symbol: str) -> Ticker: ticker = Ticker(symbol=symbol) session.add(ticker) await session.commit() await session.refresh(ticker) return ticker def _bars(symbol: str, n: int) -> list[OHLCVData]: today = date.today() return [ OHLCVData(ticker=symbol, date=today - timedelta(days=i), open=100.0, high=101.0, low=99.0, close=100.0, volume=1000) for i in range(n) ] async def test_empty_fetch_on_new_ticker_reports_no_data(session): # A non-US symbol (e.g. RHM/Rheinmetall) Alpaca doesn't cover → empty bars. # This must NOT report success; it surfaces as no_data. await _add_ticker(session, "RHM") result = await svc.fetch_and_ingest(session, MockMarketDataProvider(ohlcv_data=[]), "RHM") assert result.status == "no_data" assert result.records_ingested == 0 assert "provider" in (result.message or "").lower() async def test_happy_path_ingests_bars(session): await _add_ticker(session, "AAA") result = await svc.fetch_and_ingest(session, MockMarketDataProvider(ohlcv_data=_bars("AAA", 3)), "AAA") assert result.status == "complete" assert result.records_ingested == 3 async def test_incremental_fetch_overlaps_latest_session_and_updates_partial_bar(session): """Once today exists, a live refresh must fetch and overwrite it again.""" ticker = await _add_ticker(session, "LIVE") today = date.today() session.add_all([ OHLCVRecord( ticker_id=ticker.id, date=today - timedelta(days=i), open=100.0, high=101.0, low=99.0, close=100.0, volume=1000, ) for i in range(200) ]) session.add(IngestionProgress(ticker_id=ticker.id, last_ingested_date=today)) await session.commit() provider = MockMarketDataProvider(ohlcv_data=[ OHLCVData( ticker="LIVE", date=today, open=100.0, high=124.0, low=99.0, close=123.0, volume=2000, ) ]) result = await svc.fetch_and_ingest(session, provider, "LIVE") assert provider.calls == [{ "ticker": "LIVE", "start_date": today, "end_date": today, }] assert result.status == "complete" assert result.records_ingested == 1 records = await svc.price_service.query_ohlcv(session, "LIVE", today, today) assert records[0].close == 123.0 assert records[0].volume == 2000 async def test_empty_fetch_with_existing_history_is_up_to_date(session): # Covered ticker, just no new bars in the window → complete, not no_data. await _add_ticker(session, "BBB") await svc.fetch_and_ingest(session, MockMarketDataProvider(ohlcv_data=_bars("BBB", 2)), "BBB") result = await svc.fetch_and_ingest(session, MockMarketDataProvider(ohlcv_data=[]), "BBB") assert result.status == "complete" assert result.records_ingested == 0 async def test_empty_fetch_with_stale_history_reports_stale(session): # Provider returns nothing but last stored bar is weeks old — classic # rename/delist case (e.g. SATS after it became ECHO). Must not look like success. await _add_ticker(session, "SATS") today = date.today() old = [ OHLCVData( ticker="SATS", date=today - timedelta(days=20 + i), open=100.0, high=101.0, low=99.0, close=100.0, volume=1000, ) for i in range(3) ] await svc.fetch_and_ingest(session, MockMarketDataProvider(ohlcv_data=old), "SATS") result = await svc.fetch_and_ingest(session, MockMarketDataProvider(ohlcv_data=[]), "SATS") assert result.status == "stale" assert result.records_ingested == 0 assert result.last_date is not None assert "renamed" in (result.message or "").lower() or "halted" in (result.message or "").lower()