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