ec02a791ee
- 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 通过
6.7 KiB
6.7 KiB
技术实现方案: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] 布尔直通:
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] 取值) |
比较/逻辑/三元/自定义运算符 | 中 | 取值语义与「缺值短路」需自行包装 |
| CEL(cel-js 等) | ✗ ASCII 标识符 | 受限表达式语言(工业标准、可静态检查) | 中 | 语义偏严格,接入成本不低 |
| 解析器工具包(chevrotain / ohm-js) | ✓(自定义词法) | 自己写语法 + 求值 | 中 | 等于仍自己实现,只是换成语法文件 + 依赖 |
| 自写引擎(现状) | ✓ 原生支持 | 四则/括号/比较/and·or/round·abs·min·max | 0 | 无 eval/Function、无属性访问、无数组索引、变量来自白名单目录 → 结构上无任意代码执行面 |
为什么不换:
- 中文变量名是产品决策(R-026 Q2 老师拍板)。主流库标识符均限 ASCII → 引库就得加「中文名 → 占位符」映射层 (按长度降序替换防前缀冲突 + 错误位置与提示回译),复杂度不降反升,报错体验还变差;
- 所需能力集很小,自写约 250 行即覆盖,且语义完全可控(缺值短路 →
—、除零 →—、最终值浮点归一); - 攻击面:表达式与变量都来自用户输入,expr-eval 恰恰因变量对象注入出过 RCE;自写引擎不做属性访问/动态函数调用(仅 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 --noEmit0 错;npm run build通过;全量 17 个回归脚本全绿(r026 由 48 → 71 条断言)。