feat(data): 移植 v1 validator + 适配接口 + 测试

This commit is contained in:
2026-07-05 11:53:51 +08:00
parent 3422372948
commit 56fbde6492
3 changed files with 158 additions and 0 deletions
+132
View File
@@ -0,0 +1,132 @@
#!/usr/bin/env python3
"""数据校验层 - V1 7条fatal规则"""
import pandas as pd
from datetime import datetime
from typing import List, Tuple
class ValidationResult:
def __init__(self):
self.passed = True
self.fatal_errors: List[str] = []
self.warnings: List[str] = []
self.checked_rows = 0
self.failed_rows = 0
def __repr__(self):
return (f"ValidationResult(passed={self.passed}, "
f"fatal={len(self.fatal_errors)}, warnings={len(self.warnings)}, "
f"rows={self.checked_rows}, failed={self.failed_rows})")
def to_dict(self):
return {
"passed": self.passed,
"fatal_errors": self.fatal_errors,
"warnings": self.warnings,
"checked_rows": self.checked_rows,
"failed_rows": self.failed_rows,
}
class DataValidator:
"""数据校验器 - V1 7条fatal规则"""
def validate(self, df: pd.DataFrame, data_type: str = "daily") -> ValidationResult:
result = ValidationResult()
if df is None or df.empty:
result.fatal_errors.append("数据为空")
result.passed = False
return result
result.checked_rows = len(df)
if data_type == "daily":
self._validate_daily(df, result)
elif data_type == "realtime":
self._validate_realtime(df, result)
return result
def validate_realtime_dict(self, data: dict) -> ValidationResult:
"""校验单条实时行情"""
result = ValidationResult()
result.checked_rows = 1
errors = []
# R1: 价格>0
if not data or data.get("current", 0) <= 0:
errors.append("R1: current价格<=0")
if data.get("prev_close", 0) <= 0:
errors.append("R1: prev_close<=0")
# R7: 必须携带source和fetched_at
if not data.get("source"):
errors.append("R7: 缺少source字段")
if not data.get("fetched_at"):
errors.append("R7: 缺少fetched_at字段")
if errors:
result.fatal_errors = errors
result.passed = False
result.failed_rows = 1
return result
def _validate_daily(self, df: pd.DataFrame, result: ValidationResult):
today = datetime.now().strftime("%Y-%m-%d")
for idx, row in df.iterrows():
row_errors = []
# D1: 价格>0
for col in ["close", "open", "high", "low"]:
val = row.get(col, 0)
if pd.isna(val) or float(val) <= 0:
row_errors.append(f"D1: {col}<=0 (row {idx})")
break
# D2: OHLC一致性
o, h, l, c = float(row.get("open", 0)), float(row.get("high", 0)), \
float(row.get("low", 0)), float(row.get("close", 0))
if o > 0 and c > 0:
if h < max(o, c) or l > min(o, c):
row_errors.append(f"D2: OHLC不一致 (row {idx}, o={o} h={h} l={l} c={c})")
# D3: volume >= 0
vol = row.get("volume", 0)
if pd.notna(vol) and float(vol) < 0:
row_errors.append(f"D3: volume<0 (row {idx})")
# D7: 非未来日期
dt = str(row.get("date", row.get("datetime", "")))[:10]
if dt > today:
row_errors.append(f"D7: 未来日期 {dt} (row {idx})")
if row_errors:
result.fatal_errors.extend(row_errors)
result.failed_rows += 1
# D6: 日期不重复 (check after all rows)
date_col = "date" if "date" in df.columns else "datetime"
if date_col in df.columns:
dupes = df[df.duplicated(subset=[date_col], keep=False)]
if not dupes.empty and len(df) > 1:
result.fatal_errors.append(f"D6: {len(dupes)}条重复日期")
if result.fatal_errors:
result.passed = False
# 适配层,不改 v1 校验逻辑
def validate_daily(df):
"""对外统一接口,委托 v1 校验规则
Args:
df: 输入日线DataFrame
Returns:
过滤后的DataFrame(仅包含通过校验的行)
"""
validator = DataValidator()
valid_rows = []
for idx in range(len(df)):
row_df = df.iloc[idx:idx+1]
result = validator.validate(row_df, data_type="daily")
if result.passed:
valid_rows.append(idx)
return df.iloc[valid_rows]
+19
View File
@@ -0,0 +1,19 @@
import pandas as pd
import pytest
@pytest.fixture
def good_daily_df():
return pd.DataFrame({
"date": ["2026-01-01", "2026-01-02"],
"open": [10.0, 11.0], "high": [10.5, 11.5],
"low": [9.8, 10.8], "close": [10.2, 11.2],
"volume": [10000, 12000],
})
@pytest.fixture
def bad_daily_df():
return pd.DataFrame({
"date": ["2026-01-01"],
"open": [0.0], "high": [0.0], "low": [0.0], "close": [0.0],
"volume": [100],
})
+7
View File
@@ -0,0 +1,7 @@
from sanguo_data.validator import validate_daily
def test_validate_daily_keeps_good_rows(good_daily_df):
assert len(validate_daily(good_daily_df)) == 2
def test_validate_daily_drops_zero_price(bad_daily_df):
assert len(validate_daily(bad_daily_df)) == 0