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.

138 lines
5.7 KiB
Python

"""Tests for TGTC V2 1-minute bar cache (libs/tgtc/cache_1m.py)."""
import datetime as dt
import tempfile
from pathlib import Path
from unittest.mock import patch, MagicMock
import pytest
from libs.intraday.cache import IntradayCache
# ── IntradayCache interval_minutes parameter ──────────────────────────────────
def test_intraday_cache_default_interval():
"""Default IntradayCache still writes/reads 5-min metadata."""
with tempfile.TemporaryDirectory() as tmp:
cache = IntradayCache(cache_dir=tmp, interval_minutes=5)
meta = cache._cache_metadata
assert meta[b"intraday_cache_interval"] == b"5min"
def test_intraday_cache_1min_interval():
"""IntradayCache with interval_minutes=1 writes 1min metadata."""
with tempfile.TemporaryDirectory() as tmp:
cache = IntradayCache(cache_dir=tmp, interval_minutes=1)
meta = cache._cache_metadata
assert meta[b"intraday_cache_interval"] == b"1min"
def test_intraday_cache_1min_round_trip():
"""Writing and reading 1-min bars through IntradayCache round-trips correctly."""
with tempfile.TemporaryDirectory() as tmp:
cache = IntradayCache(cache_dir=tmp, interval_minutes=1)
bars = [
{"timestamp": f"2025-09-12T{9+h:02d}:{m:02d}:00+00:00",
"open": 100.0, "high": 101.0, "low": 99.5, "close": 100.5,
"volume": 1000.0, "vwap": 100.2}
for h in range(0, 7) for m in range(0, 60, 1)
][:60] # 60 bars = 1 hour of 1-min bars
cache.put("TEST", "2025-09-12", bars)
readback = cache.get("TEST", "2025-09-12")
assert readback is not None
assert len(readback) == 60
assert readback[0]["timestamp"] == bars[0]["timestamp"]
def test_intraday_1min_cache_does_not_validate_5min_files():
"""A 5-min cache file should NOT be valid in a 1-min cache (metadata mismatch)."""
with tempfile.TemporaryDirectory() as tmp:
# Write a file via 5-min cache
cache_5m = IntradayCache(cache_dir=tmp, interval_minutes=5)
bars = [
{"timestamp": f"2025-09-12T{9+h:02d}:{m:02d}:00+00:00",
"open": 100.0, "high": 101.0, "low": 99.5, "close": 100.5,
"volume": 1000.0, "vwap": 100.2}
for h in range(0, 2) for m in range(0, 60, 5)
] # 24 bars
cache_5m.put("TEST", "2025-09-12", bars)
# 1-min cache should NOT see it as valid (interval mismatch)
cache_1m = IntradayCache(cache_dir=tmp, interval_minutes=1)
result = cache_1m.get("TEST", "2025-09-12")
assert result is None # invalid metadata → miss
# ── ensure_1m_bars (mocked Oracle) ────────────────────────────────────────────
def _make_bars(n: int) -> list[dict]:
return [
{"timestamp": f"2025-09-12T14:{m:02d}:00+00:00",
"open": 100.0, "high": 101.0, "low": 99.5, "close": 100.5,
"volume": 500.0, "vwap": 100.2}
for m in range(n)
]
def test_ensure_1m_bars_cache_miss_fetches_from_api():
"""On cache miss, ensure_1m_bars fetches from Oracle and caches the result."""
with tempfile.TemporaryDirectory() as tmp:
with patch("libs.tgtc.cache_1m._CACHE_DIR", tmp), \
patch("libs.tgtc.cache_1m._cache", None), \
patch("libs.oracle_client.alpaca.get_multi_intraday_bars",
return_value={"AAPL": _make_bars(390)}) as mock_fetch:
from libs.tgtc import cache_1m
cache_1m._cache = None # reset singleton
result = cache_1m.ensure_1m_bars(["AAPL"], dt.date(2025, 9, 12))
assert "AAPL" in result
assert len(result["AAPL"]) == 390
mock_fetch.assert_called_once()
# Second call should be served from cache (mock not called again)
result2 = cache_1m.ensure_1m_bars(["AAPL"], dt.date(2025, 9, 12))
assert "AAPL" in result2
mock_fetch.assert_called_once() # still only once
def test_ensure_1m_bars_negative_cache_on_empty():
"""Symbols with no API bars get a negative cache entry."""
with tempfile.TemporaryDirectory() as tmp:
with patch("libs.tgtc.cache_1m._CACHE_DIR", tmp), \
patch("libs.tgtc.cache_1m._cache", None), \
patch("libs.oracle_client.alpaca.get_multi_intraday_bars",
return_value={}):
from libs.tgtc import cache_1m
cache_1m._cache = None
result = cache_1m.ensure_1m_bars(["EMPTY"], dt.date(2025, 9, 12))
assert "EMPTY" not in result
# Second call should not fetch from API again
with patch("libs.oracle_client.alpaca.get_multi_intraday_bars") as mock2:
cache_1m.ensure_1m_bars(["EMPTY"], dt.date(2025, 9, 12))
mock2.assert_not_called()
# ── DST-aware ET→UTC conversion ────────────────────────────────────────────────
def test_et_to_utc_naive_edt():
"""In EDT (summer), 10:00 ET = 14:00 UTC."""
from libs.tgtc.gainers_reconstruct import _et_to_utc_naive
import datetime as dt
date = dt.date(2025, 9, 12) # September = EDT
utc = _et_to_utc_naive(date, 10, 0)
assert utc.hour == 14
assert utc.minute == 0
def test_et_to_utc_naive_est():
"""In EST (winter), 10:00 ET = 15:00 UTC."""
from libs.tgtc.gainers_reconstruct import _et_to_utc_naive
import datetime as dt
date = dt.date(2025, 1, 15) # January = EST
utc = _et_to_utc_naive(date, 10, 0)
assert utc.hour == 15
assert utc.minute == 0