"""Tests for completeness validation.""" import pytest from unittest.mock import patch from exchange_data_manager.candles.models import Candle, CandleRequest, RequestMode from exchange_data_manager.cache.completeness import ( check_completeness, find_missing_ranges, CompletenessResult, ) def make_candle(time: int, **kwargs) -> Candle: """Helper to create candles with defaults.""" return Candle( time=time, open=kwargs.get("open", 50000.0), high=kwargs.get("high", 50100.0), low=kwargs.get("low", 49900.0), close=kwargs.get("close", 50050.0), volume=kwargs.get("volume", 10.0), ) class TestCheckCompletenessRangeMode: """Tests for RANGE mode completeness.""" def test_complete_range(self): """Test complete data for RANGE mode.""" # 3 candles at 1m intervals candles = [ make_candle(1709337600), # Second 0 make_candle(1709337660), # Second 60 make_candle(1709337720), # Second 120 ] request = CandleRequest( exchange="binance", symbol="BTC/USDT", timeframe="1m", start=1709337600, end=1709337720, ) result = check_completeness(candles, request) assert result.is_complete is True assert result.actual_count == 3 def test_missing_at_start(self): """Test incomplete when missing data at start.""" # Missing first 3 candles - larger than interval + tolerance candles = [ make_candle(1709337780), # Missing 0:00, 0:01, 0:02 make_candle(1709337840), ] request = CandleRequest( exchange="binance", symbol="BTC/USDT", timeframe="1m", start=1709337600, end=1709337840, ) result = check_completeness(candles, request) assert result.is_complete is False assert result.missing_start_ms == 1709337600000 assert "start" in result.reason.lower() def test_missing_at_end(self): """Test incomplete when missing data at end.""" # Missing last 3 candles - larger than interval + tolerance candles = [ make_candle(1709337600), make_candle(1709337660), # Missing 0:02, 0:03, 0:04 ] request = CandleRequest( exchange="binance", symbol="BTC/USDT", timeframe="1m", start=1709337600, end=1709337840, # 4 minutes later ) result = check_completeness(candles, request) assert result.is_complete is False assert result.missing_end_ms == 1709337840000 assert "end" in result.reason.lower() def test_empty_candles(self): """Test that empty candles returns incomplete.""" request = CandleRequest( exchange="binance", symbol="BTC/USDT", timeframe="1m", start=1709337600, end=1709337720, ) result = check_completeness([], request) assert result.is_complete is False assert result.missing_start_ms == 1709337600000 class TestCheckCompletenessLastNMode: """Tests for LAST_N mode completeness.""" def test_enough_candles(self): """Test complete when have enough candles.""" candles = [make_candle(1709337600 + i * 60) for i in range(100)] request = CandleRequest( exchange="binance", symbol="BTC/USDT", timeframe="1m", limit=100, ) result = check_completeness(candles, request) assert result.is_complete is True assert result.actual_count == 100 def test_not_enough_candles(self): """Test incomplete when don't have enough candles.""" candles = [make_candle(1709337600 + i * 60) for i in range(50)] request = CandleRequest( exchange="binance", symbol="BTC/USDT", timeframe="1m", limit=100, ) result = check_completeness(candles, request) assert result.is_complete is False assert result.expected_count == 100 assert result.actual_count == 50 def test_more_than_enough_candles(self): """Test complete when have more than enough candles.""" candles = [make_candle(1709337600 + i * 60) for i in range(150)] request = CandleRequest( exchange="binance", symbol="BTC/USDT", timeframe="1m", limit=100, ) result = check_completeness(candles, request) assert result.is_complete is True class TestCheckCompletenessSinceMode: """Tests for SINCE mode completeness.""" @patch("exchange_data_manager.cache.completeness.now_millis") def test_complete_to_now(self, mock_now): """Test complete when data reaches current time.""" mock_now.return_value = 1709337720000 # 2 minutes after start candles = [ make_candle(1709337600), make_candle(1709337660), make_candle(1709337720), ] request = CandleRequest( exchange="binance", symbol="BTC/USDT", timeframe="1m", start=1709337600, ) result = check_completeness(candles, request) assert result.is_complete is True class TestCheckCompletenessOpenMode: """Tests for OPEN mode completeness.""" @patch("exchange_data_manager.cache.completeness.now_millis") def test_recent_data_complete(self, mock_now): """Test complete when data is recent.""" mock_now.return_value = 1709337720000 candles = [ make_candle(1709337660), # 1 minute ago make_candle(1709337720), # Current ] request = CandleRequest( exchange="binance", symbol="BTC/USDT", timeframe="1m", ) result = check_completeness(candles, request) assert result.is_complete is True @patch("exchange_data_manager.cache.completeness.now_millis") def test_stale_data_incomplete(self, mock_now): """Test incomplete when data is stale.""" mock_now.return_value = 1709341200000 # Much later candles = [ make_candle(1709337600), # Old data make_candle(1709337660), ] request = CandleRequest( exchange="binance", symbol="BTC/USDT", timeframe="1m", ) result = check_completeness(candles, request) assert result.is_complete is False assert "stale" in result.reason.lower() class TestFindMissingRanges: """Tests for find_missing_ranges function.""" def test_no_missing(self): """Test no missing ranges when data is complete.""" candles = [ make_candle(1709337600), make_candle(1709337660), make_candle(1709337720), ] request = CandleRequest( exchange="binance", symbol="BTC/USDT", timeframe="1m", start=1709337600, end=1709337720, ) missing = find_missing_ranges(candles, request) assert len(missing) == 0 def test_gap_in_middle(self): """Test detects gap in middle of data.""" candles = [ make_candle(1709337600), # Gap: 1709337660 make_candle(1709337720), ] request = CandleRequest( exchange="binance", symbol="BTC/USDT", timeframe="1m", start=1709337600, end=1709337720, ) missing = find_missing_ranges(candles, request) assert len(missing) == 1 assert missing[0][0] == 1709337660000 def test_multiple_gaps(self): """Test detects multiple gaps.""" candles = [ make_candle(1709337600), # Gap: 1709337660 make_candle(1709337720), # Gap: 1709337780 make_candle(1709337840), ] request = CandleRequest( exchange="binance", symbol="BTC/USDT", timeframe="1m", start=1709337600, end=1709337840, ) missing = find_missing_ranges(candles, request) assert len(missing) == 2