Files
signal-platform/tests/unit/test_ingestion_service.py
T

139 lines
4.5 KiB
Python

"""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()