from datetime import UTC, datetime from pathlib import Path import pandas as pd import pytest from multi_swarm.data.ohlcv_loader import OHLCVLoader, OHLCVRequest @pytest.fixture def sample_ohlcv_rows(): base_ts = int(datetime(2024, 1, 1, tzinfo=UTC).timestamp() * 1000) rows = [] for i in range(48): rows.append( [base_ts + i * 3600 * 1000, 40000 + i, 40100 + i, 39900 + i, 40050 + i, 100.0 + i] ) return rows def test_loader_fetches_and_caches(tmp_path: Path, mocker, sample_ohlcv_rows): fake_exchange = mocker.MagicMock() fake_exchange.fetch_ohlcv.return_value = sample_ohlcv_rows mocker.patch("multi_swarm.data.ohlcv_loader.ccxt.binance", return_value=fake_exchange) loader = OHLCVLoader(cache_dir=tmp_path) req = OHLCVRequest( symbol="BTC/USDT", timeframe="1h", start=datetime(2024, 1, 1, tzinfo=UTC), end=datetime(2024, 1, 3, tzinfo=UTC), ) df = loader.load(req) assert isinstance(df, pd.DataFrame) assert list(df.columns) == ["open", "high", "low", "close", "volume"] assert len(df) == 48 assert df.index.is_monotonic_increasing cache_files = list(tmp_path.glob("*.parquet")) assert len(cache_files) == 1 def test_loader_uses_cache_on_second_call(tmp_path: Path, mocker, sample_ohlcv_rows): fake_exchange = mocker.MagicMock() fake_exchange.fetch_ohlcv.return_value = sample_ohlcv_rows mocker.patch("multi_swarm.data.ohlcv_loader.ccxt.binance", return_value=fake_exchange) loader = OHLCVLoader(cache_dir=tmp_path) req = OHLCVRequest( symbol="BTC/USDT", timeframe="1h", start=datetime(2024, 1, 1, tzinfo=UTC), end=datetime(2024, 1, 3, tzinfo=UTC), ) df1 = loader.load(req) df2 = loader.load(req) assert fake_exchange.fetch_ohlcv.call_count == 1 # 48 < limit, single batch pd.testing.assert_frame_equal(df1, df2) # Seconda chiamata legge da cache, non chiama exchange fake_exchange.fetch_ohlcv.reset_mock() df3 = loader.load(req) assert fake_exchange.fetch_ohlcv.call_count == 0 pd.testing.assert_frame_equal(df1, df3)