Files
one_divine_lot/docs/04-迭代记录/23-计算字段判定型扩展/技术实现方案.md
T
kyugao ec02a791ee 迭代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 通过
2026-09-10 14:05:06 +08:00

6.7 KiB
Raw Blame History

技术实现方案:23-计算字段判定型扩展

依据:R-028(已定稿)+ R-026 既有实现(迭代 22)| 日期:2026-09-10 状态:已实施

1. 公式引擎(src/formula/evaluator.js

  • 词法:新增多字符运算符识别(>= <= == != && || 优先于单字符);全角/符号归一扩展 ≥→>= ≤→<= ≠→!= >→> <→< =→=
  • 语法优先级(低 → 高):orand → 比较(> < >= <= == !=) → 加减 → 乘除 → 一元负 → 主; and/or 以标识符(大小写不敏感)识别,&&/|| 等价;
  • 求值:比较返回布尔;逻辑返回布尔(!!l && !!r);两者缺值短路(任一操作数为 null → null); 数字运算维持原语义(除零 → null、最终结果浮点归一);normalizeResult 对布尔直通;
  • 新增 inferResultKind(ast):顶层为比较/逻辑 → 'boolean'neg 递归下钻),否则 'number'
  • 校验(validateFormula)逻辑不变(变量存在性 / 函数白名单 / arity)。

2. 存储与校验

  • src/settings.jsfieldSchemaresultKind: 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] 布尔直通:

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 条断言)。