akg-factor-bridge/score_lab.py

204 lines
9.2 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.

"""得分口径对比器score_lab纯只读——三套排序口径对未来收益用数据收口权重之争。
背景2026-08-17 用户拍板):选层撤掉主题限额之后,排序完全由总分决定,
"哪种加权更合理"不再靠感觉定。本工具对每个历史档位日,用三套口径各生成
一份主榜前 N 名单,对齐之后 5、10、20 个交易日的真实收益,输出逐日明细与汇总。
三套口径:
A 现行词典序 t_factor_akg_score 降序(先档后分已编码在分值里,强弱档按当日中位切)
B 绝对档界 同样先档后分,但强弱传导改为绝对判据:传导分 >= strong_min 记强档,
0 < 传导分 < strong_min 记弱档(组内分沿用 A 里已编码的那份,只重排档)
C 连续加权 不分档w1·z(log1p 传导) + w2·z(upside) + w3·z(−热度),仅主榜票参赛
基准主榜gate=2全体等权——选层不做任何排序时的底线跑不赢它的口径直接出局。
读数怎么看mean 是名单等权买入持有 k 个交易日的平均收益hit 是上涨占比,
excess 是相对基准的超额。样本告知:传导史 2026-07 起、每天只多一个样本点,
头一两个月只看方向、不下死结论。
跑法【桥机 factorevaluation · ~/akg-factor-bridge】
docker compose exec -T akg-factor-bridge python score_lab.py
docker compose exec -T akg-factor-bridge python score_lab.py --top 30 --horizons 5,10,20 \
--strong-min 1.0 --weights 0.5,0.3,0.2
产出data/score_lab/对比_<起>_<止>.csv逐日×口径×期限一行+ 终端汇总表。
纯只读:因子表与行情表全部 SELECT不写任何库产物只落容器内 data/ 目录。
"""
from __future__ import annotations
import argparse
import os
import numpy as np
import pandas as pd
import common
import db
import factors
def _factor_map(table: str, ds: str) -> pd.Series:
df = db.read_mysql("factor", f"SELECT stock_code, factor_value FROM {table} "
f"WHERE trade_date=%s", (ds,))
if df.empty:
return pd.Series(dtype=float)
return df.set_index("stock_code")["factor_value"].astype(float)
def _score_dates(start=None, end=None) -> list[str]:
df = db.read_mysql("factor", "SELECT DISTINCT trade_date FROM t_factor_akg_score "
"ORDER BY trade_date")
out = [pd.Timestamp(x).date().isoformat() for x in df["trade_date"]]
if start:
out = [d for d in out if d >= start]
if end:
out = [d for d in out if d <= end]
return out
def _price_panel(start: str, end_plus: str):
"""(交易日历, {(date, code): close})。行情取到 end 之后一段,好算前瞻收益。"""
px = factors._read_gp_price(start, end_plus) # noqa: SLF001 —— 同仓自用, 口径不分叉
if px.empty:
raise SystemExit("gp_day_data 在该区间没有行情——先确认行情库连通。")
px = px.dropna(subset=["close"])
px["k"] = px["ts_code"].map(lambda s: common.to_prefix(str(s).strip().upper()))
px["d"] = px["trade_date"].map(lambda x: pd.Timestamp(x).date().isoformat())
cal = sorted(px["d"].unique())
close = {(r.d, r.k): float(r.close) for r in px.itertuples()}
return cal, close
def _tier_from_score(sc: float) -> float:
"""A 口径分值里编码的档位200+档×20+组内分, 组内分夹 ±9.9)。"""
return float(max(0, min(2, int((sc - 190.0) // 20))))
def _rank_a(score: pd.Series) -> list[str]:
main = score[score >= 150.0].sort_values(ascending=False)
return list(main.index)
def _rank_b(score: pd.Series, trans: pd.Series, strong_min: float) -> list[str]:
"""绝对档界:组内分沿用 A从分值反解档位按传导分绝对阈值重记。"""
main = score[score >= 150.0]
if main.empty:
return []
rows = []
for k, sc in main.items():
inner = float(sc) - (200.0 + _tier_from_score(float(sc)) * 20.0)
st = float(trans.get(k) or 0.0)
tier = 2.0 if st >= strong_min else (1.0 if st > 0 else 0.0)
rows.append((k, 200.0 + tier * 20.0 + inner))
rows.sort(key=lambda x: -x[1])
return [k for k, _ in rows]
def _rank_c(score: pd.Series, trans: pd.Series, upside: pd.Series, heat: pd.Series,
w: tuple) -> list[str]:
"""连续加权:仅主榜票参赛;缺失处置同 build_score传导缺=0热度缺=中位数)。"""
main = score[score >= 150.0]
if main.empty:
return []
idx = main.index
t = np.log1p(pd.Series({k: float(trans.get(k) or 0.0) for k in idx}))
u = pd.Series({k: upside.get(k) for k in idx}, dtype=float)
h = pd.Series({k: heat.get(k) for k in idx}, dtype=float)
h = h.fillna(h.median())
u = u.fillna(0.0)
z = factors._robust_z # noqa: SLF001 —— 与生产打分同一把尺子
comp = w[0] * z(t) + w[1] * z(u) + w[2] * (-z(h))
return list(comp.sort_values(ascending=False).index)
def _fwd(cal: list, close: dict, d: str, codes: list, k: int):
"""名单在 d 买入、持有 k 个交易日的逐票收益(两头都要有价,缺价的票跳过)。"""
try:
i = cal.index(d)
except ValueError:
return []
if i + k >= len(cal):
return []
dt = cal[i + k]
out = []
for c in codes:
p0, p1 = close.get((d, c)), close.get((dt, c))
if p0 and p1 and p0 > 0:
out.append(p1 / p0 - 1.0)
return out
def main() -> int:
ap = argparse.ArgumentParser(description="得分口径对比器(只读)")
ap.add_argument("--top", type=int, default=30, help="每套口径取主榜前几只(默认 30")
ap.add_argument("--horizons", default="5,10,20", help="前瞻交易日数,逗号分隔")
ap.add_argument("--strong-min", type=float, default=1.0,
help="B 口径的强传导绝对阈值(传导分 = 源数×(1已动比例)")
ap.add_argument("--weights", default="0.5,0.3,0.2",
help="C 口径权重 传导,upside,−热度")
ap.add_argument("--start")
ap.add_argument("--end")
a = ap.parse_args()
horizons = sorted({int(x) for x in a.horizons.split(",") if x.strip()})
w = tuple(float(x) for x in a.weights.split(","))
if len(w) != 3:
raise SystemExit("--weights 需要三个数,如 0.5,0.3,0.2")
dates = _score_dates(a.start, a.end)
if not dates:
raise SystemExit("t_factor_akg_score 在该区间没有数据。")
end_plus = (pd.Timestamp(dates[-1]) + pd.Timedelta(days=int(max(horizons) * 2.2 + 14))
).date().isoformat()
print(f"档位日 {dates[0]} ~ {dates[-1]}{len(dates)} 天;"
f"前瞻 {horizons} 交易日;行情取到 {end_plus}")
cal, close = _price_panel(dates[0], end_plus)
rows = []
for ds in dates:
score = _factor_map("t_factor_akg_score", ds)
if score.empty:
continue
trans = _factor_map("t_factor_akg_transmission", ds)
upside = _factor_map("t_factor_akg_upside", ds)
hd = factors._heat_day_for(ds) # noqa: SLF001
heat = _factor_map("t_factor_akg_heat", hd) if hd else pd.Series(dtype=float)
base_codes = list(score[score >= 150.0].index)
lists = {"A现行": _rank_a(score)[:a.top],
"B绝对档界": _rank_b(score, trans, a.strong_min)[:a.top],
"C连续加权": _rank_c(score, trans, upside, heat, w)[:a.top],
"基准主榜等权": base_codes}
for name, codes in lists.items():
for k in horizons:
rets = _fwd(cal, close, ds, codes, k)
if not rets:
continue
rows.append({"date": ds, "scheme": name, "horizon": k,
"n": len(rets), "mean": float(np.mean(rets)),
"hit": float(np.mean([1.0 if r > 0 else 0.0 for r in rets])),
"median": float(np.median(rets))})
if not rows:
raise SystemExit("没有任何可评样本——多半是前瞻天数超出了已有行情(等几天再跑)。")
df = pd.DataFrame(rows)
os.makedirs("data/score_lab", exist_ok=True)
out = f"data/score_lab/对比_{dates[0]}_{dates[-1]}.csv"
df.to_csv(out, index=False, encoding="utf-8-sig")
print(f"\n—— 汇总逐日等权平均excess = 相对基准主榜等权)——")
print(f"{'口径':<10}{'期限':>4}{'样本日':>5}{'均值':>9}{'胜率':>7}{'超额':>9}")
agg = df.groupby(["scheme", "horizon"]).agg(days=("date", "nunique"),
mean=("mean", "mean"),
hit=("hit", "mean")).reset_index()
base = {(r.horizon): r.mean for r in agg[agg["scheme"] == "基准主榜等权"].itertuples()}
for r in agg.itertuples():
ex = r.mean - base.get(r.horizon, 0.0)
print(f"{r.scheme:<10}{r.horizon:>4}{r.days:>5}{r.mean:>9.2%}{r.hit:>7.1%}"
+ (f"{ex:>9.2%}" if r.scheme != "基准主榜等权" else f"{'':>9}"))
n_days = df["date"].nunique()
print(f"\n已写 {out}{len(df)} 行)。样本 {n_days}"
+ ("——样本很小, 只看方向、别下死结论。" if n_days < 40 else ""))
return 0
if __name__ == "__main__":
raise SystemExit(main())