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]
CI/CD / test (push) Successful in 3s
CI/CD / nas-deploy (push) Successful in 8s
CI/CD / nas-verify (push) Successful in 14s

This commit is contained in:
2026-09-01 19:03:53 +08:00
parent aa7e7b8eba
commit 8dc3f494b7
5 changed files with 328 additions and 4 deletions
@@ -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 (策略侧能看到撕裂详情)