文案第四批:计划的环境标签与周报的表头、导读、口径栏
环境标签那行原先印出「环境标签:UNKNOWN,弱势指数 None/8」。三处毛病:UNKNOWN 是 枚举值;8 是硬写的分母,而快照里其实带着真实的指数总数;None 是取不到时的空值。 改成三种情况分开写,并补上一句此前没说的要紧话——这个标签不影响任何一只票买不买, 只在事后复盘时用来分组。不写的话,看到「弱势日」会以为下面的名单被压过一轮。 周报整张表的表头都是英文字段名。翻译放在渲染函数里,每张表都受益。有三处不能想当然 地译:比全池多涨几个点与跑赢全池的占比都跟全池比,但一个是收益差、一个是只数比例, 译成同一个说法会在同一张表里紧挨着撞车;往后看几个交易日不能译成持有天数,脚本开头 反复写明不建仓、不计成本、不是回测。 导读段落原先按英文列名写,表头一改就指向不存在的列,而且里面有一处事实错误:写着 样本纪律三档,代码里只有两档。一并重写并改正。 自评与账本那两栏的口径说明里有库表名与枚举值。改成说清算法本身,特别是"为什么这个 数不能和上一行直接比大小"——那正是并列这两行时最容易读错的地方。 两处会把数据库异常原文印进周报的,改成只进运行日志。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
7651dbc815
commit
0bf29287a1
|
|
@ -252,5 +252,5 @@
|
|||
- 顺带接上一个此前从没被渲染过的字段:候选卡的缺失项一路从选股系统带到 PMS 前端,页面上一次都没显示过。现在"关注"态终于说得出到底缺什么。
|
||||
- **发现一个环境问题,与本批改动无关但影响核验可信度**:`docker compose exec pms-web` 打到的不是服务容器,而是一个 2026-08-03 由 `docker compose run` 留下的孤儿容器(容器名 tradingsystem-pms-web-run-a4f4a87e7662),跑了四周、状态 unhealthy、用的是四周前的镜像。它跑的是 scripts/watch.py,不接端口、不影响生产,但会让 exec 做的核验落在旧代码上。
|
||||
- 识别办法:`docker compose ps -q pms-web` 报的容器 id 与 `docker compose exec -T pms-web hostname` 不一致就是撞上了。核验要紧的读数时用 `docker exec <真实容器 id>` 直接指名。
|
||||
- 处置:清掉那个孤儿容器要经用户同意,本次未动。
|
||||
- 复核日期:清理之后确认 exec 与 ps 报的是同一个容器。
|
||||
- 处置:09-04 经用户同意后已清除。清除前逐项确认过四条:compose 自己标着 oneoff=True(一次性命令留下的,不是编排里的服务);脚本开头写明只读不写,实际扫代码零写库零写文件;不接任何端口、没有别的容器把它当依赖;跑了四周累计占用 7 小时 23 分处理器时间做没人看的事。
|
||||
- 清除后复验通过:exec 与 ps 报的是同一个容器 ac9b9289;四个服务容器都在、通道在线心跳 0.9 秒;那个"依赖装不上"的矛盾消失。
|
||||
|
|
|
|||
26
plan.py
26
plan.py
|
|
@ -112,6 +112,29 @@ def _val(series: pd.Series, k: str):
|
|||
return None if v is None or pd.isna(v) else float(v)
|
||||
|
||||
|
||||
def _fmt_regime(reg) -> str:
|
||||
"""市场环境那一行。三种情况分开写,因为读的人要做的事不一样。
|
||||
|
||||
原先写「环境标签:UNKNOWN,弱势指数 None/8」。三处毛病:UNKNOWN 是枚举值,
|
||||
8 是硬写的分母(快照里其实带着真实的指数总数),None 是取不到时的空值——
|
||||
三样都是内部表示,不该印在给人读的报告里。
|
||||
|
||||
另外必须写清一件事:这个标签**不影响任何一只票买不买**。它只在事后复盘时用来
|
||||
分组。不写的话,读的人看到「弱势日」会以为下面的名单被压过一轮。
|
||||
"""
|
||||
tail = "这个标签只在事后复盘时用来分组,不参与今天选股,也不影响任何一只票买不买。"
|
||||
if (reg or {}).get("status") != "OK" or reg.get("weak_count") is None:
|
||||
return ("市场环境:今天没拿到读数(择时决策系统的环境数据当天没送过来)。"
|
||||
"这里留空不代表市场好或坏,下面的名单照常,不受影响。")
|
||||
n = reg["weak_count"]
|
||||
total = reg.get("weak_total")
|
||||
line = config.REGIME_WEAK_COUNT
|
||||
scope = f"{total} 个主参考指数里有 {n} 个走弱" if total else f"有 {n} 个主参考指数走弱"
|
||||
if reg.get("weak_day"):
|
||||
return f"市场环境:{scope}。走弱满 {line} 个就算弱势日,今天算。{tail}"
|
||||
return f"市场环境:{scope},没到 {line} 个,今天不算弱势日。{tail}"
|
||||
|
||||
|
||||
def _state_out(st) -> dict | None:
|
||||
"""逻辑状态发给下游的形状:只留人要读的和判决要用的,不发内部中间量。
|
||||
|
||||
|
|
@ -630,8 +653,7 @@ def render_md(d: dict) -> str:
|
|||
L.append("")
|
||||
reg = d.get("regime")
|
||||
if reg:
|
||||
L.append(f"环境标签:{reg.get('status')},弱势指数 {reg.get('weak_count')}/8"
|
||||
f"{',弱势日' if reg.get('weak_day') else ''}(只展示与复盘分组,不作交易前置)。")
|
||||
L.append(_fmt_regime(reg))
|
||||
L.append("")
|
||||
mk = d.get("market")
|
||||
if mk:
|
||||
|
|
|
|||
|
|
@ -125,7 +125,9 @@ def pms_roster(day: str) -> tuple[list[str], str]:
|
|||
n_df = db.read_mysql(
|
||||
"pms", "SELECT param_value FROM pms_runtime_param WHERE param_key='PMS_PLAN_TOP_N'")
|
||||
except Exception as e: # noqa: BLE001
|
||||
return [], f"PMS 快照读取失败({e!r}),生产名单退为桥重算"
|
||||
# 异常原文会被印进周报的逐日注记。只进日志,不进报告。
|
||||
print(f" (PMS 计划快照读取失败: {e!r})")
|
||||
return [], "当天没能连上 PMS 数据库,取不到计划快照"
|
||||
if df.empty:
|
||||
return [], "PMS 无当日快照,生产名单退为桥重算"
|
||||
n = int(n_df.iloc[0, 0]) if not n_df.empty else 30
|
||||
|
|
@ -196,10 +198,15 @@ def timing_self_eval(since: str, until: str | None) -> str:
|
|||
n = int(df.iloc[0]["n"] or 0) if not df.empty else 0
|
||||
hits = int(df.iloc[0]["hits"] or 0) if not df.empty else 0
|
||||
except Exception as e: # noqa: BLE001
|
||||
return f"未接入(decision_outcome 读取失败:{type(e).__name__})"
|
||||
# 表名是 PMS 的库表名,不该出现在周报的读数列里。原因写进运行日志。
|
||||
print(f" (择时决策系统的打分表读取失败: {e!r})")
|
||||
return (f"读不到择时决策系统的打分数据,"
|
||||
f"{since} 至 {until or '今日'} 这段的五日方向命中率这期算不出来")
|
||||
if n == 0:
|
||||
return "未接入(区间内无已判分的日终策略行)"
|
||||
return f"五日方向命中 {hits}/{n} = {hits / n * 100:.0f}%(基准日 {since} 至 {until or '今日'})"
|
||||
# 与"读不到"分开写:这两种情况在表里要一眼看得出差别。
|
||||
return "择时决策系统这段时间没有打过分的日终策略记录,这期没有命中率"
|
||||
return (f"看 5 个交易日后涨跌方向判断对不对,{n} 条里对了 {hits} 条,"
|
||||
f"命中率 {hits / n * 100:.0f}%({since} 至 {until or '今日'})")
|
||||
|
||||
|
||||
def decision_ledger_entries(path: str = DECISION_LEDGER_PATH) -> list[dict]:
|
||||
|
|
@ -230,11 +237,40 @@ def forward(pivot: pd.DataFrame, days: list[str], day: str, h: int, start: str)
|
|||
return block.sum(min_count=h)
|
||||
|
||||
|
||||
# 表头的中文说法。周报是给人读的,列名不该是代码里的字段名。
|
||||
# 有几处必须区分开,不能想当然地译:
|
||||
# excess_all 与 beat 都跟"全池"比,但一个是收益差、一个是只数比例,
|
||||
# 译成同一个说法会在同一张表里紧挨着撞车;
|
||||
# h 不能译成"持有天数"——这个脚本开头反复写明不建仓、不计成本、不是回测,
|
||||
# 它只是"往后看几个交易日";
|
||||
# grade 只有两档(可读、方向),代码在 :387,导读段落此前写着"三档"是错的。
|
||||
_COL_CN = {
|
||||
"days": "一共几个计划日", "n": "一共几只股票", "ret": "名单平均涨跌%",
|
||||
"excess_all": "比全池多涨几个点", "excess_main": "比主榜多涨几个点",
|
||||
"excess_cap": "比同市值股票多涨几个点",
|
||||
"beat": "跑赢全池的股票占比%", "share": "最多的一天占了多少%",
|
||||
"grade": "样本够不够看", "h": "往后看几个交易日",
|
||||
"list": "名单", "group": "分组", "date": "计划日",
|
||||
"regime_post": "这几天全池是涨是跌", "regime_pre": "下单当天是不是弱势日",
|
||||
}
|
||||
|
||||
|
||||
def _col_cn(c) -> str:
|
||||
"""列名翻成中文。带 _w 后缀的是同一个读数的样本加权版本,只用于对账。"""
|
||||
name = str(c)
|
||||
if name.endswith("_w"):
|
||||
base = _COL_CN.get(name[:-2])
|
||||
return f"股票多的日子算得重·{base}" if base else name
|
||||
return _COL_CN.get(name, name)
|
||||
|
||||
|
||||
def _md(df: pd.DataFrame) -> str:
|
||||
"""自己拼 Markdown 表:桥镜像没装 tabulate,pandas.to_markdown 用不了。"""
|
||||
"""自己拼 Markdown 表:桥镜像没装 tabulate,pandas.to_markdown 用不了。
|
||||
|
||||
表头统一在这里翻成中文,每张表都受益,不用逐处改。"""
|
||||
if df is None or df.empty:
|
||||
return "(无数据)"
|
||||
cols = [str(c) for c in df.columns]
|
||||
cols = [_col_cn(c) for c in df.columns]
|
||||
lines = ["| " + " | ".join(cols) + " |", "|" + "---|" * len(cols)]
|
||||
for _, r in df.iterrows():
|
||||
lines.append("| " + " | ".join("" if (isinstance(v, float) and pd.isna(v)) else str(v)
|
||||
|
|
@ -384,7 +420,7 @@ def run(since: str, until: str | None, horizons: tuple, start: str, out_dir: str
|
|||
w = n / n.sum() if n.sum() else n
|
||||
days, total = g["date"].nunique(), int(n.sum())
|
||||
# 样本纪律(方案 2.4)机器标注,只有两档:可读=计划日≥20 且样本≥100;其余只看方向
|
||||
grade = "可读" if (days >= 20 and total >= 100) else "方向"
|
||||
grade = "够" if (days >= 20 and total >= 100) else "不够,只看方向"
|
||||
return pd.Series({"days": days, "n": total,
|
||||
"ret": round(g["ret"].mean(), 2),
|
||||
"excess_all": round(g["excess_all"].mean(), 2),
|
||||
|
|
@ -406,14 +442,21 @@ def run(since: str, until: str | None, horizons: tuple, start: str, out_dir: str
|
|||
[["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"口径切换的总样本少于六十个计划日只写方向。", "",
|
||||
f"这份周报覆盖 {len(days_plan)} 个计划日。"
|
||||
f"一份名单要够二十个计划日、且总共够一百只股票,读数才算够看;"
|
||||
f"不够的只看方向,不看具体数值。", "",
|
||||
"## 一、六份名单 × 期限", "",
|
||||
"列的读法:excess_all 相对全池等权,excess_main 相对主榜等权(剥掉大票对小票的贝塔),"
|
||||
"excess_cap 相对同市值桶(成交额除以换手率三分位,方案 1.8c 口径);beat 是跑赢全池比例,"
|
||||
"全池自己的 beat 只有四成多(分布右偏),读它时与全池等权那一行相减;share 是最大单日样本占比"
|
||||
"(超过三成说明读数由一天主导,只看逐日表);grade 是样本纪律三档。"
|
||||
"不带后缀的列按计划日等权(主口径),带 _w 的列按样本加权,只用于与方案第一节手工读数对账。", "",
|
||||
"怎么读这张表。"
|
||||
"「比全池多涨几个点」是拿这份名单和当天全部股票的平均涨跌相比;"
|
||||
"「比主榜多涨几个点」是和主榜相比,这样能去掉大盘股和小盘股整体差异的影响;"
|
||||
"「比同市值股票多涨几个点」是只和市值相近的股票比。"
|
||||
"「跑赢全池的股票占比」和上面几列不是一回事:上面是收益差,这一列是只数比例。"
|
||||
"全池自己这一行的占比只有四成多,因为少数大涨的股票把平均值拉高了;"
|
||||
"所以读别的名单时要拿它和全池那一行相减,不能直接看绝对值。"
|
||||
"「最多的一天占了多少」超过三成,说明整个读数被一天主导,那就只看逐日那张表。"
|
||||
"「往后看几个交易日」不是持有天数——这个脚本不建仓、不计成本,不是回测。"
|
||||
"不带前缀的列按计划日等权,一天算一票,这是主口径;"
|
||||
"带「股票多的日子算得重」前缀的列是另一种算法,只用来和手工读数对账。", "",
|
||||
_md(lists_tbl), "",
|
||||
"## 二、分组读数", "", _md(groups_tbl), "",
|
||||
f"## 二之二、候选单逐日(期限 {h0} 日;第一节按日等权的读数就是这张表的平均,看集中度)", "",
|
||||
|
|
@ -449,7 +492,7 @@ def _list_reading(lists_tbl: pd.DataFrame, name: str) -> str:
|
|||
parts = []
|
||||
for _, r in sub.iterrows():
|
||||
parts.append(f"{int(r['h'])} 日:计划日 {int(r['days'])},样本 {int(r['n'])},"
|
||||
f"超额(全池){r['excess_all']:+.2f},跑赢 {r['beat']:.1f}%,纪律 {r['grade']}")
|
||||
f"比全池多涨 {r['excess_all']:+.2f} 个点,跑赢全池的占 {r['beat']:.1f}%,样本{r['grade']}")
|
||||
return ";".join(parts)
|
||||
|
||||
|
||||
|
|
@ -460,11 +503,15 @@ def compare_table(lists_tbl: pd.DataFrame, since: str, until: str | None) -> pd.
|
|||
{"系统": "选股系统", "口径": "候选单按计划日等权,相对全池等权超额与跑赢比例(第一节主口径)",
|
||||
"读数": _list_reading(lists_tbl, "候选单")},
|
||||
{"系统": "择时决策系统",
|
||||
"口径": "自评:判分表 decision_outcome 日终策略五日方向命中率,按原始收益方向判、非超额(其判分脚本口径)",
|
||||
"口径": "择时决策系统自己打的分。每个交易日收盘后给出的策略,看 5 个交易日后涨跌"
|
||||
"方向判断对不对,统计判对的比例。只看股票自己的涨跌,没有减去大盘涨跌,"
|
||||
"与上一行候选单的超额口径不同,两个数字不能直接比大小。",
|
||||
"读数": timing_self_eval(since, until)},
|
||||
{"系统": "PMS 账本",
|
||||
"口径": "动作账本当日 OPEN 行:机器通过(judge PASS)、人批(user PASS)、人拒(user REJECT);"
|
||||
"另列择时看多(结论表 BUY);四份名单与候选单同口径算收益",
|
||||
"口径": "这一行是 PMS 当天的新开仓审批记录,拆成三份名单:系统自动放行的、"
|
||||
"人工同意的、人工否决的。否决的也照样算收益,用来看当初拒得对不对。"
|
||||
"另外单列一份择时决策系统当天看多的股票。这四份名单的买入时点、"
|
||||
"往后看几天、跟谁比,都和候选单完全一样,可以直接跟候选单那一行对着看。",
|
||||
"读数": ";".join(f"{name}—{_list_reading(lists_tbl, name)}"
|
||||
for name in (*LEDGER_LISTS, TIMING_LIST))},
|
||||
]
|
||||
|
|
|
|||
Loading…
Reference in New Issue