# 迭代复盘: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/21(R-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`:**A1–A11**(重构)+ **B1–B12**(计算字段),重点四条: 1. 设置页无「策略分组」;策略 tab 恒两个、不可增删改名,但 Tab 设置仍可显隐/拖拽排序; 2. 策略 tab 顶栏「字段配置 / 列设置」可用,字段增删保存后表格列跟随、重启后定义仍在; 3. 计算字段可用:中文变量公式 + 变量选择器 + 试算 → 持仓列只读显示结果(`ƒ` 标记 + 悬停看公式); 4. 边界:非法公式/未知变量/名称冲突被拒 + 描红;缺数据与历史行显示 `—`;`strategy_holdings.values` 不含计算字段键。 ## 经验沉淀(候选) - **并行委派的契约必须在开工前冻结**:本轮前端由子代理并行实现,期间服务端 `settings.js` 仍在演进 (子代理观察到 mtime 变化、并在报告里提出「疑似工作区未冻结」)——契约冻结 + 文件集互斥是并行可行性的前提。 - **真相源迁移要全量搜读点**:本次 `normalizeStrategyColumns` 差点漏改,靠既有回归脚本的红灯暴露; 遗留的旧键兼容读取(而非直接删键)让迁移零风险、零脚本。 - **测试期望也需要跟着设计演进**:`test-r013-columns` 的过期期望在迭代 20 复盘就被记录却未处理, 本轮顺手清偿,避免「红灯常态化」。