300 lines
8.3 KiB
Python
300 lines
8.3 KiB
Python
"""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
|