Files
sanguo_vnpy_v2/tests/data_platform/test_verify_increment.py
T
claude_dev e3b688354f fix(data): data_platform硬化(增量merge/verify+raw_redownload/run_daily_update)+测试
merge_increment/verify_increment 增量staging→验证→合并工具; raw_redownload/run_daily_update/import_vnpy_daily 强化; 补 data_platform 与 index_downloader 测试.
2026-07-15 07:12:46 +08:00

239 lines
9.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Tests for verify_increment.py — 安全闸门。"""
from __future__ import annotations
import os
import sys
import time
from datetime import datetime
import pandas as pd
import pytest
_HERE = os.path.dirname(os.path.abspath(__file__))
_SCRIPT_DIR = os.path.abspath(os.path.join(_HERE, "..", "..", "scripts", "data_platform"))
if _SCRIPT_DIR not in sys.path:
sys.path.insert(0, _SCRIPT_DIR)
import verify_increment as vi # noqa: E402
# ---------- helpers ----------
def _good_df(dates: list[str]) -> pd.DataFrame:
"""合法日线(通过 DataValidator 所有 fatal)。"""
n = len(dates)
return pd.DataFrame({
"date": pd.to_datetime(dates),
"open": [10.0 + i for i in range(n)],
"high": [10.5 + i for i in range(n)],
"low": [9.8 + i for i in range(n)],
"close": [10.2 + i for i in range(n)],
"volume": [10000 + i for i in range(n)],
})
def _write_staging(staging_root: str, year: str, fname: str, df: pd.DataFrame) -> None:
ydir = os.path.join(staging_root, year)
os.makedirs(ydir, exist_ok=True)
df.to_parquet(os.path.join(ydir, fname), index=False)
def _recent_dates(n: int = 3) -> list[str]:
"""最近 n 个工作日(保证 fresh,含今天/最近交易日)。"""
today = pd.Timestamp(time.strftime("%Y-%m-%d"))
dates = pd.bdate_range(end=today, periods=n).strftime("%Y-%m-%d").tolist()
return dates
# ---------- symbol_from_filename ----------
def test_symbol_from_filename():
assert vi.symbol_from_filename("sh600000_daily.parquet") == "600000"
assert vi.symbol_from_filename("sz000001_daily.parquet") == "000001"
assert vi.symbol_from_filename("bj920000_daily.parquet") == "920000"
# ---------- all good → passed ----------
def test_verify_all_good_passes(tmp_path):
"""staging 全合法且 fresh → passed=True。"""
staging = tmp_path / "staging"
dates = _recent_dates(3)
# 5 只正常 + 1 只北交所(应被扣分母,不影响通过)
for sym in ("sh600000", "sh600004", "sz000001", "sz300001", "sh688001"):
_write_staging(str(staging), "2026", f"{sym}_daily.parquet", _good_df(dates))
_write_staging(str(staging), "2026", "bj920000_daily.parquet", _good_df(dates))
start = dates[0]
result = vi.verify(str(staging), start)
assert result["passed"] is True
assert result["total"] == 6
assert result["unsupported_skipped"] == 1 # 北交所 920
assert result["success"] == 5
assert result["success_rate"] == 1.0
assert result["fresh_rate"] == 1.0
assert result["failed_symbols"] == []
# ---------- fatal cases → failed ----------
def test_verify_empty_file_fails(tmp_path):
"""空 dfDataValidator 直接判 fatal '数据为空')→ 该 symbol 失败。"""
staging = tmp_path / "staging"
dates = _recent_dates(3)
# 4 只好 + 1 只空
for sym in ("sh600000", "sh600004", "sz000001", "sz300001"):
_write_staging(str(staging), "2026", f"{sym}_daily.parquet", _good_df(dates))
_write_staging(str(staging), "2026", "sh688001_daily.parquet", pd.DataFrame(
{"date": [], "open": [], "high": [], "low": [], "close": [], "volume": []}
))
result = vi.verify(str(staging), dates[0])
# 4 好 / 5 总 = 0.8 < 0.95 → fail
assert result["passed"] is False
assert result["success"] == 4
assert result["total"] == 5
assert "688001" in result["failed_symbols"]
assert result["success_rate"] < vi.MIN_SUCCESS_RATE
# fatal 样本里有 688001
assert any(s["symbol"] == "688001" for s in result["fatal_samples"])
def test_verify_zero_price_fails(tmp_path):
"""价格<=0D1 fatal)→ 该 symbol 失败。"""
staging = tmp_path / "staging"
dates = _recent_dates(3)
for sym in ("sh600000", "sh600004", "sz000001"):
_write_staging(str(staging), "2026", f"{sym}_daily.parquet", _good_df(dates))
# 构造 close<=0 的坏 df
bad = pd.DataFrame({
"date": pd.to_datetime(dates),
"open": [0.0, 0.0, 0.0], "high": [0.0, 0.0, 0.0],
"low": [0.0, 0.0, 0.0], "close": [0.0, 0.0, 0.0],
"volume": [100, 200, 300],
})
_write_staging(str(staging), "2026", "sz300001_daily.parquet", bad)
result = vi.verify(str(staging), dates[0])
# 3 好 / 4 总 = 0.75 < 0.95 → fail
assert result["passed"] is False
assert "300001" in result["failed_symbols"]
# 样本错误里有 D1
sample = next(s for s in result["fatal_samples"] if s["symbol"] == "300001")
assert any("D1" in e for e in sample["errors"])
def test_verify_bse_excluded_from_denominator(tmp_path):
"""北交所码(920/921/83/87)从分母扣——不算失败也不算成功。"""
staging = tmp_path / "staging"
dates = _recent_dates(3)
# 3 只全北交所 → denom=0 → passed=Falsedenom=0 算不通过,因为没有有效样本可验)
for sym in ("bj920000", "bj920001", "sz830001", "sz870001"):
_write_staging(str(staging), "2026", f"{sym}_daily.parquet", _good_df(dates))
result = vi.verify(str(staging), dates[0])
assert result["unsupported_skipped"] == 4
assert result["denom"] == 0
assert result["passed"] is False # denom=0 → 不通过
def test_verify_missing_year_dir_raises(tmp_path):
"""staging 的 year 目录不存在 → FileNotFoundError。"""
staging = tmp_path / "staging"
staging.mkdir()
with pytest.raises(FileNotFoundError):
vi.verify(str(staging), "2099-01-01")
# ---------- _latest_available_trading_day(收盘感知) ----------
# 2026-07-13=Mon, 07-14=Tue, 07-10=Fri, 07-11=Sat, 07-12=Sun
def test_latest_available_premarket_weekday():
"""工作日盘前(<15:00)→ 上一交易日。"""
now = datetime(2026, 7, 14, 4, 12) # 周二 04:12
assert vi._latest_available_trading_day(now) == "2026-07-13"
def test_latest_available_after_market_weekday():
"""工作日盘后(>=15:00)→ 今天。"""
now = datetime(2026, 7, 14, 16, 0) # 周二 16:00
assert vi._latest_available_trading_day(now) == "2026-07-14"
def test_latest_available_at_15_exact():
"""15:00 整点算盘后(>= 15:00 → 今天)。"""
now = datetime(2026, 7, 14, 15, 0) # 周二 15:00
assert vi._latest_available_trading_day(now) == "2026-07-14"
def test_latest_available_weekend():
"""周末 → 上周五。"""
assert vi._latest_available_trading_day(datetime(2026, 7, 11, 10, 0)) == "2026-07-10" # Sat
assert vi._latest_available_trading_day(datetime(2026, 7, 12, 20, 0)) == "2026-07-10" # Sun
def test_latest_available_monday_premarket():
"""周一盘前 → 上周五(回退周末)。"""
now = datetime(2026, 7, 13, 4, 0) # 周一 04:00
assert vi._latest_available_trading_day(now) == "2026-07-10"
def test_latest_available_default_now():
"""不传 now → 返回字符串(不抛异常)。"""
result = vi._latest_available_trading_day()
assert isinstance(result, str)
assert len(result) == 10 # YYYY-MM-DD
# ---------- verify 收盘感知场景(跨夜 catch-up 核心 bug ----------
def test_verify_premarket_overnight_passes(tmp_path):
"""用例1 跨夜/盘前:now=周二 04:12, staging max=周一 07-13 → 目标=07-13 → fresh → passed."""
staging = tmp_path / "staging"
dates = ["2026-07-09", "2026-07-10", "2026-07-13"] # max=周一
for sym in ("sh600000", "sh600004", "sz000001", "sz300001", "sh688001"):
_write_staging(str(staging), "2026", f"{sym}_daily.parquet", _good_df(dates))
_write_staging(str(staging), "2026", "bj920000_daily.parquet", _good_df(dates))
now = datetime(2026, 7, 14, 4, 12) # 周二 04:12 盘前
result = vi.verify(str(staging), "2026-07-06", _now=now)
assert result["latest_trading_day"] == "2026-07-13"
assert result["fresh_rate"] == 1.0
assert result["passed"] is True
def test_verify_after_market_fails_if_stale(tmp_path):
"""用例2 盘后:now=周二 16:00, staging max=周一 07-13 → 目标=07-14 → fresh_rate=0 → fail."""
staging = tmp_path / "staging"
dates = ["2026-07-09", "2026-07-10", "2026-07-13"] # max=周一
for sym in ("sh600000", "sh600004", "sz000001", "sz300001", "sh688001"):
_write_staging(str(staging), "2026", f"{sym}_daily.parquet", _good_df(dates))
_write_staging(str(staging), "2026", "bj920000_daily.parquet", _good_df(dates))
now = datetime(2026, 7, 14, 16, 0) # 周二 16:00 盘后
result = vi.verify(str(staging), "2026-07-06", _now=now)
assert result["latest_trading_day"] == "2026-07-14"
assert result["fresh_rate"] == 0.0
assert result["passed"] is False
def test_verify_weekend_passes(tmp_path):
"""用例3 周末:now=周六, staging max=周五 07-10 → 目标=07-10 → fresh → passed."""
staging = tmp_path / "staging"
dates = ["2026-07-08", "2026-07-09", "2026-07-10"] # max=周五
for sym in ("sh600000", "sh600004", "sz000001", "sz300001", "sh688001"):
_write_staging(str(staging), "2026", f"{sym}_daily.parquet", _good_df(dates))
_write_staging(str(staging), "2026", "bj920000_daily.parquet", _good_df(dates))
now = datetime(2026, 7, 11, 10, 0) # 周六 10:00
result = vi.verify(str(staging), "2026-07-06", _now=now)
assert result["latest_trading_day"] == "2026-07-10"
assert result["fresh_rate"] == 1.0
assert result["passed"] is True