"""得分口径对比器(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())