akg-factor-bridge/run.py

170 lines
7.9 KiB
Python
Raw 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.

"""akg-factor-bridge CLI。
python run.py views # 连通性自检:打印视图/表行数
python run.py apply-views [--dry-run] # 把插槽视图 DDL 应用到基座 PG
python run.py probe # G1 体检(只读,详见 probe.py
python run.py freeze [--date D] # 输入冻结G0.5,详见 freeze.py
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
daily 模式:不给 --date 则取今天start=end=date跑完自动冻结--no-freeze 可关)。
history 模式:需 --start/--end默认不逐日冻结。
所有写入幂等(删涉及日期区间再插),可安全重跑。
"""
import argparse
import datetime as dt
import warnings
# pandas 用 DBAPI 连接读 SQL 会 warn无害功能正常——静音保持日志干净
warnings.filterwarnings("ignore", message=".*only supports SQLAlchemy.*")
import common
import config
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}")
# 视图版本自检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} —— 未就绪")
fronts = [
("热度 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"),
]
print("\n数据历史深度min ~ maxdistinct 天数——回填范围据此定):")
for name, src, col, tbl in fronts:
sql = f"SELECT MIN({col}), MAX({col}), COUNT(DISTINCT {col}) FROM {tbl}"
try:
df = db.read_pg(sql) if src == "pg" else db.read_mysql(src, sql)
lo, hi, n = df.iloc[0, 0], df.iloc[0, 1], df.iloc[0, 2]
print(f" {name}: {lo} ~ {hi} ({int(n)} 天)")
except Exception as e: # noqa: BLE001
print(f" {name}: ❌ {e!r}")
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}")
_META = {
"akg_upside": ("astock-kg 预期空间", "分析师一致预期目标价隐含收益率(target_mid/price-1)"),
"akg_heat": ("astock-kg 热度", "生态日频资金热度分(0~1)"),
"akg_event": ("astock-kg 事件", "利好利空事件时间衰减加权分(仅公告来源,单文档封顶)"),
"akg_transmission": ("astock-kg 传导", "板块传导未动成员传导强度(distinct源数×(1-已动比例))"),
}
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)
def cmd_build(which, mode, start, end, date, do_freeze=True):
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]
frames = {}
for code in codes:
if code not in factors.BUILDERS:
raise SystemExit(f"未知因子: {code}(可选: {list(factors.FACTORS)} 或 all")
print(f"[{code}] {mode} {start} ~ {end}")
try:
df = factors.BUILDERS[code](start, end)
frames[f"factor_{code}"] = df
common.write_factor(factors.FACTORS[code], df, mode)
except Exception as e: # noqa: BLE001 —— 一路失败不拖累其余(批量容错)
print(f"{code} 失败: {e!r}")
# 日更顺手冻结:输入与输出落在同一目录,任何一行因子值都能被逐步复算。
# 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}")
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")
p = sub.add_parser("probe")
p.add_argument("--section", choices=["all", "pools", "price", "upside", "corr"],
default="all",
help="pools=池结构清单 price=行情年表 upside=分布与q档位 corr=三项相关矩阵")
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="只列语句不执行")
f = sub.add_parser("freeze")
f.add_argument("--date", help="默认今天")
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")
b.add_argument("--no-freeze", action="store_true", help="daily 模式下跳过输入冻结")
a = ap.parse_args()
if a.cmd == "views":
cmd_views()
elif a.cmd == "probe":
import probe # 按需加载:一次性诊断命令,不影响常规链路
probe.run(a.section)
elif a.cmd == "apply-views":
import apply_views
raise SystemExit(1 if apply_views.apply(a.file, a.dry_run) else 0)
elif a.cmd == "freeze":
import freeze
freeze.snapshot(a.date)
elif a.cmd == "register":
cmd_register()
elif a.cmd == "build":
cmd_build(a.factor, a.mode, a.start, a.end, a.date, do_freeze=not a.no_freeze)
if __name__ == "__main__":
main()