akg-factor-bridge/run.py

170 lines
7.9 KiB
Python
Raw Normal View History

2026-07-24 14:20:54 +08:00
"""akg-factor-bridge CLI。
python run.py views # 连通性自检:打印视图/表行数
2026-07-27 10:03:45 +08:00
python run.py apply-views [--dry-run] # 把插槽视图 DDL 应用到基座 PG
2026-07-27 09:49:05 +08:00
python run.py probe # G1 体检(只读,详见 probe.py
python run.py freeze [--date D] # 输入冻结G0.5,详见 freeze.py
2026-07-24 14:20:54 +08:00
python run.py register # 注册四子因子到 factor_metadata
python run.py build all --mode history --start 2024-01-01 --end 2025-12-31
python run.py build akg_heat --mode daily --date 2026-07-24
python run.py build akg_event --mode history --start 2025-01-01 --end 2026-07-24
2026-07-27 09:49:05 +08:00
daily 模式不给 --date 则取今天start=end=date跑完自动冻结--no-freeze 可关
history 模式 --start/--end默认不逐日冻结
2026-07-24 14:20:54 +08:00
所有写入幂等删涉及日期区间再插可安全重跑
"""
import argparse
import datetime as dt
2026-07-24 14:57:11 +08:00
import warnings
# pandas 用 DBAPI 连接读 SQL 会 warn无害功能正常——静音保持日志干净
warnings.filterwarnings("ignore", message=".*only supports SQLAlchemy.*")
2026-07-24 14:20:54 +08:00
import common
2026-07-27 09:49:05 +08:00
import config
2026-07-24 14:20:54 +08:00
import db
import factors
def cmd_views():
checks = [
("PG v_factor_universe", "pg", "SELECT count(*) FROM v_factor_universe"),
("PG v_factor_consensus", "pg", "SELECT count(*) FROM v_factor_consensus"),
("PG v_factor_events", "pg", "SELECT count(*) FROM v_factor_events"),
("PG v_factor_transmission", "pg", "SELECT count(*) FROM v_factor_transmission"),
("153 stock_fund_heat_scores","heat", "SELECT count(*) FROM stock_fund_heat_scores"),
("平台 gp_day_data", "price", "SELECT count(*) FROM gp_day_data"),
("平台 factor_metadata", "factor", "SELECT count(*) FROM factor_metadata"),
]
print("连通性自检:")
for name, src, sql in checks:
try:
df = db.read_pg(sql) if src == "pg" else db.read_mysql(src, sql)
print(f"{name}: {int(df.iloc[0, 0])}")
except Exception as e: # noqa: BLE001
print(f"{name}: {e!r}")
2026-07-27 09:49:05 +08:00
# 视图版本自检v2 才有的列在不在(评审第一批是否已应用)
print("\n视图版本2026-07-26 评审 v2")
for label, sql in (
("v_factor_transmission.n_sources",
"SELECT n_sources FROM v_factor_transmission LIMIT 1"),
("v_factor_transmission.mkt_trade_date",
"SELECT mkt_trade_date FROM v_factor_transmission LIMIT 1"),
("v_factor_events.source_type",
"SELECT source_type FROM v_factor_events LIMIT 1"),
("v_factor_segment_members第五插槽G2",
"SELECT 1 FROM v_factor_segment_members LIMIT 1")):
try:
db.read_pg(sql)
print(f"{label}")
except Exception: # noqa: BLE001
print(f"{label} —— 未就绪")
2026-07-24 14:57:11 +08:00
fronts = [
2026-07-24 15:20:29 +08:00
("热度 heat", "heat", "trade_date", "stock_fund_heat_scores"),
("一致预期 consensus", "pg", "asof_date", "v_factor_consensus"),
("事件 events", "pg", "disclosure_date", "v_factor_events"),
("传导 transmission", "pg", "scan_date", "v_factor_transmission"),
("行情 gp_day_data", "price", "`timestamp`", "gp_day_data"),
2026-07-24 14:57:11 +08:00
]
2026-07-24 15:20:29 +08:00
print("\n数据历史深度min ~ maxdistinct 天数——回填范围据此定):")
for name, src, col, tbl in fronts:
sql = f"SELECT MIN({col}), MAX({col}), COUNT(DISTINCT {col}) FROM {tbl}"
2026-07-24 14:57:11 +08:00
try:
df = db.read_pg(sql) if src == "pg" else db.read_mysql(src, sql)
2026-07-24 15:20:29 +08:00
lo, hi, n = df.iloc[0, 0], df.iloc[0, 1], df.iloc[0, 2]
print(f" {name}: {lo} ~ {hi} ({int(n)} 天)")
2026-07-24 14:57:11 +08:00
except Exception as e: # noqa: BLE001
print(f" {name}: ❌ {e!r}")
2026-07-27 09:49:05 +08:00
print(f"\n当前口径SUBFACTOR_UNIVERSE={config.SUBFACTOR_UNIVERSE} "
f"| EVENT_SOURCE_TYPES={sorted(config.EVENT_SOURCE_TYPES)} "
f"| EVENT_MAX_PER_DOC={config.EVENT_MAX_PER_DOC}")
2026-07-24 14:20:54 +08:00
_META = {
"akg_upside": ("astock-kg 预期空间", "分析师一致预期目标价隐含收益率(target_mid/price-1)"),
"akg_heat": ("astock-kg 热度", "生态日频资金热度分(0~1)"),
2026-07-27 09:49:05 +08:00
"akg_event": ("astock-kg 事件", "利好利空事件时间衰减加权分(仅公告来源,单文档封顶)"),
"akg_transmission": ("astock-kg 传导", "板块传导未动成员传导强度(distinct源数×(1-已动比例))"),
2026-07-24 14:20:54 +08:00
}
def cmd_register():
print("注册四子因子:")
for code, (name, desc) in _META.items():
common.register(code, name, factors.FACTORS[code], ["astock-kg", code.split("_", 1)[1]], desc)
2026-07-27 09:49:05 +08:00
def cmd_build(which, mode, start, end, date, do_freeze=True):
2026-07-24 14:20:54 +08:00
if mode == "daily":
d = date or dt.date.today().isoformat()
start = end = d
if not start or not end:
raise SystemExit("history 模式需要 --start 与 --end")
codes = list(factors.FACTORS) if which == "all" else [which]
2026-07-27 09:49:05 +08:00
frames = {}
2026-07-24 14:20:54 +08:00
for code in codes:
if code not in factors.BUILDERS:
raise SystemExit(f"未知因子: {code}(可选: {list(factors.FACTORS)} 或 all")
print(f"[{code}] {mode} {start} ~ {end}")
2026-07-24 15:07:31 +08:00
try:
df = factors.BUILDERS[code](start, end)
2026-07-27 09:49:05 +08:00
frames[f"factor_{code}"] = df
2026-07-24 15:07:31 +08:00
common.write_factor(factors.FACTORS[code], df, mode)
except Exception as e: # noqa: BLE001 —— 一路失败不拖累其余(批量容错)
print(f"{code} 失败: {e!r}")
2026-07-27 09:49:05 +08:00
# 日更顺手冻结:输入与输出落在同一目录,任何一行因子值都能被逐步复算。
# history 模式默认不冻结(逐日冻结应单独跑,避免一次回填写出几百个目录)。
if do_freeze and mode == "daily":
try:
import freeze
freeze.snapshot(start, extra_frames=frames)
except Exception as e: # noqa: BLE001 —— 冻结失败不该让因子构建算失败
print(f" ❌ 冻结失败(因子已落库): {e!r}")
2026-07-24 14:20:54 +08:00
def main():
ap = argparse.ArgumentParser(description="akg-factor-bridge")
sub = ap.add_subparsers(dest="cmd", required=True)
sub.add_parser("views")
sub.add_parser("register")
2026-07-27 09:49:05 +08:00
p = sub.add_parser("probe")
p.add_argument("--section", choices=["all", "pools", "price", "upside", "corr"],
default="all",
help="pools=池结构清单 price=行情年表 upside=分布与q档位 corr=三项相关矩阵")
2026-07-27 10:03:45 +08:00
av = sub.add_parser("apply-views")
av.add_argument("--file", default="sql/astock_kg_slot_views.sql")
av.add_argument("--dry-run", action="store_true", help="只列语句不执行")
2026-07-27 09:49:05 +08:00
f = sub.add_parser("freeze")
f.add_argument("--date", help="默认今天")
2026-07-24 14:20:54 +08:00
b = sub.add_parser("build")
b.add_argument("factor", help="akg_upside|akg_heat|akg_event|akg_transmission|all")
b.add_argument("--mode", choices=["daily", "history"], default="daily")
b.add_argument("--start")
b.add_argument("--end")
b.add_argument("--date")
2026-07-27 09:49:05 +08:00
b.add_argument("--no-freeze", action="store_true", help="daily 模式下跳过输入冻结")
2026-07-24 14:20:54 +08:00
a = ap.parse_args()
if a.cmd == "views":
cmd_views()
2026-07-27 09:49:05 +08:00
elif a.cmd == "probe":
import probe # 按需加载:一次性诊断命令,不影响常规链路
probe.run(a.section)
2026-07-27 10:03:45 +08:00
elif a.cmd == "apply-views":
import apply_views
raise SystemExit(1 if apply_views.apply(a.file, a.dry_run) else 0)
2026-07-27 09:49:05 +08:00
elif a.cmd == "freeze":
import freeze
freeze.snapshot(a.date)
2026-07-24 14:20:54 +08:00
elif a.cmd == "register":
cmd_register()
elif a.cmd == "build":
2026-07-27 09:49:05 +08:00
cmd_build(a.factor, a.mode, a.start, a.end, a.date, do_freeze=not a.no_freeze)
2026-07-24 14:20:54 +08:00
if __name__ == "__main__":
main()