"""Tests for gap detection and filling.""" import pytest from exchange_data_manager.candles.models import Candle from exchange_data_manager.cache.gaps import ( find_gaps, count_missing_candles, has_gaps, GapRange, ) from exchange_data_manager.cache.gap_filler import ( fill_gaps, fill_gaps_backward, fill_to_range, ) def make_candle(time: int, close: float = 50000.0) -> Candle: """Helper to create candles with defaults.""" return Candle( time=time, open=close, high=close + 100, low=close - 100, close=close, volume=10.0, ) class TestFindGaps: """Tests for find_gaps function.""" def test_no_gaps(self): """Test no gaps when data is continuous.""" candles = [ make_candle(1709337600), make_candle(1709337660), make_candle(1709337720), ] gaps = find_gaps(candles, "1m") assert len(gaps) == 0 def test_single_candle_gap(self): """Test detects single missing candle (critical regression test).""" candles = [ make_candle(1709337600), # 0:00 # Missing: 1709337660 (0:01) make_candle(1709337720), # 0:02 ] gaps = find_gaps(candles, "1m") assert len(gaps) == 1 assert gaps[0].start_ms == 1709337660000 assert gaps[0].end_ms == 1709337660000 def test_multi_candle_gap(self): """Test detects multiple missing candles.""" candles = [ make_candle(1709337600), # 0:00 # Missing: 0:01, 0:02, 0:03 make_candle(1709337840), # 0:04 ] gaps = find_gaps(candles, "1m") assert len(gaps) == 1 assert gaps[0].start_ms == 1709337660000 assert gaps[0].end_ms == 1709337780000 # 3 minutes of gap def test_multiple_gaps(self): """Test detects multiple separate gaps.""" candles = [ make_candle(1709337600), # 0:00 # Gap 1 make_candle(1709337720), # 0:02 # Gap 2 make_candle(1709337840), # 0:04 ] gaps = find_gaps(candles, "1m") assert len(gaps) == 2 def test_empty_candles(self): """Test empty candles returns no gaps (unless start/end provided).""" gaps = find_gaps([], "1m") assert len(gaps) == 0 def test_empty_with_range(self): """Test empty candles with range returns full range as gap.""" gaps = find_gaps( [], "1m", start_ms=1709337600000, end_ms=1709337720000, ) assert len(gaps) == 1 assert gaps[0].start_ms == 1709337600000 assert gaps[0].end_ms == 1709337720000 def test_gap_at_start(self): """Test detects gap at start of range.""" candles = [ make_candle(1709337660), # Missing first make_candle(1709337720), ] gaps = find_gaps( candles, "1m", start_ms=1709337600000, ) assert len(gaps) == 1 assert gaps[0].start_ms == 1709337600000 def test_gap_at_end(self): """Test detects gap at end of range.""" candles = [ make_candle(1709337600), make_candle(1709337660), # Missing at end ] gaps = find_gaps( candles, "1m", end_ms=1709337780000, ) assert len(gaps) == 1 assert gaps[0].start_ms == 1709337720000 def test_different_timeframes(self): """Test gap detection works with different timeframes.""" # 5m candles candles = [ make_candle(1709337600), # 0:00 # Missing: 0:05 make_candle(1709338200), # 0:10 ] gaps = find_gaps(candles, "5m") assert len(gaps) == 1 assert gaps[0].start_ms == 1709337900000 class TestCountMissingCandles: """Tests for count_missing_candles function.""" def test_no_missing(self): """Test count is 0 when no gaps.""" candles = [ make_candle(1709337600), make_candle(1709337660), make_candle(1709337720), ] count = count_missing_candles(candles, "1m") assert count == 0 def test_single_missing(self): """Test counts single missing candle.""" candles = [ make_candle(1709337600), make_candle(1709337720), # Gap of 1 ] count = count_missing_candles(candles, "1m") assert count == 1 def test_multiple_missing(self): """Test counts multiple missing candles.""" candles = [ make_candle(1709337600), make_candle(1709337840), # Gap of 3 ] count = count_missing_candles(candles, "1m") assert count == 3 class TestHasGaps: """Tests for has_gaps quick check.""" def test_no_gaps_returns_false(self): """Test returns False when no gaps.""" candles = [ make_candle(1709337600), make_candle(1709337660), make_candle(1709337720), ] assert has_gaps(candles, "1m") is False def test_with_gaps_returns_true(self): """Test returns True when gaps exist.""" candles = [ make_candle(1709337600), make_candle(1709337720), # Gap ] assert has_gaps(candles, "1m") is True def test_single_candle_returns_false(self): """Test single candle returns False (can't have gaps).""" candles = [make_candle(1709337600)] assert has_gaps(candles, "1m") is False class TestFillGaps: """Tests for fill_gaps function.""" def test_no_gaps_returns_same(self): """Test no-gap data returns unchanged.""" candles = [ make_candle(1709337600, close=50000), make_candle(1709337660, close=50100), make_candle(1709337720, close=50200), ] filled = fill_gaps(candles, "1m") assert len(filled) == 3 def test_fills_single_gap(self): """Test fills single missing candle.""" candles = [ make_candle(1709337600, close=50000), make_candle(1709337720, close=50200), # Gap at 1709337660 ] filled = fill_gaps(candles, "1m") assert len(filled) == 3 assert filled[1].time == 1709337660 # Forward-fill: uses next candle's values assert filled[1].close == 50200 def test_fills_multiple_gaps(self): """Test fills multiple missing candles.""" candles = [ make_candle(1709337600, close=50000), make_candle(1709337840, close=50400), # 3 missing ] filled = fill_gaps(candles, "1m") assert len(filled) == 5 # Original 2 + 3 filled assert filled[1].time == 1709337660 assert filled[2].time == 1709337720 assert filled[3].time == 1709337780 def test_copy_volume_true(self): """Test volume is copied when copy_volume=True.""" candles = [ Candle(time=1709337600, open=50000, high=50100, low=49900, close=50000, volume=100.0), Candle(time=1709337720, open=50200, high=50300, low=50100, close=50200, volume=200.0), ] filled = fill_gaps(candles, "1m", copy_volume=True) assert filled[1].volume == 200.0 # Copied from next def test_copy_volume_false(self): """Test volume is 0 when copy_volume=False.""" candles = [ Candle(time=1709337600, open=50000, high=50100, low=49900, close=50000, volume=100.0), Candle(time=1709337720, open=50200, high=50300, low=50100, close=50200, volume=200.0), ] filled = fill_gaps(candles, "1m", copy_volume=False) assert filled[1].volume == 0.0 def test_preserves_order(self): """Test result is sorted by time.""" candles = [ make_candle(1709337720), # Out of order make_candle(1709337600), make_candle(1709337840), ] filled = fill_gaps(candles, "1m") times = [c.time for c in filled] assert times == sorted(times) class TestFillGapsBackward: """Tests for backward-fill strategy.""" def test_backward_fill_uses_previous_close(self): """Test backward fill uses previous candle's close.""" candles = [ make_candle(1709337600, close=50000), make_candle(1709337720, close=50200), # Gap ] filled = fill_gaps_backward(candles, "1m") assert len(filled) == 3 # Backward fill: uses previous candle's close assert filled[1].close == 50000 class TestFillToRange: """Tests for fill_to_range function.""" def test_extends_to_start(self): """Test extends data backward to cover start.""" candles = [ make_candle(1709337660, close=50100), make_candle(1709337720, close=50200), ] filled = fill_to_range( candles, "1m", start_ms=1709337600000, end_ms=1709337720000, ) assert len(filled) == 3 assert filled[0].time == 1709337600 def test_extends_to_end(self): """Test extends data forward to cover end.""" candles = [ make_candle(1709337600, close=50000), make_candle(1709337660, close=50100), ] filled = fill_to_range( candles, "1m", start_ms=1709337600000, end_ms=1709337780000, ) assert len(filled) == 4 assert filled[-1].time == 1709337780