迭代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,88 @@
# 技术实现方案:23-计算字段判定型扩展
> 依据:R-028(已定稿)+ R-026 既有实现(迭代 22)| 日期:2026-09-10 状态:**已实施**
## 1. 公式引擎(src/formula/evaluator.js
- **词法**:新增多字符运算符识别(`>= <= == != && ||` 优先于单字符);全角/符号归一扩展
`≥→>= ≤→<= ≠→!= >→> <→< =→=`
- **语法优先级**(低 → 高):`or``and` → 比较(`> < >= <= == !=`) → 加减 → 乘除 → 一元负 → 主;
`and`/`or` 以标识符(大小写不敏感)识别,`&&`/`||` 等价;
- **求值**:比较返回布尔;逻辑返回布尔(`!!l && !!r`);两者**缺值短路**(任一操作数为 null → null);
数字运算维持原语义(除零 → null、最终结果浮点归一);`normalizeResult` 对布尔直通;
- **新增 `inferResultKind(ast)`**:顶层为比较/逻辑 → `'boolean'``neg` 递归下钻),否则 `'number'`
- 校验(`validateFormula`)逻辑不变(变量存在性 / 函数白名单 / arity)。
## 2. 存储与校验
- **src/settings.js**`fieldSchema``resultKind: z.union([z.const('number'), z.const('boolean')]).default('number')`
`normalizeFields` 对 formula 字段归一 `resultKind`(非 `'boolean'` 一律 `'number'`),`decimals` 维持夹取 0-4
- **src/api/strategies.js#validateConfigSchema**:公式校验通过后增加**结果类型一致性**校验 ——
`declared = resultKind`(默认 number)必须等于 `inferResultKind(compileFormula(formula).ast)`
不一致抛 `field-validation`
- 声明「判定」但公式无比较 → 「结果类型选了「判定」,但公式没有比较运算(如 涨停价 > 基准值 + 1)」;
- 公式是判定但声明「数字」 → 「公式结果是判定,请把「结果类型」改为「判定」」。
## 3. 现算(src/formula/FormulaService.js
`computed[key]` 布尔直通:
```js
computed[c.key] = (typeof v === 'boolean') ? v
: ((typeof v === 'number' && Number.isFinite(v)) ? v : null);
```
其余不变(无 formula 字段零开销、只读 QuoteHub 内存缓存、不落库、历史行不计算)。
## 4. 前端
**StrategyFieldsEditor.jsx**
- 表单草稿增 `resultKind`(默认 `number`,编辑时回填);
- 「类型」行新增「结果」下拉(数字 / 判定);
- 公式框 placeholder 按结果类型切换(判定示例:`涨停价 > 基准值 + 网格大小`);
- 公式框下新增运算符提示行:`运算:+ - × ÷ 括号;判定:> < >= <= == != ,组合:and / or`
- `resultKind === 'boolean'` 时**隐藏小数位**输入;
- 列表「类型」列显示 `计算(判定)`
- **判定型试算**`value` 为布尔时显示 `试算:<名称> <代码> → ✓ 满足 / — 不满足`
**StrategyTab.jsx**
- 计算列渲染:**先判布尔**`typeof raw === 'boolean'`)—— `true``✓ <字段展示名>``state-success-primary`),
`false` / 缺值 → `—`(tertiary);否则走原数字格式化(`toFixed(decimals)` + 单位);
顺序不可颠倒(`Number(false) === 0` 会被误格式化成 `0.00`);
- 表头 `ƒ` 前缀、悬停公式原文、历史行 `—` 维持不变。
## 4.5 附:公式引擎选型论证(2026-09-10 老师提问「有没有现成的库」)
**结论:维持自写引擎**`src/formula/evaluator.js`,约 250 行 / 零依赖 / 71 条回归断言)。
| 候选 | 中文变量名 | 能力覆盖 | 体积/依赖 | 风险与维护 |
|---|---|---|---|---|
| **expr-eval** | ✗ ASCII 标识符 | 四则/比较/and·or/**三元 ?:**/属性访问/数组索引 | 小 | **CVE-2025-12735**:构造 variables 对象可致**任意代码执行**GHSA-jc85-fpwf-qm7x);上游维护停滞,社区有 fork |
| **mathjs** | ✗ 明确枚举可用字符(拉丁/希腊/字母类/数学字母数字),**不含 CJK** | 极全(矩阵/单位/复数/函数库) | 大(数百 KB 级) | 重;unicode 标识符诉求自 2015 年 issue #265 起未覆盖 CJK |
| **Jexl** | ✗ ASCII 标识符(支持 `a.b` / `a[b]` 取值) | 比较/逻辑/三元/自定义运算符 | 中 | 取值语义与「缺值短路」需自行包装 |
| **CELcel-js 等)** | ✗ ASCII 标识符 | 受限表达式语言(工业标准、可静态检查) | 中 | 语义偏严格,接入成本不低 |
| **解析器工具包**chevrotain / ohm-js | ✓(自定义词法) | 自己写语法 + 求值 | 中 | **等于仍自己实现**,只是换成语法文件 + 依赖 |
| **自写引擎(现状)** | ✓ 原生支持 | 四则/括号/比较/and·or/round·abs·min·max | 0 | 无 eval/Function、无属性访问、无数组索引、变量来自白名单目录 → **结构上无任意代码执行面** |
**为什么不换**
1. **中文变量名是产品决策**(R-026 Q2 老师拍板)。主流库标识符均限 ASCII → 引库就得加「中文名 → 占位符」映射层
(按长度降序替换防前缀冲突 + 错误位置与提示回译),**复杂度不降反升**,报错体验还变差;
2. 所需能力集很小,自写约 250 行即覆盖,且**语义完全可控**(缺值短路 → `—`、除零 → `—`、最终值浮点归一);
3. **攻击面**:表达式与变量都来自用户输入,expr-eval 恰恰因变量对象注入出过 RCE;自写引擎不做属性访问/动态函数调用(仅 4 个纯数学函数白名单),无此类面;
4. 项目为 DSH 外部插件,依赖越少越稳(现依赖仅 schemastery + MCP SDK)。
**什么时候值得换成库(判据)**
- 需要**条件分支/三元**(如三态信号「空/多/观望」)→ 自写加 `?:` 约 30 行即可,仍不必引库;
- 需要**字符串/日期/正则** → 能力超出「数值公式」,届时评估 CEL / Jexl(并重做安全评估);
- 需要**Excel 级函数库**SUM/IF/VLOOKUP…)→ 考虑 hyperformula / Formula.js,但那是产品形态变化(表格公式),应先讨论需求。
> 若后续决定改用第三方库,需同步修订**技术约束-022**(现规定「自写零依赖、禁止 eval/Function」)。
- `scripts/test-r026-formula-fields.mjs` 新增两节(第 7 节判定型 15 条 + 第 8 节端到端 8 条):
比较/and/or/全角符号/短路、`inferResultKind` 三例、结果类型一致性双向拒绝、布尔直通、
`strategy-positions` 附判定 `computed`、判定试算;
- **端到端夹具 = 真实数据**:以 2026-09-10 实盘涨跌停(积成电子 8.22/6.72 等)+ 真实持仓基准值构造三行,
断言 `可下空单 / 可下多单` 判定与实算一致;
- `npx tsc --noEmit` 0 错;`npm run build` 通过;全量 **17 个回归脚本全绿**r026 由 48 → 71 条断言)。
@@ -0,0 +1,43 @@
# 迭代复盘:23-计算字段判定型扩展
## 结论
**实现完成,待老师人工验收**。计算字段由「只出数字」扩展为「数字 / 判定」双结果类型;
网格超市两条网格规则(可下空单 / 可下多单)已可用公式表达,行为与实盘数据一致(真实验证 5 只 / 2 只)。
## 事实记录
- **引擎**src/formula/evaluator.js):多字符运算符识别 + 全角 `≥ ≤ ≠ ` 归一;
优先级 `or < and < 比较 < 加减 < 乘除`;比较/逻辑求值(缺值短路);`normalizeResult` 布尔直通;
新增 `inferResultKind(ast)`
- **存储**src/settings.js):`fieldSchema.resultKind`number|boolean,默认 number+ `normalizeFields` 归一;
- **校验**src/api/strategies.js):结果类型一致性校验(声明 vs `inferResultKind`),不一致抛 `field-validation`
- **现算**src/formula/FormulaService.js):`computed` 布尔直通;
- **前端**`StrategyFieldsEditor` 结果类型下拉 + 判定时隐藏小数位 + 运算符提示 + 判定试算(✓ 满足 / — 不满足);
`StrategyTab` 判定列渲染 `✓ 字段名`(绿)/ `—`(灰,含缺值与 `false`);列表类型显示 `计算(判定)`
- **验证**`test-r026` 48 → **71** 条(新增第 7 节判定型 15 条、第 8 节真实夹具端到端 8 条);
typecheck 0 错;build 通过(client bundle 197794 bytes);全量 17 脚本全绿。
## 设计与实现要点(沉淀)
1. **布尔必须先判**:渲染分支若先做 `Number.isFinite(Number(raw))``false` 会被当成 `0` 格式化成 `0.00`——
实现中显式把 `typeof raw === 'boolean'` 放在最前(代码注释标注);
2. **判定型「不满足」= `—` 而非「否」**:缺行情与条件不满足在视觉上同形,避免把「数据缺失」误读为「不可以」;
3. **结果类型显式声明 + 静态推断校验**:避免「公式悄悄变成判定」导致列渲染与语义脱节;
推断只看**顶层**运算(`neg` 下钻),规则简单可预期;
4. **阈值用字段引用而非字面量**`基准值 + 网格大小` 让规则随字段可调(老师 Q5 拍板)。
## 偏差
- 无功能偏差;范围按 Q1–Q5 收敛(方案 C 三态信号、`not` 取反未做,已记入「不做」)。
## 补记(2026-09-10 老师反馈)
- 字段配置弹层的变量选择从「+ 插入变量 ▾」下拉改为**标签平铺**(分组成行、点选即插入公式框光标处);
已随迭代实施并重建 bundletypecheck/build/17 脚本回归全绿)。
## 遗留
- 判定「不满足」与「缺数据」同显示为 `—`:若老师希望区分(如缺数据显示 `?` 或 tooltip「数据缺失」),
可后续加 hover 原因;
- 由老师侧重载插件后手动添加两个字段(或授权我用 `strategies/schema-update` 端点直接写入 settings)。
@@ -0,0 +1,42 @@
# 迭代目标:23-计算字段判定型扩展(含网格超市信号字段)
> 依据:**R-028(已定稿,2026-09-10** | 类型:小步迭代(R-026 能力延伸,不单列计划文档)
> 日期:2026-09-10 | 状态:**已实现,待老师人工验收**
## 目标
把计算字段从「只出数字」扩展到**判定型**(比较运算 + and/or 组合,结果类型由字段显式声明),
并用它给网格超市落地两条网格规则:**可下空单** = `涨停价 > 基准值 + 网格大小`
**可下多单** = `跌停价 < 基准值 - 网格大小`
## 目标描述
- **背景**:老师提出「当天涨停值 > 当前基准值 + 1 即可下空单 / 当天跌停值 < 当前基准值 - 1 可下多单」;
R-026 的计算字段只产数字(无比较运算),需先扩能力;
- **范围**:引擎(比较/逻辑/inferResultKind)、存储(`resultKind`)、API 校验(结果类型一致性)、
字段表单(结果类型选择 + 判定时隐藏小数位 + 运算符提示 + 判定试算)、持仓表判定列渲染(`✓ 字段名` / `—`);
- **不做**:三态信号字段(空/多/观望,方案 C)、not 一元取反、判定与数字的混合字段、策略级聚合;
- **验收线**(详见 `验收标准.md`):C1C8。
## 目标分解
1. 引擎:比较 `> < >= <= == !=`(含全角 `≥ ≤ ≠`+ 逻辑 `and/or`(含 `&& ||`+ 优先级 + 缺值短路 + `inferResultKind`
2. 存储/校验:`resultKind: number|boolean`(归一化 + 保存校验一致性);
3. 现算:`computed` 布尔直通(判定型不参与 toFixed 格式化);
4. UI:结果类型选择、判定占位与运算符提示、判定列 `✓ 字段名`(绿)/ `—`(灰)、判定试算(✓ 满足 / — 不满足);
5. 回归:`test-r026` 增补 R-028 段(23 条)+ 网格超市真实夹具端到端;
6. 文档:R-028 + 本迭代四件套 + 约束变更 3 处。
## 目标讨论过程
1. 2026-09-10 老师提出网格超市两条判定规则;
2. AI 用**真实数据**验证可行性(13 只持仓 × 今日涨跌停 × 真实基准值 → 空单 5 / 多单 2),
并指出 R-026「只出数字」的能力边界 → 提出 A/B/C 三方案;
3. 老师拍板 Q1–Q5(全采建议):A 判定型 / ✓ 与 — / 支持 and·or / 结果类型显式 / 阈值引用网格大小;
4. T-012 转正 R-028 → 本轮实施完成(typecheck + build + 17 脚本全绿)。
## 对老师(项目主理人)的配合需求
- **重载插件后**在页面添加这两个字段(字段配置 → 添加字段 → 类型「计算」→ 结果「判定」→ 公式粘贴 → 试算 → 保存);
或告知由我用端点写入;
-`验收标准.md` C1C8 人工确认。
@@ -0,0 +1,48 @@
# 验收标准:23-计算字段判定型扩展
> 依据:R-028 + `技术实现方案.md` 日期:2026-09-10 状态:**待老师人工验收**
## 1. 验收目标
判定型计算字段可用(比较 + and/or + 结果类型显式声明 + 校验一致),并用它给网格超市落地
「可下空单 / 可下多单」两列;数字型计算字段与既有能力零回归。
## 2. 验收标准线
| # | 验收项 | 判定 |
|---|---|---|
| C1 | 字段表单出现「结果」下拉(数字 / 判定) | 选「判定」后**小数位输入消失**;公式框 placeholder 变为判定示例 |
| C2 | 判定公式可保存 | 展示名「可下空单」/ 类型「计算」/ 结果「判定」/ 公式 `涨停价 > 基准值 + 网格大小` → 保存成功 |
| C3 | 判定试算 | 点「试算」显示 `试算:<名称> <代码> → ✓ 满足``— 不满足`(无持仓 → 「暂无可试算的持仓数据」) |
| C4 | 持仓列渲染 | 满足行显示 `✓ 可下空单`(绿);不满足 / 缺行情 → `—`(灰);表头 `ƒ` 前缀、悬停见公式原文 |
| C5 | 两条规则与实盘一致 | 今日上证实况下,「可下空单」应为 5 只(积成电子/天海防务/万顺新材/大连热电/华智数媒)、「可下多单」应 2 只(中国化学/TCL中环),其余两列均为 `—` |
| C6 | 逻辑组合可用 | 公式 `涨停价 > 基准值 + 网格大小 and 现价 < 基准值 + 网格上边界` 可保存并算出(and / or / `&&` / `\|\|` 均可) |
| C7 | 结果类型一致性校验 | 声明「判定」但公式为算式(`1 + 1`)→ 拒绝并提示;公式为判定但声明「数字」→ 拒绝并提示 |
| C8 | 零回归 | 数字型计算字段(如 `(现价 - 成本价) × 份额`)照常;四类手填字段、列设置、历史行 `—`、Tab 设置与静态策略均不受影响 |
## 3. 边界与异常
| 场景 | 期望 |
|---|---|
| 缺行情 / 缺合约信息(涨停跌停取不到) | 判定列 `—`(不显示 false 误导) |
| 手填字段值空(如未填基准值) | 判定列 `—` |
| 全角符号输入(`≥ ≤ ≠ ×÷` | 正常解析 |
| 公式引用另一个公式字段 | 仍被拒(零循环依赖不变) |
| 历史持仓行 | 判定列 `—` |
## 4. 验收方法
1. `node scripts/test-r026-formula-fields.mjs`**71/71**(含判定型 15 条 + 真实夹具端到端 8 条);
2. 全量回归 17 个脚本全绿;`npx tsc --noEmit` 0 错;`npm run build` 通过;
3. 老师人工验收:重载插件 → 网格超市 tab → 字段配置 → 添加上述两字段 → 对照 C1–C8(重点 C5 与实盘一致性)。
## 5. 判定线
**通过**:C1–C8 全部通过 + 回归/构建全绿 + 老师人工确认。
**任一即不通过**:判定列把 `false` 显示成数字(`0.00`);缺数据显示为 `否`(误导);结果类型声明与公式不一致仍能保存;
数字型计算字段或既有四类字段出现回归。
## 6. 不在本次范围
三态信号字段(空 / 多 / 观望,方案 C)、`not` 一元取反、判定与数字混用的单字段、策略级聚合(跨行求和)、
历史持仓行的时点判定。