"""Tests for CerberoOHLCVLoader (mocked CerberoClient).""" from datetime import UTC, datetime from pathlib import Path import pandas as pd import pytest from multi_swarm.data.cerbero_ohlcv import CerberoOHLCVLoader, OHLCVRequest @pytest.fixture def sample_records_object() -> list[dict[str, float | int]]: base = int(datetime(2024, 1, 1, tzinfo=UTC).timestamp() * 1000) return [ { "ts": base + i * 3600 * 1000, "open": 40000 + i, "high": 40100 + i, "low": 39900 + i, "close": 40050 + i, "volume": 100.0 + i, } for i in range(48) ] @pytest.fixture def sample_records_array() -> list[list[float | int]]: base = int(datetime(2024, 1, 1, tzinfo=UTC).timestamp() * 1000) return [ [base + i * 3600 * 1000, 40000 + i, 40100 + i, 39900 + i, 40050 + i, 100.0 + i] for i in range(48) ] def test_loader_parses_object_records( tmp_path: Path, mocker, sample_records_object ) -> None: fake_client = mocker.MagicMock() fake_client.call_tool.return_value = {"candles": sample_records_object} loader = CerberoOHLCVLoader(client=fake_client, cache_dir=tmp_path) df = loader.load( OHLCVRequest( symbol="BTC-PERPETUAL", timeframe="1h", start=datetime(2024, 1, 1, tzinfo=UTC), end=datetime(2024, 1, 3, tzinfo=UTC), exchange="deribit", ) ) assert list(df.columns) == ["open", "high", "low", "close", "volume"] assert len(df) == 48 assert df.index.tz is not None fake_client.call_tool.assert_called_once_with( "deribit", "get_historical", { "instrument": "BTC-PERPETUAL", "start_date": "2024-01-01T00:00:00+00:00", "end_date": "2024-01-03T00:00:00+00:00", "resolution": "1h", }, ) def test_loader_parses_array_records( tmp_path: Path, mocker, sample_records_array ) -> None: fake_client = mocker.MagicMock() fake_client.call_tool.return_value = {"candles": sample_records_array} loader = CerberoOHLCVLoader(client=fake_client, cache_dir=tmp_path) df = loader.load( OHLCVRequest( symbol="BTC-PERPETUAL", timeframe="1h", start=datetime(2024, 1, 1, tzinfo=UTC), end=datetime(2024, 1, 3, tzinfo=UTC), ) ) assert len(df) == 48 def test_loader_uses_cache_on_second_call( tmp_path: Path, mocker, sample_records_object ) -> None: fake_client = mocker.MagicMock() fake_client.call_tool.return_value = {"candles": sample_records_object} loader = CerberoOHLCVLoader(client=fake_client, cache_dir=tmp_path) req = OHLCVRequest( symbol="BTC-PERPETUAL", timeframe="1h", start=datetime(2024, 1, 1, tzinfo=UTC), end=datetime(2024, 1, 3, tzinfo=UTC), ) df1 = loader.load(req) fake_client.call_tool.reset_mock() df2 = loader.load(req) assert fake_client.call_tool.call_count == 0 pd.testing.assert_frame_equal(df1, df2) def test_loader_unsupported_exchange_raises(tmp_path: Path, mocker) -> None: fake_client = mocker.MagicMock() loader = CerberoOHLCVLoader(client=fake_client, cache_dir=tmp_path) req = OHLCVRequest( symbol="X", timeframe="1h", start=datetime(2024, 1, 1, tzinfo=UTC), end=datetime(2024, 1, 2, tzinfo=UTC), exchange="kraken", ) with pytest.raises(ValueError, match="unsupported exchange"): loader.load(req) def test_loader_bybit_args(tmp_path: Path, mocker, sample_records_object) -> None: fake_client = mocker.MagicMock() fake_client.call_tool.return_value = {"candles": sample_records_object} loader = CerberoOHLCVLoader(client=fake_client, cache_dir=tmp_path) loader.load( OHLCVRequest( symbol="BTCUSDT", timeframe="1h", start=datetime(2024, 1, 1, tzinfo=UTC), end=datetime(2024, 1, 3, tzinfo=UTC), exchange="bybit", ) ) args = fake_client.call_tool.call_args.args assert args[0] == "bybit" assert args[1] == "get_historical" payload = args[2] assert payload["symbol"] == "BTCUSDT" assert payload["interval"] == 60