Files
FinDataHub/Scripts/bin/audit_schedule_coverage.py
FinDataHub 6456fcd299 fix(schedule): 补排期两张静默停摆 65 个交易日的事件表 + 新增排期覆盖审计
问题(用户报告)
----------------
`stk_holdertrade`(股东增减持)与 `repurchase`(股票回购)双双停在
`20260618`,而行情已到 `20260917` —— **停摆约 65 个交易日且无任何告警**。

根因
----
两张表在 `cli.py` 的 `_TYPE_TABLES` 任何分组里都不存在,也不在任何
`--tables` 列表里 —— **从未被任何调度任务调用过**。
`config.yaml` 里的 `tables:` 启用开关不救场:该开关只在
「未显式指定要抓哪些表」时才生效(`fetcher.py:239-243`),
而两个 launchd 调度脚本总是显式传 `--type` / `--tables`,开关被完全绕过。
于是「配置里写着 true」与「实际在不在跑」是两件事,且没有任何对账机制。

改动
----
1. `scripts/tushare_sync.sh` 补排期
   - daily:新增 `stk_holdertrade,repurchase`
   - monthly:新增 `fund_div,fund_manager,fund_share,fund_nav`
2. 新增 `Scripts/bin/audit_schedule_coverage.py`
   把三份来源交叉对账:TABLE_MAP 定义 / 调度实际覆盖 / config 声明启用。
   输出「启用但无任何排期」的真缺陷清单与「孤儿且未声明」的参考清单。
   —— 这类缺陷的特征就是**静默**,必须有机制主动对账。

补数结果(实测)
----------------
  stk_holdertrade  51,030 → 52,937 行,最新 20260618 → 20260918
  repurchase       43,584 → 45,901 行,最新 20260618 → 20260918
最近交易日分布正常(20260918 新增 11 条、20260917 新增 29 条)

审计复核
--------
  调度覆盖 55 → 61 张;「启用但无排期」真缺陷 12 → 7 张

仍未处理(本次已查明,需单独决策)
----------------------------------
剩余 7 张「启用但无排期」中,6 张**表根本不存在**,从未建过:
  hk_hold / shibor / stk_managers / stk_rewards / ths_daily / us_daily
(需先建表 DDL 再首次全量加载,属独立工作量)
另有 `stk_factor`:接口限 5 次/天,**无法批量抓取**,
`fetcher.py` 已显式跳过并打 WARNING —— 应从 config 里改为停用,避免长期假"启用"。
另有 44 张「孤儿且 config 未声明」的历史表,需人工确认是否应纳入采集。
2026-09-19 00:12:54 +08:00

119 lines
4.7 KiB
Python
Raw Permalink 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.
#!/usr/bin/env python3
"""audit_schedule_coverage.py — 排查「定义了但没有任何调度任务采集」的孤儿表
背景(2026-09-19)
------------------
用户报告 `stk_holdertrade` / `repurchase` 停摆约 65 个交易日。
排查结论:这两张表在 `_TYPE_TABLES` 的任何分组里都不存在,
也不在任何 `--tables` 列表里 —— **从未被任何调度任务调用过**。
`config.yaml` 里的 `tables:` 启用开关不救场:它只在
「未显式指定要抓哪些表」时才生效(`fetcher.py:239-243`),
而两个 launchd 调度脚本总是显式传 `--type` / `--tables`,开关被完全绕过。
于是「配置里写着 true」与「实际有没有在跑」是两件事,且没有任何机制对账。
本脚本就是那个对账机制:把
① TABLE_MAP 定义的表
② 调度脚本 + CLI 分组实际覆盖的表
③ config.yaml 声明启用的表
三者交叉比对,输出:
· 启用但无排期(真缺陷,会导致采集静默停摆)
· 定义的但 config 未声明(多半是历史遗留,需人工确认是否该采集)
用法
----
python3 Scripts/bin/audit_schedule_coverage.py
python3 Scripts/bin/audit_schedule_coverage.py --root /Applications/FinDataHub.app/Contents/Resources/tushare_fetcher
"""
from __future__ import annotations
import argparse
import re
import sys
from pathlib import Path
def parse_table_map_keys(fetcher_py: Path) -> set[str]:
src = fetcher_py.read_text(encoding="utf-8")
start = src.index("TABLE_MAP")
# TABLE_MAP 内的键都缩进 8 空格
return set(re.findall(r'^\s{8}"([a-z0-9_]+)":\s*\(', src[start:], re.M))
def parse_cli_groups(cli_py: Path) -> set[str]:
src = cli_py.read_text(encoding="utf-8")
covered: set[str] = set()
for _name, items in re.findall(r'"(\w+)":\s*\[([^\]]*)\]', src, re.S):
covered |= {x.strip().strip('"') for x in items.split(",") if x.strip()}
return covered
def parse_sync_tables(sync_sh: Path) -> set[str]:
if not sync_sh.exists():
return set()
src = sync_sh.read_text(encoding="utf-8")
covered: set[str] = set()
for m in re.findall(r"--tables\s+([A-Za-z0-9_,]+)", src):
covered |= {x.strip() for x in m.split(",") if x.strip()}
return covered
def parse_config_enabled(cfg_yaml: Path) -> tuple[set[str], set[str]]:
if not cfg_yaml.exists():
return set(), set()
src = cfg_yaml.read_text(encoding="utf-8")
if "\ntables:" not in src:
return set(), set()
sec = src[src.index("\ntables:"):]
enabled, disabled = set(), set()
for name, val in re.findall(r'^\s{2}([a-z0-9_]+):\s*(true|false)\s*$', sec, re.M):
(enabled if val == "true" else disabled).add(name)
return enabled, disabled
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--root", default="/Applications/FinDataHub.app/Contents/Resources/tushare_fetcher",
help="tushare_fetcher 根目录(默认取部署位)")
args = ap.parse_args()
root = Path(args.root)
if not (root / "src/tushare_fetcher/core/fetcher.py").exists():
print(f"✗ 未找到 {root}/src/tushare_fetcher/core/fetcher.py")
return 2
keys = parse_table_map_keys(root / "src/tushare_fetcher/core/fetcher.py")
covered = parse_cli_groups(root / "src/tushare_fetcher/cli.py")
covered |= parse_sync_tables(root / "scripts/tushare_sync.sh")
# 分红由独立脚本按报告期刷新,不在 --tables 里
covered.add("dividend")
enabled, disabled = parse_config_enabled(root / "config.yaml")
orphan = keys - covered
defect = sorted(orphan & enabled)
undeclared = sorted(orphan - enabled)
print(f"TABLE_MAP 定义的表 : {len(keys)}")
print(f"其中被调度任务覆盖 : {len(keys & covered)}")
print(f"config.yaml 启用 / 停用 : {len(enabled)} / {len(disabled)}")
print()
print(f"★ 真缺陷 — config 启用但无任何排期 : {len(defect)} 张")
if defect:
print(" 这些表声明为启用,却不在任何 --type / --tables 里 → 采集静默停摆。")
for i in range(0, len(defect), 4):
print(" " + " ".join(f"{t:<22}" for t in defect[i:i + 4]))
print()
print(f"(参考)孤儿且 config 未声明 : {len(undeclared)} 张")
print(" 多半是历史遗留或从未启用的表;需人工确认是否应纳入采集。")
for i in range(0, len(undeclared), 4):
print(" " + " ".join(f"{t:<22}" for t in undeclared[i:i + 4]))
print()
print("提示:`tables:` 开关只在未显式指定表时生效,因此**不要以 config 为准判断某项是否在跑**。")
return 1 if defect else 0
if __name__ == "__main__":
sys.exit(main())