feat(data): 静态表调度挪周末+vintage盘前自检——③sanguo-ak-quarter月内1号02:00改财报月首个周六02:00(/d SAT,周六02:00→周一09:30有55h跑道;旧排程7.5h夜窗连单表8h都装不下,09-01注册后首爆16h+穿整个交易日,09:32决策窗撞balance(94%Q2'26)/income(0%)vintage撕裂致value ROE归零清仓;月内1-4号读旧但一致,策略配对修复侧兼容)④新增static_vintage_check.py每日09:00自检:三报表最大报告期+覆盖率盘点(REPORT_DATE列裁剪扫描),跨表撕裂/估值断更(>48h)告警exit 3,原子落vintage_status.json供策略盘前预判(策略session协作契约);register加sanguo-ak-vintage+新wrapper;+8测试 [vps]
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""静态表 vintage 一致性自检测试 (2026-09-01 洗库撕裂事故防线④)。
|
||||
|
||||
核心契约: 跨表最大报告期不一致 → 告警 (退出码 3) + vintage_status.json
|
||||
落盘 (策略侧盘前预判数据源); 一致 → OK (退出码 0); 估值断更 → 告警。
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
|
||||
import pandas as pd
|
||||
|
||||
import static_vintage_check as svc # noqa: E402 (conftest 已加 sys.path)
|
||||
|
||||
|
||||
def _mk_pq(path, periods):
|
||||
df = pd.DataFrame({"REPORT_DATE": list(periods), "v": range(len(periods))})
|
||||
df.to_parquet(path, index=False)
|
||||
|
||||
|
||||
def _mk_table(root, name, stocks_periods):
|
||||
"""stocks_periods: {filename_stem: [报告期,...]}"""
|
||||
d = root / name
|
||||
d.mkdir(parents=True, exist_ok=True)
|
||||
for stem, periods in stocks_periods.items():
|
||||
_mk_pq(d / f"{stem}.parquet", periods)
|
||||
return d
|
||||
|
||||
|
||||
# ======================== 单表扫描 ========================
|
||||
|
||||
class TestScanStatementTable:
|
||||
|
||||
def test_basic_distribution(self, tmp_path):
|
||||
d = _mk_table(tmp_path, "balance", {
|
||||
"a": ["2025-12-31", "2026-06-30"],
|
||||
"b": ["2026-06-30"],
|
||||
"c": ["2026-03-31"], # 滞后股 (未披露中报)
|
||||
})
|
||||
(d / "empty.parquet").write_bytes(b"x" * 10) # <1KB 空文件
|
||||
s = svc.scan_statement_table(d)
|
||||
assert s["max_period"] == "2026-06-30"
|
||||
assert s["n_valid"] == 3 and s["n_at_max"] == 2
|
||||
assert s["n_empty"] == 1 and s["n_unreadable"] == 0
|
||||
assert s["coverage"] == round(2 / 3, 4) # coverage 保留 4 位小数
|
||||
|
||||
def test_datetime_dtype_period_normalized(self, tmp_path):
|
||||
"""真实数据 REPORT_DATE 形如 '2026-06-30 00:00:00' → 归一 YYYY-MM-DD。"""
|
||||
_mk_table(tmp_path, "income", {
|
||||
"a": ["2026-06-30 00:00:00"],
|
||||
})
|
||||
s = svc.scan_statement_table(tmp_path / "income")
|
||||
assert s["max_period"] == "2026-06-30"
|
||||
|
||||
def test_missing_dir(self, tmp_path):
|
||||
s = svc.scan_statement_table(tmp_path / "nope")
|
||||
assert "error" in s and s["max_period"] is None
|
||||
|
||||
|
||||
# ======================== 跨表一致性 / 告警 ========================
|
||||
|
||||
class TestCheckAll:
|
||||
|
||||
def test_vintage_tear_alert(self, tmp_path):
|
||||
"""09-01 事故签名: balance 已到 Q2'26, income 停在 Q1'26 → 撕裂告警。"""
|
||||
_mk_table(tmp_path, "balance", {"a": ["2026-06-30"]})
|
||||
_mk_table(tmp_path, "income", {"a": ["2026-03-31"]})
|
||||
_mk_table(tmp_path, "cashflow", {"a": ["2026-06-30"]})
|
||||
_, alerts = svc.check_all(tmp_path)
|
||||
assert any("撕裂" in a and "income=2026-03-31" in a for a in alerts)
|
||||
|
||||
def test_consistent_ok(self, tmp_path):
|
||||
_mk_table(tmp_path, "balance", {"a": ["2026-06-30"]})
|
||||
_mk_table(tmp_path, "income", {"a": ["2026-06-30"]})
|
||||
_mk_table(tmp_path, "cashflow", {"a": ["2026-06-30"]})
|
||||
_, alerts = svc.check_all(tmp_path)
|
||||
assert alerts == []
|
||||
|
||||
def test_valuation_stale_alert(self, tmp_path):
|
||||
"""估值表 (每日 ak-eod) 超 48h 未刷新 → 断更告警。"""
|
||||
_mk_table(tmp_path, "balance", {"a": ["2026-06-30"]})
|
||||
_mk_table(tmp_path, "income", {"a": ["2026-06-30"]})
|
||||
_mk_table(tmp_path, "cashflow", {"a": ["2026-06-30"]})
|
||||
d = _mk_table(tmp_path, "valuation", {"v": ["2026-08-31"]})
|
||||
old = time.time() - 100 * 3600
|
||||
os.utime(d / "v.parquet", (old, old))
|
||||
_, alerts = svc.check_all(tmp_path)
|
||||
assert any("估值表断更" in a for a in alerts)
|
||||
|
||||
|
||||
# ======================== main / JSON 契约 ========================
|
||||
|
||||
class TestMain:
|
||||
|
||||
def test_ok_exit0_and_json_written(self, tmp_path, monkeypatch, capsys):
|
||||
_mk_table(tmp_path, "balance", {"a": ["2026-06-30"]})
|
||||
_mk_table(tmp_path, "income", {"a": ["2026-06-30"]})
|
||||
_mk_table(tmp_path, "cashflow", {"a": ["2026-06-30"]})
|
||||
jp = tmp_path / "vintage_status.json"
|
||||
monkeypatch.setattr(
|
||||
"sys.argv", ["x", "--root", str(tmp_path), "--json-out", str(jp)])
|
||||
assert svc.main() == 0
|
||||
payload = json.loads(jp.read_text(encoding="utf-8"))
|
||||
assert set(payload["tables"]) == {"balance", "income", "cashflow"}
|
||||
assert payload["tables"]["balance"]["max_period"] == "2026-06-30"
|
||||
assert "OK" in capsys.readouterr().out
|
||||
# 幂等重跑 (原子覆盖)
|
||||
monkeypatch.setattr(
|
||||
"sys.argv", ["x", "--root", str(tmp_path), "--json-out", str(jp)])
|
||||
assert svc.main() == 0
|
||||
|
||||
def test_tear_exit3_with_alert_line(self, tmp_path, monkeypatch, capsys):
|
||||
_mk_table(tmp_path, "balance", {"a": ["2026-06-30"]})
|
||||
_mk_table(tmp_path, "income", {"a": ["2026-03-31"]})
|
||||
_mk_table(tmp_path, "cashflow", {"a": ["2026-03-31"]})
|
||||
jp = tmp_path / "v.json"
|
||||
monkeypatch.setattr(
|
||||
"sys.argv", ["x", "--root", str(tmp_path), "--json-out", str(jp)])
|
||||
assert svc.main() == 3
|
||||
assert "[vintage][ALERT]" in capsys.readouterr().out
|
||||
assert jp.exists() # 告警态也落 json (策略侧能看到撕裂详情)
|
||||
Reference in New Issue
Block a user