From 3681c21a32aae32b6712f7b406b1c9d5fdeee404 Mon Sep 17 00:00:00 2001 From: zlt Date: Wed, 2 Sep 2026 17:39:38 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=8D=E7=9B=98=E6=B1=87=E6=80=BB=E5=8A=A0?= =?UTF-8?q?=E7=9B=B8=E5=AF=B9=E4=B8=BB=E6=A6=9C=E3=80=81=E7=9B=B8=E5=AF=B9?= =?UTF-8?q?=E5=90=8C=E5=B8=82=E5=80=BC=E6=A1=B6=E4=B8=A4=E5=88=97=E8=B6=85?= =?UTF-8?q?=E9=A2=9D=EF=BC=8C=E6=9C=80=E5=A4=A7=E5=8D=95=E6=97=A5=E6=A0=B7?= =?UTF-8?q?=E6=9C=AC=E5=8D=A0=E6=AF=94=E4=B8=8E=E6=A0=B7=E6=9C=AC=E7=BA=AA?= =?UTF-8?q?=E5=BE=8B=E4=B8=89=E6=A1=A3=E6=A0=87=E6=B3=A8=EF=BC=8C=E5=80=99?= =?UTF-8?q?=E9=80=89=E5=8D=95=E9=80=90=E6=97=A5=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- plan_review.py | 58 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 45 insertions(+), 13 deletions(-) diff --git a/plan_review.py b/plan_review.py index db7373d..c27c0b1 100644 --- a/plan_review.py +++ b/plan_review.py @@ -150,14 +150,24 @@ def _md(df: pd.DataFrame) -> str: return "\n".join(lines) -def summarize(ret: pd.Series, codes: list[str], base_all: float, base_main: float) -> dict | None: +def summarize(ret: pd.Series, codes: list[str], base_all: float, base_main: float, + caps: dict | None = None, cap_base: dict | None = None) -> dict | None: + """一份名单在一个计划日、一个期限上的读数。 + excess_all 相对全池等权;excess_main 相对主榜等权(同为有券商覆盖的篮子,剥掉大票对小票的贝塔); + excess_cap 相对同市值桶均值(每票减当日同桶全池均值再平均,方案 1.8c 的市值中性口径)。""" r = ret.reindex([c for c in codes if c in ret.index]).dropna() if r.empty: return None - return {"n": int(len(r)), "ret": round(float(r.mean()), 2), - "excess_all": round(float(r.mean() - base_all), 2), - "excess_main": round(float(r.mean() - base_main), 2) if base_main is not None else None, - "beat": round(float((r > base_all).mean() * 100), 1)} + out = {"n": int(len(r)), "ret": round(float(r.mean()), 2), + "excess_all": round(float(r.mean() - base_all), 2), + "excess_main": round(float(r.mean() - base_main), 2) if base_main is not None else None, + "excess_cap": None, + "beat": round(float((r > base_all).mean() * 100), 1)} + if caps and cap_base: + adj = [float(v) - cap_base[caps[c]] for c, v in r.items() if caps.get(c) in cap_base] + if adj: + out["excess_cap"] = round(sum(adj) / len(adj), 2) + return out # ============================================================================ @@ -208,6 +218,12 @@ def run(since: str, until: str | None, horizons: tuple, start: str, out_dir: str base_all = float(ret.dropna().mean()) main_ret = ret.reindex([c for c in main_codes if c in ret.index]).dropna() base_main = float(main_ret.mean()) if not main_ret.empty else None + cap_base: dict = {} # 当日三个市值桶各自的全池均值,供 excess_cap 用 + if caps: + for cap in ("小盘", "中盘", "大盘"): + v = ret.reindex([c for c, b in caps.items() if b == cap and c in ret.index]).dropna() + if not v.empty: + cap_base[cap] = float(v.mean()) regime_post = "涨周" if base_all > 0 else "跌周" regime_pre = (("弱势日" if reg.get("weak_day") else "非弱势日") if reg and reg.get("weak_day") is not None else "无标签") @@ -217,20 +233,20 @@ def run(since: str, until: str | None, horizons: tuple, start: str, out_dir: str "全池等权": list(ret.dropna().index), } for name, codes in lists.items(): - s = summarize(ret, codes, base_all, base_main) + s = summarize(ret, codes, base_all, base_main, caps, cap_base) if s: rows.append({"date": day, "h": h, "list": name, "group": "全部", "regime_post": regime_post, "regime_pre": regime_pre, **s}) # 分组:档位、判决、吸筹三态、市值 for tier in ("强传导", "弱传导", "无传导"): codes = [c for c, t in tier_of.items() if t == tier] - s = summarize(ret, codes, base_all, base_main) + s = summarize(ret, codes, base_all, base_main, caps, cap_base) if s: rows.append({"date": day, "h": h, "list": "主榜", "group": f"档位={tier}", "regime_post": regime_post, "regime_pre": regime_pre, **s}) for v in ("候选", "关注", "仅展示"): codes = [c for c, vv in verdict_of.items() if vv == v] - s = summarize(ret, codes, base_all, base_main) + s = summarize(ret, codes, base_all, base_main, caps, cap_base) if s: rows.append({"date": day, "h": h, "list": "档位表", "group": f"判决={v}", "regime_post": regime_post, "regime_pre": regime_pre, **s}) @@ -238,14 +254,14 @@ def run(since: str, until: str | None, horizons: tuple, start: str, out_dir: str ("潜在吸筹", lambda s: str(s).startswith("潜在")), ("其他", lambda s: not (str(s).startswith("明确") or str(s).startswith("潜在")))): codes = [c for c, s in acc.items() if s and pred(s)] - s = summarize(ret, codes, base_all, base_main) + s = summarize(ret, codes, base_all, base_main, caps, cap_base) if s: rows.append({"date": day, "h": h, "list": "全池", "group": f"吸筹={st_label}", "regime_post": regime_post, "regime_pre": regime_pre, **s}) if caps: for cap in ("小盘", "中盘", "大盘"): codes = [c for c in cands if caps.get(c) == cap] - s = summarize(ret, codes, base_all, base_main) + s = summarize(ret, codes, base_all, base_main, caps, cap_base) if s: rows.append({"date": day, "h": h, "list": "候选单", "group": f"市值={cap}", "regime_post": regime_post, "regime_pre": regime_pre, **s}) @@ -266,10 +282,17 @@ def run(since: str, until: str | None, horizons: tuple, start: str, out_dir: str # 带 _w 的三列按样本加权(大名单的日子权重大),与方案 1.8b/1.8c 手工读数同口径,只作对账。 n = g["n"].astype(float) w = n / n.sum() if n.sum() else n - return pd.Series({"days": g["date"].nunique(), "n": int(n.sum()), + days, total = g["date"].nunique(), int(n.sum()) + # 样本纪律(方案 2.4)机器标注:可读=计划日≥20 且样本≥100;方向=样本≥100;其余不作数 + grade = "可读" if (days >= 20 and total >= 100) else ("方向" if total >= 100 else "不作数") + return pd.Series({"days": days, "n": total, "ret": round(g["ret"].mean(), 2), "excess_all": round(g["excess_all"].mean(), 2), + "excess_main": round(g["excess_main"].mean(), 2) if g["excess_main"].notna().any() else None, + "excess_cap": round(g["excess_cap"].mean(), 2) if g["excess_cap"].notna().any() else None, "beat": round(g["beat"].mean(), 1), + "share": round(float(n.max() / n.sum() * 100), 0) if n.sum() else None, + "grade": grade, "ret_w": round(float((g["ret"] * w).sum()), 2), "excess_w": round(float((g["excess_all"] * w).sum()), 2), "beat_w": round(float((g["beat"] * w).sum()), 1)}) @@ -278,14 +301,23 @@ def run(since: str, until: str | None, horizons: tuple, start: str, out_dir: str regime_tbl = df[df["group"] == "全部"].groupby(["regime_post", "list", "h"]).apply(agg).reset_index() pre_tbl = df[(df["group"] == "全部") & (df["regime_pre"] != "无标签")] \ .groupby(["regime_pre", "list", "h"]).apply(agg).reset_index() + h0 = int(df["h"].min()) + daily_c = df[(df["list"] == "候选单") & (df["group"] == "全部") & (df["h"] == h0)] \ + [["date", "n", "ret", "excess_all", "excess_cap", "beat", "regime_post"]].sort_values("date") md = [f"# 候选单复盘 · {days_plan[0]} 至 {stamp}(起点价 {start})", "", f"计划日 {len(days_plan)} 期;样本纪律:分组样本少于一百只或覆盖计划日少于二十个只看方向;" f"口径切换的总样本少于六十个计划日只写方向。", "", - "## 一、六份名单 × 期限(超额=相对全池等权,命中=跑赢全池比例;" - "不带后缀的列按计划日等权,带 _w 的列按样本加权,后者只用于与方案第一节手工读数对账)", "", + "## 一、六份名单 × 期限", "", + "列的读法:excess_all 相对全池等权,excess_main 相对主榜等权(剥掉大票对小票的贝塔)," + "excess_cap 相对同市值桶(成交额除以换手率三分位,方案 1.8c 口径);beat 是跑赢全池比例," + "全池自己的 beat 只有四成多(分布右偏),读它时与全池等权那一行相减;share 是最大单日样本占比" + "(超过三成说明读数由一天主导,只看逐日表);grade 是样本纪律三档。" + "不带后缀的列按计划日等权(主口径),带 _w 的列按样本加权,只用于与方案第一节手工读数对账。", "", _md(lists_tbl), "", "## 二、分组读数", "", _md(groups_tbl), "", + f"## 二之二、候选单逐日(期限 {h0} 日;第一节按日等权的读数就是这张表的平均,看集中度)", "", + _md(daily_c), "", "## 三、按事后环境分组(未来 h 日全池涨跌,只作解释,不作交易前置)", "", _md(regime_tbl), ""] if not pre_tbl.empty: