docs(agent): 新增「买卖理由词表」与「长任务反馈」两条硬约束(§27.1 / §27.2)

把这两次踩过的坑固化成约束,避免后续 agent 各自发挥:
- §27.1:买卖点必须带结构化理由(成交与未成交都要),原因分类封闭词表、两个引擎共用;
  data 只放引擎当时的真实数字,前端不推算;跌出 TopN / 不在候选池 / 全量换仓 / Tmin /
  Tmax / 涨跌停 / 现金不足必须区分,宁可新增 code 也不套语义不符的旧 code;
  因子曲线口径(持仓市值加权原始值、空仓不落点)+ 方向/单位必须写明;
  每条曲线可新页面放大且放大页从归档读。
- §27.2:长任务点了立刻有字、显示真实作业号/阶段/逐秒已用、可取消、失败给后端原文、
  成功给归档入口、自动滚入视野;同步接口没有作业号时如实说明,禁止伪造。
This commit is contained in:
Simon
2026-10-01 18:13:37 +08:00
parent 2a88ca6076
commit a3055eff5d
+32
View File
@@ -817,6 +817,38 @@ factor_exposure
前端组件只依赖这个结构。 前端组件只依赖这个结构。
## 27.1 买卖理由:必须能回答「为什么买 / 为什么卖」,且数字来自引擎
用户看回测结果时的第一个问题不是「赚了多少」,而是「**为什么在这里买 / 卖**」。
因此每个买卖点(**成交的与未成交的都要**)必须带结构化理由
(`TradeReason{code, text, data}`,见 `backend/app/quant/trade_reasons.py`):
- **原因分类是封闭词表**:组合引擎(`combo_engine.py`)与单策略引擎(`local_engine.py`)
共用同一套 code 与文案构造器,禁止各自手写措辞 —— 否则同一件事会出现两种说法。
- **`data` 里只能是引擎当时的真实数字**(名次 / 候选数 / 综合分 / 各因子**原始值** /
持有交易日 / 预算 / 涨停比值…)。前端**只展示不推算**;拿不到名次就写「未给出名次」,
不拿旧名次或其他日期的数据冒充。
- **把事实说准**:跌出 TopN ≠ 不在候选池(被股票池/条件过滤)≠ 全量换仓
(策略每次调仓先清仓,被卖的股票可能仍排在前列)≠ Tmin 保护暂留 ≠ 超 Tmax 强制了结
≠ 涨停/跌停/停牌/现金不足。宁可为一种情况新增一个 code,也不要套一个语义不符的旧 code。
- **因子曲线**(`result.factor_curves`)= 当日**持仓按市值加权平均的原始值**
(不做 z-score、不按方向取反,空仓日不落点、不插值、不用 0 填充),
界面必须同时写出方向与单位(如「股息率 %,越高越好」),否则读者会误判曲线的含义。
- **每条曲线都要能新页面放大**(`/charts/{归档id}?s=...`):放大页从**归档**读同一份数据
(URL 可分享、口径不漂移);没有归档 id 时如实说明「未归档,无法放大」,不给坏链接。
## 27.2 长任务反馈:点了立刻有字、看得出在动、出事了能自救
用户原话:「点了回测没有任何反馈,不清楚是不是已经开始」。因此跑异步 Job 的页面必须:
- 提交**瞬间**就有反馈(提交中 → 排队中),不等到后端返回;
- 显示**真实**作业号 / 阶段 / 逐秒自增的已用时间(用 `lib/jobs.ts` 的 `useJobRunner`
+ `components/JobProgress.tsx`,不要各页手写一套);
- 排队/运行中可**取消**;失败显示后端原文;成功给「打开归档 / 去对比」入口;
- 反馈条出现时**自动滚入视野**,并带 `role="status" aria-live="polite"`;
- 同步接口(如 `POST /api/signals`)**没有**作业号与阶段时,如实说明「同步请求、无阶段、
不可取消」,**禁止**合成假作业号或假进度喂给反馈组件。
--- ---
# 28. AI Agent 约束 # 28. AI Agent 约束