复盘汇总加相对主榜、相对同市值桶两列超额,最大单日样本占比与样本纪律三档标注,候选单逐日表

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
zlt 2026-09-02 17:39:38 +08:00
parent 1d6ee160c8
commit 3681c21a32
1 changed files with 45 additions and 13 deletions

View File

@ -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: