迭代22+23: 策略tab静态化 + 计算字段(数字/判定型)+ 网格超市信号字段

- R-027 静态化:策略恒为内置两项(不可增删改名,保留显隐/排序);设置页移除「策略分组」;
  字段配置入口迁到策略 tab 内「列设置」旁(FieldConfigDialog + schema-update 单策略写入)
- R-026 计算字段:自写公式引擎(中文变量、四则/括号/round·abs·min·max、缺值短路→—)+
  变量目录(行情/合约/持仓/自定义字段)+ strategy-positions 逐行现算(只读内存缓存,不落库、列只读)
- R-028 判定型:比较运算 + and/or + inferResultKind + 结果类型一致性校验 + 判定列 ✓/— 渲染;
  网格超市落地 可下空单=涨停价>基准值+网格大小、可下多单=跌停价<基准值-网格大小
- 变量选择改标签平铺(老师反馈);公式手册 docs/99-其他材料/计算字段公式说明.md
- 约束同步:产品约束-002/003/004/009/013/014、技术约束-014/015/022/023/024、UI约束-002/003/005/008
- 回归:新增 test-r026/test-r027,更新 r011/r013/r017,17 个脚本全绿;typecheck/build 通过
This commit is contained in:
2026-09-10 14:05:06 +08:00
parent 3dfa39b8bf
commit ec02a791ee
40 changed files with 3369 additions and 659 deletions
@@ -0,0 +1,105 @@
# 迭代复盘:22-策略计算字段与策略 tab 静态化
## 结论
**实现完成(两阶段全范围),待老师人工验收**(尚未宣告验收通过)。事实:策略 tab 静态化 + 字段配置入口迁移(R-027)
与计算字段(R-026)均已落地;`pnpm typecheck` 0 错、`pnpm build` 通过、17 个回归脚本全绿(含新增 2 个)。
## 事实记录
### 阶段一 · 策略 tab 静态化与入口迁移(R-027)
- **src/settings.js**:新增 `BUILTIN_STRATEGIES`(内置两项:网格超市 grid-supermarket / 手动做T manual-t)、
`isBuiltinStrategy / assertBuiltinStrategy / normalizeFields`;字段定义真相源迁到 **`strategyFields`**
`getStrategies` 恒返回内置两项、缺失时兼容读取旧 `strategies[].configSchema``getStrategyFields` /
`updateStrategyFields` 为唯一读写入口);`normalizeTabs` 改为**常量派生**3 内置 tab + 2 策略 tab
只保留存量 visible/order 偏好、丢弃未知条目、补齐缺失条目),`updateTabs` 只接受内置 id
`normalizeStrategyColumns` 的字段列来源改读 `getStrategyFields`;新增 `scopeValue` 容错读取
(测试桩无 `get` 不炸);**退役** `addStrategy / removeStrategy / renameStrategy / updateStrategies /
appendStrategyTab / removeStrategyTab / DEFAULT_STRATEGIES`
- **src/api/strategies.js****退役** `strategies/add | strategies/remove | strategies/update`(调用返回 not-found);
新增 `strategies/schema-update { strategyId, configSchema }`(唯一写路径)+ `validateConfigSchema`
(结构 / key 唯一 / 展示名唯一 / 内置变量名冲突 / 枚举选项 / 公式校验,失败统一 `code='field-validation'`);
新增 `formula/variables`(变量目录)与 `formula/trial`(试算)。
- **设置页**SettingsSection.jsx):「策略分组」子 tab 整体移除(含 CRUD 与删除确认)→ 设置页仅
「Tab 设置 / QMT 连接配置」。
- **字段配置弹层**FieldConfigDialog.jsx 新建 + StrategyFieldsEditor.jsx 改造):策略 tab 顶栏
「字段配置」(列设置左侧)→ 锚定弹层(520px / 70vh / 内部滚动);字段列表 + 添加/编辑/删除 + 底部
[关闭]/[保存字段]dirty 时关闭/外点/Esc 二次确认;保存走 `strategies/schema-update`
### 阶段二 · 计算字段(R-026)
- **src/formula/evaluator.js**(自写零依赖引擎,不用 `eval/Function`):词法支持中文标识符 + 全角归一
(()+-×÷),递归下降解析(四则 / 括号 / 一元负 / round·abs·min·max;长度 ≤500、深度 ≤32);
`validateFormula` 返回 `{ok,vars}``{ok:false,error:{code:empty|syntax|unknown-var|unknown-func|arity,message,position}}`
求值**缺值短路**(任一变量 null → 整式 null)、除零/非有限 → null、**最终结果浮点归一**|v|<1e12 四舍五入 10 位,
消除 `0.7000000000000002` 类噪声;中间步骤不归一再保精度——首版曾因每步归一导致 `6.66666667` 精度劣化,已改)。
- **src/formula/variables.js**:变量目录单一入口(行情 7 项 / 合约 2 项 / 持仓账本 3 项 + 运行时按策略
非 formula 字段展示名展开「自定义字段」组);`allowedVarNames` 排除 formula 字段(零循环依赖);
`buildContext` 取数(昨收兜底合约 preClose、布尔 → 0/1、空值 → null)。
- **src/formula/FormulaService.js**`computeRows``strategy-positions` 行附 `computed`;行情/合约
**只读 QuoteHub 内存缓存(不读穿透)**;**无 formula 字段直接返回原数组(零开销)**;值不落库。
- **前端**:字段表单类型「计算」分支(等宽公式框 + 「+ 插入变量」分组菜单 + 试算 + 小数位 0-4 默认 2;
保存失败公式框描红 + 服务端文案);持仓表计算列**只读**(表头 `ƒ` + title 公式原文、按 `toFixed(decimals)` + 单位
格式化如 `6.67 %` / `700.00 元`、缺数据 `—`、点击不进内联编辑);历史持仓行 `—`;列设置弹层列名带 `ƒ`
### 实机数据核实(实施前)
- `~/.dsh/settings.yaml`:策略仅两个内置(网格超市 5 字段 / 手动做T 1 字段);
- `~/.dsh/one-divine-lot/store.db``strategy_holdings` 13 / 3 行,交易与归属数据只涉及这两个策略
**无自建策略,静态化零数据迁移**
### 验证
- `pnpm typecheck` → 0 错;`pnpm build` → 通过(lib 32 文件 + client bundle 195545 bytes`scripts/wrap-client.mjs` 正常);
- 回归脚本 **17 / 17 全绿**
- 新增 `scripts/test-r027-static-strategies.mjs` 35/35(策略常量 / 旧定义兼容 / 单策略写入 / tabs 归一化 / 端点退役);
- 新增 `scripts/test-r026-formula-fields.mjs` 48/48(引擎 / 目录 / 上下文 / 现算 / 保存校验 / 试算);
- 更新 `test-r011-tabs` 22/0、`test-r011-api` 17/0、`test-r013-custom-fields` 23/0、
`test-r013-columns` 18/0(期望值按 R-015 行情列 + R-027 真相源更新)、`test-r017-candidates` 12/0(策略 id 改用内置);
- 其余(r009 系列 / r016 / r018 / quote-sync / position-sync / health / mcp)零改动全绿。
## 偏差、顺带修复与发现
1. **`generateStrategyId` / `PINYIN_MAP` 保留**(方案原文列入退役):QMT 连接 id 生成
`generateQmtConnectionId` 仍依赖,本轮不动(已回写技术方案)。
2. **真 bug(实施中发现并修复)**`normalizeStrategyColumns` 仍读旧 `strategies[].configSchema`
字段定义迁到 `strategyFields` 后策略列会丢失字段列 → 改为 `getStrategyFields(scope, strategyId)`
教训:真相源迁移必须全量搜「读点」而不只是「写点」。
3. **兼容修复**`strategy-positions` 新增 settings 依赖后,测试里无 `get` 的 settings 桩会抛错
test-r016/test-r017 暴露)→ 新增 `scopeValue` 容错读取。
4. **顺带修复迭代 20 复盘遗留第 1 项**`test-r013-columns` 期望过期(R-015 增加 4 个默认隐藏行情列后应为
12 列)——本次随实现更新期望并补「strategyFields 优先」用例。
5. **`test-r017-candidates` 策略 id 迁移**`attribution-targets``getStrategies` 取策略表,
静态化后仅内置两项 → 测试改用 `manual-t / grid-supermarket`R-027 的必然结果,非缺陷)。
6. **交互边界修复**:字段配置弹层的「外点关闭」原以弹层面板为界,点「字段配置」按钮自身会触发放弃确认
(dirty 时甚至静默卸载)→ 改为以**锚点容器**(含两个按钮)为界。
7. **工作区背景(非本轮产出)**:仓库工作区另含迭代 19/20/21R-022/R-023/R-024/R-025)的未提交改动,
本轮未触碰其逻辑,仅在其上叠加;建议老师确认后一并提交。
## 遗留与风险
- **行情 miss 显示 `—`**:计算列取数只读内存缓存、不读穿透(技术约束-023);某 code 无快照时该格为空,
等 QuoteSync 下轮(≤5s)覆盖。若老师更希望「宁可慢也要立刻有价」,可改判为读穿透。
- **需要重载插件才能人工验收**`~/.dsh/profiles/web/node_modules/one-divine-lot` 是指向本工作区的软链,
lib 已构建完成;老师侧需重载插件(或重启 dsh web)后在页面验收(未由我启动任何服务)。
- **旧 `strategies[].configSchema` 仅只读兼容**:本机字段定义在其首次保存后落入 `strategyFields`
(不写迁移脚本、不动库数据)。
- 自建策略若未来出现:读取忽略、tab 丢弃,其 `strategy_holdings` 数据保留在库(本机无此情况)。
## 待老师确认事项(人工验收)
`验收标准.md`**A1A11**(重构)+ **B1B12**(计算字段),重点四条:
1. 设置页无「策略分组」;策略 tab 恒两个、不可增删改名,但 Tab 设置仍可显隐/拖拽排序;
2. 策略 tab 顶栏「字段配置 / 列设置」可用,字段增删保存后表格列跟随、重启后定义仍在;
3. 计算字段可用:中文变量公式 + 变量选择器 + 试算 → 持仓列只读显示结果(`ƒ` 标记 + 悬停看公式);
4. 边界:非法公式/未知变量/名称冲突被拒 + 描红;缺数据与历史行显示 `—``strategy_holdings.values` 不含计算字段键。
## 经验沉淀(候选)
- **并行委派的契约必须在开工前冻结**:本轮前端由子代理并行实现,期间服务端 `settings.js` 仍在演进
(子代理观察到 mtime 变化、并在报告里提出「疑似工作区未冻结」)——契约冻结 + 文件集互斥是并行可行性的前提。
- **真相源迁移要全量搜读点**:本次 `normalizeStrategyColumns` 差点漏改,靠既有回归脚本的红灯暴露;
遗留的旧键兼容读取(而非直接删键)让迁移零风险、零脚本。
- **测试期望也需要跟着设计演进**`test-r013-columns` 的过期期望在迭代 20 复盘就被记录却未处理,
本轮顺手清偿,避免「红灯常态化」。