Lk / tests /test_data_layer.py
Kashaf1's picture
Upload moirai_forecast_app contents
ef20ebe
Raw
History Blame Contribute Delete
4.51 kB
"""
Covers the parts of data_layer.py that don't require network access:
validate_ohlcv (duplicate/invalid/NaN handling), LocalCache round-trip,
and data_freshness classification. fetch_ohlcv() itself needs a real
network call and is exercised only by the smoke-test script (expected
to fail here with DataSourceError -- see README).
"""
import numpy as np
import pandas as pd
from data_layer import LocalCache, data_freshness, validate_ohlcv
def test_validate_removes_duplicates_and_keeps_first():
idx = pd.to_datetime(["2024-01-01", "2024-01-01", "2024-01-02"], utc=True)
df = pd.DataFrame({
"open": [1, 999, 2], "high": [1, 999, 2], "low": [1, 999, 2],
"close": [1, 999, 2], "volume": [10, 10, 10],
}, index=idx)
out, report = validate_ohlcv(df)
assert report.duplicates_removed == 1
assert len(out) == 2
assert out.iloc[0]["close"] == 1 # first occurrence kept, not the duplicate
def test_validate_removes_invalid_ohlc_relationship():
idx = pd.date_range("2024-01-01", periods=3, freq="1h", tz="UTC")
df = pd.DataFrame({
"open": [10, 10, 10],
"high": [11, 9, 11], # row 1: high < open -> invalid
"low": [9, 9, 9],
"close": [10.5, 10.5, 10.5],
"volume": [5, 5, 5],
}, index=idx)
out, report = validate_ohlcv(df)
assert report.invalid_ohlc_removed == 1
assert len(out) == 2
def test_validate_removes_nan_rows_and_reports_them():
idx = pd.date_range("2024-01-01", periods=3, freq="1h", tz="UTC")
df = pd.DataFrame({
"open": [10, np.nan, 10], "high": [11, 11, 11], "low": [9, 9, 9],
"close": [10.5, 10.5, 10.5], "volume": [5, 5, 5],
}, index=idx)
out, report = validate_ohlcv(df)
assert report.nan_rows_removed == 1
assert len(out) == 2
def test_validate_marks_negative_volume_unavailable_not_invented():
idx = pd.date_range("2024-01-01", periods=2, freq="1h", tz="UTC")
df = pd.DataFrame({
"open": [10, 10], "high": [11, 11], "low": [9, 9],
"close": [10.5, 10.5], "volume": [5, -3],
}, index=idx)
out, _ = validate_ohlcv(df)
assert np.isnan(out.iloc[1]["volume"]), "negative volume must become NaN, never a guessed positive number"
def test_local_cache_round_trip(tmp_path_str="/tmp/_moirai_test_cache.sqlite3"):
import os
if os.path.exists(tmp_path_str):
os.remove(tmp_path_str)
cache = LocalCache(tmp_path_str)
idx = pd.date_range("2024-01-01", periods=3, freq="1h", tz="UTC")
df = pd.DataFrame({"open": [1, 2, 3], "high": [1, 2, 3], "low": [1, 2, 3],
"close": [1, 2, 3], "volume": [1, 2, 3]}, index=idx)
assert cache.get("EURUSD=X", "1h", "yfinance", "2024-01-01", "2024-01-02") is None
cache.set("EURUSD=X", "1h", "yfinance", "2024-01-01", "2024-01-02", df)
round_tripped = cache.get("EURUSD=X", "1h", "yfinance", "2024-01-01", "2024-01-02")
assert round_tripped is not None
assert len(round_tripped) == 3
assert list(round_tripped["close"]) == [1, 2, 3]
os.remove(tmp_path_str)
def test_data_freshness_classification():
now = pd.Timestamp.now(tz="UTC")
fresh = data_freshness(now - pd.Timedelta(minutes=1), "1h")
stale = data_freshness(now - pd.Timedelta(hours=10), "1h")
assert fresh["status"] == "fresh"
assert stale["status"] == "stale"
def test_resolve_history_window_days():
from data_layer import resolve_history_window
kind, value = resolve_history_window("1 day")
assert kind == "start"
kind, value = resolve_history_window("30 days")
assert kind == "start"
def test_resolve_history_window_months_uses_calendar_months():
from data_layer import resolve_history_window
from datetime import datetime, timezone
kind, value = resolve_history_window("3 months")
assert kind == "start"
now = datetime.now(timezone.utc)
delta_days = (now - value).days
# 3 calendar months is 89-92 days depending on which months are spanned --
# NOT exactly 90 (which a naive 30*3 approximation would assume).
assert 88 <= delta_days <= 93, f"expected ~3 calendar months, got {delta_days} days"
def test_resolve_history_window_max():
from data_layer import resolve_history_window
kind, value = resolve_history_window("max")
assert kind == "period"
assert value == "max"
def test_resolve_history_window_years():
from data_layer import resolve_history_window
kind, value = resolve_history_window("2 years")
assert kind == "start"