You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

186 lines
5.8 KiB
Python

from __future__ import annotations
import pyarrow as pa
import pyarrow.parquet as pq
from libs.intraday.cache import DailyBarCache, IntradayCache
def test_legacy_cache_file_is_treated_as_miss_and_removed(tmp_path) -> None:
cache = IntradayCache(str(tmp_path))
cache_path = tmp_path / "AAPL" / "2026-01-05.parquet"
cache_path.parent.mkdir(parents=True, exist_ok=True)
legacy = pa.table({
"timestamp": ["2026-01-05T13:30:00Z"],
"open": [100.0],
"high": [101.0],
"low": [99.5],
"close": [100.5],
"volume": [1000.0],
"vwap": [100.4],
})
pq.write_table(legacy, str(cache_path))
assert cache.has("AAPL", "2026-01-05") is False
assert cache.get("AAPL", "2026-01-05") is None
assert cache_path.exists() is False
def test_cache_put_writes_metadata_valid_file(tmp_path) -> None:
cache = IntradayCache(str(tmp_path))
bars = []
for i in range(10):
bars.append({
"timestamp": f"2026-01-05T13:{30 + i:02d}:00Z",
"open": 100.0,
"high": 101.0,
"low": 99.5,
"close": 100.5,
"volume": 1000.0,
"vwap": 100.4,
})
cache.put("AAPL", "2026-01-05", bars)
assert cache.has("AAPL", "2026-01-05") is True
assert cache.get("AAPL", "2026-01-05") == bars
def test_intraday_cache_rejects_too_few_rows(tmp_path) -> None:
cache = IntradayCache(str(tmp_path))
cache_path = tmp_path / "AAPL" / "2026-01-05.parquet"
cache_path.parent.mkdir(parents=True, exist_ok=True)
tiny = pa.table({
"timestamp": [f"2026-01-05T13:{30+i:02d}:00Z" for i in range(5)],
"open": [100.0] * 5,
"high": [101.0] * 5,
"low": [99.5] * 5,
"close": [100.5] * 5,
"volume": [1000.0] * 5,
"vwap": [100.4] * 5,
}, schema=pa.schema([
pa.field("timestamp", pa.string()),
pa.field("open", pa.float64()),
pa.field("high", pa.float64()),
pa.field("low", pa.float64()),
pa.field("close", pa.float64()),
pa.field("volume", pa.float64()),
pa.field("vwap", pa.float64()),
]).with_metadata({
b"intraday_cache_version": b"3",
b"intraday_cache_source": b"api_v1_alpaca_intraday",
b"intraday_cache_interval": b"5min",
}))
pq.write_table(tiny, str(cache_path))
assert cache.has("AAPL", "2026-01-05") is False
assert cache_path.exists() is False
def test_intraday_cache_accepts_existing_positive_file_without_kind_metadata(tmp_path) -> None:
cache = IntradayCache(str(tmp_path))
cache_path = tmp_path / "AAPL" / "2026-01-05.parquet"
cache_path.parent.mkdir(parents=True, exist_ok=True)
rows = 12
table = pa.table({
"timestamp": [f"2026-01-05T13:{30+i:02d}:00Z" for i in range(rows)],
"open": [100.0] * rows,
"high": [101.0] * rows,
"low": [99.5] * rows,
"close": [100.5] * rows,
"volume": [1000.0] * rows,
"vwap": [100.4] * rows,
}, schema=pa.schema([
pa.field("timestamp", pa.string()),
pa.field("open", pa.float64()),
pa.field("high", pa.float64()),
pa.field("low", pa.float64()),
pa.field("close", pa.float64()),
pa.field("volume", pa.float64()),
pa.field("vwap", pa.float64()),
]).with_metadata({
b"intraday_cache_version": b"3",
b"intraday_cache_source": b"api_v1_alpaca_intraday",
b"intraday_cache_interval": b"5min",
}))
pq.write_table(table, str(cache_path))
assert cache.has("AAPL", "2026-01-05") is True
assert len(cache.get("AAPL", "2026-01-05") or []) == rows
def test_intraday_cache_negative_entry_suppresses_rereads(tmp_path) -> None:
cache = IntradayCache(str(tmp_path))
cache.put_negative("AAPL", "2026-01-05", reason="sparse")
assert cache.has("AAPL", "2026-01-05") is True
assert cache.get("AAPL", "2026-01-05") == []
def test_daily_cache_put_and_get_uses_requested_coverage_range(tmp_path) -> None:
cache = DailyBarCache(str(tmp_path))
bars = [
{
"date": "2026-01-02",
"open": 100.0,
"high": 101.0,
"low": 99.0,
"close": 100.5,
"volume": 1000.0,
},
{
"date": "2026-01-05",
"open": 101.0,
"high": 102.0,
"low": 100.0,
"close": 101.5,
"volume": 1200.0,
},
]
cache.put("AAPL", "2026-01-01", "2026-01-10", bars)
assert cache.get("AAPL", "2026-01-01", "2026-01-10") == bars
assert cache.get("AAPL", "2026-01-02", "2026-01-05") == bars
assert cache.get("AAPL", "2025-12-31", "2026-01-10") is None
def test_daily_cache_rejects_egregiously_partial_warmup_file(tmp_path) -> None:
cache = DailyBarCache(str(tmp_path))
cache_path = tmp_path / "BLD.parquet"
rows = []
for i in range(11):
rows.append(
{
"date": f"2026-04-{6 + i:02d}",
"open": 100.0 + i,
"high": 101.0 + i,
"low": 99.0 + i,
"close": 100.5 + i,
"volume": 1_000.0 + i,
}
)
schema = pa.schema([
pa.field("date", pa.string()),
pa.field("open", pa.float64()),
pa.field("high", pa.float64()),
pa.field("low", pa.float64()),
pa.field("close", pa.float64()),
pa.field("volume", pa.float64()),
]).with_metadata({
b"daily_cache_version": b"1",
b"daily_cache_source": b"api_v1_price_data",
b"daily_cache_interval": b"1d",
b"daily_cache_coverage_start": b"2026-01-20",
b"daily_cache_coverage_end": b"2026-04-20",
})
pq.write_table(pa.Table.from_pylist(rows, schema=schema), str(cache_path))
assert cache.get("BLD", "2026-01-20", "2026-04-20") is None
assert cache_path.exists() is False