迭代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,92 @@
# 计划:策略计算字段 + 策略 tab 静态化重构(阶段航点)
> 编号:PLAN-019 | 粒度:阶段航点 创建:2026-09-10 | 状态:**进行中(迭代 22 交互设计阶段)**
> 派生自终极目标:目标-002(策略定义能力)、目标-003(按策略监控市场)
> 依据需求:**R-027(已定稿,2026-09-10,重构·第一阶段)+ R-026(已定稿,2026-09-10,计算字段·第二阶段)** —— 均符合入范围门槛
> 设计约束:沿用 技术约束-012(策略定义存储)、技术约束-015configSchema)、UI约束-003/004/005/006/007
> 本次拟修订(实施时执行):产品约束-002/003/004/009、UI约束-002/003/005;新增计算字段约束条目
## 目标
**一句话**:策略字段能力升级为「可算」,并把字段配置的入口搬到它该在的地方。
- **第一阶段(R-027 重构)**:策略 tab **静态化**(插件内置、不可增删改名,保留显隐/排序);设置页「策略分组」移除;
字段编辑功能迁到**主窗口策略 tab 内、「列设置」旁的「字段配置」弹层**,保存走单策略字段端点;
- **第二阶段(R-026 计算字段)**:在迁移后的入口上新增第五种字段类型「**计算(formula)**」——用户写中文变量公式
引用服务端缓存数据集,服务端按持仓行**实时计算**列值(不落库、列只读、缺数据显示 `—`)。
**为什么合并成一个迭代**:计算字段的公式表单必须落在字段配置入口上;若先按旧入口(设置页)实现再搬迁,等于白做一遍。
## 范围
**第一阶段 · 重构(R-027**
1. **tab 体系**`strategies` 常量化为内置两项(网格超市 / 手动做T);`settings.tabs` 中策略条目固定(不可增删);
保留 Tab 设置的显隐 + 拖拽排序;`addStrategy / removeStrategy / appendStrategyTab / removeStrategyTab / generateStrategyId` 退役;
2. **设置页**:移除「策略分组」子 tab(含策略新增/重命名/删除与确认弹窗)→ 设置页 = Tab 设置 + QMT 连接配置;
3. **字段配置入口**:策略 tab 顶部新增「字段配置」按钮(列设置旁)→ 独立弹层(字段列表 + 添加/编辑/删除 + 保存);
`StrategyFieldsEditor` 改造为弹层内容;保存改走新增端点 `strategies/schema-update { strategyId, configSchema }`
4. **存储归一化**:settings 只保留字段定义(按 strategyId+ `strategyColumns` 覆盖;存量 settings 读取时归一化(无迁移脚本)。
**第二阶段 · 计算字段(R-026)**
5. **轻量变量目录**(服务端):数据集 → 中文变量名 → 取数函数(行情+合约 / 持仓账本 / 本行自定义字段值),T-006 数据池的第一块砖;
6. **公式引擎**:自写小型表达式解析器(零依赖、支持中文标识符、四则 + 括号 + round/abs/min/max),含语法校验与求值;
7. **数据/API**`configSchema` 支持 `type='formula'` + 公式串 + 小数位;`strategy-positions` 逐行现算随行返回;
字段保存校验(语法 / 变量存在性 / 变量命名规则)+ **试算端点**(取一行真实持仓试算);
8. **UI**:字段配置弹层内公式分支(公式框 + 变量选择器 + 试算 + 单位/小数位);策略持仓表计算列只读渲染(`ƒ` 表头 + 悬停看公式 + 格式化 + 缺数据 `—`);
9. 交互设计 → 技术方案 → 实现 → 回归脚本(独立数据目录,技术约束-011)→ 验收。
**不做**(本期边界):
- 策略级聚合(跨行求和)——留二期;QMT 账户域 / 交易记录作为变量来源(用例不明确,后补);
- 公式引用公式(变量目录排除 formula 字段);历史持仓行时点计算(显示 `—`);布尔型公式结果;公式框实时高亮/自动补全;
- 完整 T-006 数据池中间层(本期只落变量目录);
- 策略的重命名 / 增删 / 归档(静态化后不存在);自建策略数据迁移(实测无自建策略)。
## 涉及文件(技术方案阶段细化)
```
src/
├── settings.js # strategies 常量 + tabs 固定 + 字段定义/列配置归一化;退役 CRUD 辅助
├── api/strategies.js # 策略 CRUD 端点退役;新增 strategies/schema-update(单策略字段)
├── formula/ # 新增:公式引擎(第二阶段)
│ ├── variables.js # 轻量变量目录(数据集 → 中文变量名 → 取数函数)
│ └── evaluator.js # 表达式解析 + 校验 + 求值
├── position/PositionManager.js # strategy-positions 行内公式现算
├── client/views/SettingsSection.jsx # 移除「策略分组」子 tab;Tab 设置保留
├── client/views/StrategyTab.jsx # 顶栏「字段配置」+「列设置」;计算列只读渲染
├── client/views/FieldConfigDialog.jsx # 新增:字段配置弹层(复用/改造 StrategyFieldsEditor
└── client/views/StrategyFieldsEditor.jsx # 改造为弹层内容 + 单策略保存 + 公式分支
scripts/
└── test-r026-formula-fields.mjs # 回归脚本(第二阶段)
```
## 实现步骤
1. **文档骨架**PLAN-019 + 迭代 22(迭代目标 / UI交互设计),R-026 + R-027 定稿 —— 本步;
2. **交互设计过审**(老师拍板)→ 出技术实现方案(两阶段规格)+ 验收标准;
3. **第一阶段实现**:tab 静态化 + 设置页收敛 + 字段配置弹层 + 单策略端点 + 存量归一化 → 回归(策略 CRUD 相关用例退役);
4. **第二阶段实现**:变量目录 + 公式引擎 + 现算/校验/试算端点 + UI(公式表单 / 只读列)→ 回归;
5. **验证 + 老师人工验收 + 迭代复盘**
## 验收要点
**第一阶段(重构)**
- 设置页只有「Tab 设置 / QMT 连接配置」,无「策略分组」;无新增/重命名/删除策略入口;
- 策略 tab 恒为「网格超市 / 手动做T」两个:不可增删改名;Tab 设置里仍可显隐 + 拖拽排序(现有偏好生效);
- 策略 tab 顶部「字段配置」按钮打开弹层:可添加/编辑/删除字段并保存;保存后表格列同步跟随;重启后定义仍在;
- 服务端只接受内置两个 strategyId;`strategy_holdings` / 归属数据零影响(13 / 3 行不变)。
**第二阶段(计算字段)**
- 字段类型可选「计算」:写中文变量公式、选择器插入、试算可算出结果;非法公式/未知变量被拒并提示;
- 策略持仓表计算列只读,按单位/小数位格式化,悬停可见公式;缺数据/除零显示 `—`,不炸表;
- 计算字段不落库(公式存 settings、值每次现算);四式用例(浮动盈亏 / 盈亏比例 / 距涨停 / 网格占用)可算出;
- 既有四类字段与手填值编辑、列设置、历史持仓展示零回归;
- typecheck + build + 回归脚本通过;老师人工验收。
## 关联
- 需求:**R-027**(重构·第一阶段)、**R-026**(计算字段·第二阶段);上游 R-013(字段模型/列机制)、R-011(Tab 显隐排序)、R-015(行情/合约缓存)、R-010(最后一笔成交价)、R-018(份额账本);
- 取代:R-003、R-011(部分)、R-013(入口条款)——由 R-027 取代;
- 延展:T-006(数据池中间层草稿)——变量目录为其起点;
- 迭代:22-策略计算字段与策略tab静态化(两阶段)。
+6 -3
View File
@@ -10,13 +10,16 @@
| 编号 | 约束说明 | 添加日期 | 生效状态 | 失效日期 | 最后一次变更描述 |
|---|---|---|---|---|---|
| UI约束-001 | 会话头部 QMT 连接切换 chip:紧凑形态 `QMT: <激活配置名> ▾`,与 PTC 模式标签并排;下拉菜单列出全部配置(当前激活项勾选标记),点选即激活;操作结果用顶部轻提示反馈(成功绿/失败红,约 3s 消失,沿用现有 Toast 体系) | 2026-08-29 | 生效 | - | R-004 定稿(2026-08-29):Q9/Q10 确认的头部控件形态 |
| UI约束-002 | 设置页「QMT 连接配置」子 tab:与「Tab 设置 / 策略分组」并列的第三个子 tab;配置以**卡片形式**展示(自适应网格 minmax(260px,1fr),激活卡片绿色描边):卡片含名称 + 激活/默认徽标 + HTTP 地址 + 行内操作(激活 / 设为默认 / 编辑 / 测试连接 / 删除)+ 新增/编辑表单(纵排两字段);删除沿用现有确认弹窗模式,删除最后一条时给出禁止提示;超时不设输入框(Q5);长文本(地址/错误信息)单行省略 + 悬停看全文,组件设 minWidth:0 防撑行 | 2026-08-29 | 生效 | - | 变更(2026-08-29 老师反馈):列表形式改卡片形式,约束组件宽度、长文本省略不换行;原定稿:设置页子 tab 形态沿用现有设置交互体系;变更(2026-09-02 R-011 定稿):「通用设置」更名为「Tab 设置」 |
| UI约束-003 | 设置页「Tab 设置」子 tab:表格列出**全部会话 tab(系统内置 + 策略分组)混排**,内置行带「内置」徽标、策略行带「策略」徽标;每行 = 拖动手柄(原生 HTML5 Drag & Drop+ 名称 + 显示/隐藏开关;**任何行均无重命名/删除按钮**;拖动落点确认后**立即持久化**(每次 drop 提交整表,沿用「刷新页面后生效」提示机制);「策略分组」子 tab 只保留 新增/重命名/删除,移除排序箭头与显隐开关,并提示「顺序与显示请在「Tab 设置」中调整」 | 2026-09-02 | 生效 | - | R-011 定稿(2026-09-02 Q1/Q4/Q5 确认):拖动排序 + 显隐开关,禁重命名/删除 |
| UI约束-002 | 设置页「QMT 连接配置」子 tab:与「Tab 设置」并列的子 tab(R-027 后设置页仅此两个子 tab;配置以**卡片形式**展示(自适应网格 minmax(260px,1fr),激活卡片绿色描边):卡片含名称 + 激活/默认徽标 + HTTP 地址 + 行内操作(激活 / 设为默认 / 编辑 / 测试连接 / 删除)+ 新增/编辑表单(纵排两字段);删除沿用现有确认弹窗模式,删除最后一条时给出禁止提示;超时不设输入框(Q5);长文本(地址/错误信息)单行省略 + 悬停看全文,组件设 minWidth:0 防撑行 | 2026-08-29 | 生效 | - | 变更(2026-08-29 老师反馈):列表形式改卡片形式,约束组件宽度、长文本省略不换行;原定稿:设置页子 tab 形态沿用现有设置交互体系;变更(2026-09-02 R-011 定稿):「通用设置」更名为「Tab 设置」**变更(2026-09-10 R-027**:移除「策略分组」子 tab,设置页子 tab 由三变二 |
| UI约束-003 | 设置页「Tab 设置」子 tab:表格列出**全部会话 tab(系统内置 + 策略分组)混排**,内置行带「内置」徽标、策略行带「策略」徽标;每行 = 拖动手柄(原生 HTML5 Drag & Drop+ 名称 + 显示/隐藏开关;**任何行均无重命名/删除按钮**;拖动落点确认后**立即持久化**(每次 drop 提交整表,沿用「刷新页面后生效」提示机制);**落点指示为行间间隙高亮线**(迭代 21,与列设置弹层一致,见 UI约束-007);「策略分组」子 tab 已移除(R-027 策略静态化),设置页不再有任何策略增删改名入口 | 2026-09-02 | 生效 | - | R-011 定稿(2026-09-02 Q1/Q4/Q5 确认):拖动排序 + 显隐开关,禁重命名/删除;变更(2026-09-10 R-025 老师指令):落点从整行高亮改行间间隙高亮线;**变更(2026-09-10 R-027**:策略 tab 静态化、策略分组子 tab 移除 |
| UI约束-005 | 策略自定义字段配置 UIR-013,2026-09-02):设置页「策略分组」子 tab 策略行**可展开** → 展开区 = 字段列表(label/key/type/enum/默认值)+ 添加/编辑/删除字段表单(名称/类型/枚举选项/默认值)+ 保存(走 strategies/update 整表);策略持仓 tab 持仓行展开区(R-010 基础上)增加「自定义字段」区块:按该策略 configSchema 渲染输入控件(文本=输入框、数字=数字输入、布尔=开关、枚举=下拉),值来自该行 values,编辑即保存调 holdings/values-updateToast 反馈;未配置字段定义的策略持仓行不渲染字段区 | 2026-09-02 | 生效 | - | 新增(2026-09-02 R-013 定稿 D6 老师确认:值编辑入口 = 持仓行展开区 |
| UI约束-005 | 策略自定义字段配置 UIR-013**R-027 入口迁移 + 现状更正**):**入口** = 策略 tab 顶部「列设置」**旁**的「字段配置」按钮 → 独立弹层(约 520px 宽、70vh 高、内部滚动,锚定弹层视觉同列设置):字段列表(展示名 / key / 类型 / 公式或默认值 / 单位 / 操作)+ 添加/编辑/删除字段表单(名称/类型/枚举选项/默认值/单位)+ 底部 [关闭] + [保存字段](保存走 strategies/schema-update 单策略写入);有未保存改动时关闭需二次确认;**现状更正**:字段**值**不再是「持仓行展开区编辑」,而是**作为持仓表列展示 + 单元格点击内联编辑**(R-013 §4 演进:文本=输入框、数字=数字输入、布尔=开关、枚举=下拉,编辑即保存调 holdings/values-update + Toast);列显隐/顺序由「列设置」弹层管理(每策略独立);未配置字段定义的策略该列不存在 | 2026-09-02 | 生效 | - | 新增(2026-09-02 R-013 定稿 D6);**变更(2026-09-10 R-027/R-026 实施)**:入口由设置页「策略分组」迁至策略 tab 内「字段配置」弹层;同时更正文案到现状(值编辑 = 表格列内联编辑,展开区仅保留交易明细 |
| UI约束-004 | 神之一手 UI 适配 DSH 主题:所有颜色引用宿主 `--dsw-*` token(带 fallback `var(--dsw-alias-xxx, #原色)`),随 DSH 浅色 / 深色 / 跟随系统自动切换,不自行维护主题偏好(跟随宿主);语义映射:表面底→bg-layer-1、次级面→bg-layer-2、hover→interactive-bg-hover、主/次/弱文字→label-primary/secondary/tertiary、边框→border-l1..l4、红(涨/删/错)→state-error-primary、绿(跌/成/激活)→state-success-primary、蓝(信息/业务)→state-business-primary、实心按钮字→button-contrast-fill、遮罩→bg-mask-1、阴影→shadow-lv3;淡底用 color-mix(in srgb, var(--语义色) 10-12%, transparent);禁止硬编码色值(#hex/rgb)进运行代码 | 2026-09-02 | 生效 | - | R-012 定稿(2026-09-02,暂定跟随系统)+ 迭代 10 实施:141 处硬编码色 token 化,宿主机制实证(body[data-ds-dark-theme] + alias token 双值定义) |
| UI约束-006 | 展开/收起箭头全站统一:使用共享组件 `ExpandChevron`src/client/views/ExpandChevron.jsx)——14×14 chevron-right、描边圆角、主题 success 绿、展开时旋转 90°(0.15s 过渡);适用:策略持仓 tab / 交易记录 tab 表格行、设置页「策略分组」行首箭头(原 ▸/▾ 文字箭头废弃);不得再散落内联 SVG 或文字符号实现 | 2026-09-03 | 生效 | - | 新增(2026-09-03 老师反馈:设置页策略分组箭头与策略 tab 表格行展开图标一致;顺带消除两处内联 SVG 重复) |
| UI约束-007 | 列表/弹层排序交互:优先**原生 HTML5 拖拽排序**draggable 行 + ≡ 拖拽手柄 + drop 重排 + 落点立即持久化,行之间允许 ↑↓ 箭头兜底微调);**落点指示用间隙高亮线**(两行之间的主色线,指示精确插入位置/分清前与后,不做整行高亮)——策略 tab 列设置弹层(R-024)与设置页 Tab 设置(R-025)均已采用;不做表头直拖列排序(兼容成本高,需处理排序点击/固定列冲突) | 2026-09-10 | 生效 | - | 新增(2026-09-10 R-024 定稿:策略 tab 列设置弹层从 ↑↓ 升级为拖拽排序,复用 Tab 设置已验证模式,去掉「保存」按钮改立即持久化);变更(2026-09-10 老师反馈:整行高亮无法判断插入前/后,落点改行间间隙高亮线);变更(2026-09-10 R-025 老师指令:设置页「Tab 设置」落点统一为同款间隙线,整行高亮全站废止) |
| UI约束-008 | 计算字段 UI(R-026):字段类型下拉含「计算」——选中后**隐藏默认值**,显示公式输入框(等宽字体)+ 变量**标签平铺**(行情 / 合约 / 持仓 / 自定义字段 分组成行,每组 = 灰色组名 + 胶囊形 tag,点选即插入光标处;不自弹下拉)+「试算」按钮(用该策略一行真实持仓数据算出结果行内展示:`试算:平安银行 000001.SZ → 6.67 %`;无持仓提示「暂无可试算的持仓数据」)+ **结果类型下拉(数字 / 判定,R-028)**:选「判定」时**隐藏小数位**、公式框 placeholder 切换为判定示例(如 `涨停价 > 基准值 + 网格大小`),公式框下方常驻运算符提示(`运算:+ - × ÷ 括号;判定:> < >= <= == != ,组合:and / or`+ 判定型试算显示 `→ ✓ 满足 / — 不满足`;数字型仍为小数位输入(0-4,默认 2);保存失败时公式框**描红**(state-error)+ 显示服务端提示,表单不关闭、改动保留;持仓表中计算列**只读**:表头列名前缀 `ƒ`tertiary 色,title="计算字段(只读)")、列头与单元格 title 显示公式原文(`= <公式>`)、**判定型(R-028)**:满足 → `✓ <字段展示名>`(success 绿),不满足或数据缺失 → `—`(tertiary 灰,两者同形以免把「缺数据」误读为「不可以」);数字型按小数位 + 单位格式化(如 `6.67 %``700.00 元`)、缺数据/计算失败显示 `—`(tertiary 色)、点击不进内联编辑;历史持仓行显示 `—`;列设置弹层中该列名同样带 `ƒ` 前缀 | 2026-09-10 | 生效 | - | 新增(2026-09-10 R-026 定稿 + D-1~D-7 老师拍板:中文变量名 + 选择器插入、表头 ƒ 前缀 + 悬停看公式、本期做试算、已删变量 ⚠ 标记、小数位 0-4 默认 2、不做实时高亮);**变更(2026-09-10 R-028)**:新增「结果类型(数字/判定)」下拉与判定列渲染(`✓ 字段名` / `—`+ 判定试算;**变更(2026-09-10 老师反馈)**:变量选择从「+ 插入变量 ▾」下拉改为**标签平铺**(分组成行、点选插入),弃用下拉交互;判定型隐藏小数位 |
<!-- 示例条目(确认格式后删除):
| UI约束-001 | 示例:求签页面必须保持单屏完整,不出现滚动 | 2026-08-26 | 生效 | - | 讨论确认:移动端优先,避免滚动打断仪式感 |
+7 -4
View File
@@ -11,17 +11,20 @@
|---|---|---|---|---|---|
| 产品约束-001 | 分仓管理以「全量持仓」为数据基础:系统必须完整展示账户全部持仓信息(代码/名称/数量/可用/均价/现价/市值/盈亏),不遗漏、不裁剪 | 2026-08-27 | 生效 | - | 讨论确认(R-002):老师要求「首先肯定支持全部持仓信息」 |
| 产品约束-002 | 分仓通过「持仓标签」体系实现:在全量持仓之上,用不同标签对持仓进行逻辑分组,每个标签展示其下的仓位汇总(数量/市值/盈亏/占比) | 2026-08-27 | 生效 | - | 讨论确认(R-002):老师要求「分不同的持仓标签,各自有多少仓位」 |
| 产品约束-004 | 删除持仓策略时,该策略下已分配的份额自动回到「未分配」,数据不丢失(删除前需弹窗确认) | 2026-08-28 | 生效 | - | R-003 O3 定稿(2026-08-28:删除策略 = 份额回未分配 + 弹窗确认 |
| 产品约束-003 | 持仓标签可自定义、可增删改;标签维度候选包括策略/用途/风险等级等(具体维度待讨论确认) | 2026-08-27 | 生效 | - | 讨论确认(R-002):标签体系需支持自定义,维度待细化 |
| 产品约束-004 | 删除持仓策略时,该策略下已分配的份额自动回到「未分配」,数据不丢失(删除前需弹窗确认) | 2026-08-28 | **失效** | 2026-09-10 | **失效(2026-09-10 R-027 策略静态化)**:策略不可删除,本条款无适用场景,随之退役(原定稿:删除策略 = 份额回未分配 + 弹窗确认 |
| 产品约束-003 | 持仓标签(策略)**集合与名称固定**为插件内置两项(网格超市 / 手动做T):不可新增、不可删除、不可重命名(R-027 策略静态化);「可自定义」收窄为**标签下的字段定义可自定义**(见 产品约束-010 / 产品约束-014);策略 tab 仍支持显隐与拖拽排序(见 产品约束-009) | 2026-08-27 | 生效 | - | **变更(2026-09-10 R-027**:原「标签可自定义、可增删改」(维度候选策略/用途/风险等级)收窄为「固定两标签 + 字段自定义」;老师重构指令「策略 tab 做成静态的,和全部持仓、交易记录一样是插件内置 tab」 |
| 产品约束-005 | QMT 连接配置多配置管理:设置页「QMT 连接配置」子 tab 维护多个连接配置(名称 + HTTP 地址,增/删/改/查);一次只能激活一个,激活配置为当前生效连接;默认 = 插件启动时自动激活的配置(运行中手动激活其他配置立即生效,重启回到默认);测试连接为手动操作(请求 /health 返回可达性与延迟,激活不强制先测);删除边界:删除激活配置自动切换到默认,删除默认配置则默认标记转移到列表第一条并激活之,禁止删除最后一条 | 2026-08-29 | 生效 | - | R-004 定稿(2026-08-29):Q1-Q6/Q8 逐条确认(不迁移宿主配置、超时不纳入表单) |
| 产品约束-007 | 3 个监控表格(全部持仓 / 手动做T / 网格超市)展示**盘中现价**:现价列显示 lastPrice,红涨绿跌着色(对比昨收),价格变化轻微高亮;本期仅现价一项实时数据 | 2026-08-31 | 生效 | - | R-005 定稿(2026-08-31):老师确认现价列 + 涨跌色 + 变化高亮 |
| 产品约束-006 | QMT 连接会话头部快捷切换:会话窗口顶栏(PTC 模式标签旁)常驻下拉控件(chip 显示 `QMT: <激活配置名>`),点开列出全部配置(激活项勾选),点选即激活并轻提示反馈;头部控件仅做切换,配置管理(增删改/测试连接/默认标记)仍在设置页子 tab | 2026-08-29 | 生效 | - | R-004 定稿(2026-08-29):Q9/Q10 确认(入口=会话头部 PTC 旁,菜单仅切换激活) |
| 产品约束-008 | 交易记录支持**按策略过滤**:交易记录 tab 提供策略过滤下拉(全部 / 各策略 / 未关联),对今日(QMT 实时)与历史(本地 SQLite)均生效;策略归属 = 委托时间 join 持仓生命周期窗口推导(一码多策略取份额最大,未命中=未关联);历史范围展示本地积累数据(不再「接口开发中」占位) | 2026-09-01 | 生效 | - | 新增(2026-09-01 R-009 定稿 + 迭代 07 实施):策略过滤 + 历史本地展示 |
| 产品约束-010 | 策略自定义字段配置:每个策略可在设置页「策略分组」子 tab 配置自定义字段定义(字段名 / 类型文本·数字·布尔·枚举 / 枚举选项 / 默认值),定义随策略存 settings(不落库);该策略下每个持仓(strategy_holdings 行)按所属策略的定义存取一份字段值(values JSON 列,持仓级键值对,key 对齐定义、允许扩展额外键);旧策略无定义时行为与现状一致(不渲染字段区、不迁移历史值) | 2026-09-02 | 生效 | - | 新增2026-09-02 R-013 定稿 Q1-Q4 + D6):定义随策略走、值随持仓行,类型化(含枚举),旧策略兼容 |
| 产品约束-009 | 会话 tab 统一由「Tab 设置」管理:设置页「Tab 设置」子 tab 是**所有会话 tab(系统内置 + 策略分组)的唯一顺序与显隐入口**,两类 tab 混排;每行 = 拖动排序 + 显示/隐藏开关;**任何 tab 均不支持重命名与删除**(内置 tab 名称只读,策略命名/删除仍在「策略分组」子 tab);策略改名后 Tab 设置中的名称自动跟随(只存引用);新增策略默认追加到列表末尾;删除策略联动删除 Tab 设置中对应条目;顺序与显隐唯一数据源 = settings.tabs 有序数组 | 2026-09-02 | 生效 | - | R-011 定稿2026-09-02 Q1-Q5 确认):Tab 设置 = 显示/隐藏 + 拖动排序(落点立即持久化),全 tab 禁重命名/删除 |
| 产品约束-010 | 策略自定义字段配置:每个策略在**策略 tab 顶部「列设置」旁的「字段配置」按钮(独立弹层)**中配置本策略的字段定义(字段名 / 类型文本·数字·布尔·枚举·**计算** / 枚举选项 / 默认值 / 单位 / 计算字段的公式与小数位),定义存 settings(不落库);该策略下每个持仓(strategy_holdings 行)按所属策略的定义存取一份字段值(values JSON 列,持仓级键值对,key 对齐定义、允许扩展额外键);旧策略无定义时行为与现状一致(不渲染字段区、不迁移历史值) | 2026-09-02 | 生效 | - | **变更2026-09-10 R-027/R-026**:入口由设置页「策略分组」迁到策略 tab 内「字段配置」弹层;类型新增「计算」(见 产品约束-014);定义真相源迁 settings.strategyFields(原定稿:设置页策略分组配置、四类型、定义随策略存 settings) |
| 产品约束-009 | 会话 tab 统一由「Tab 设置」管理:设置页「Tab 设置」子 tab 是**所有会话 tab(系统内置 + 策略)的唯一顺序与显隐入口**,两类 tab 同权混排;每行 = 拖动排序(间隙高亮线)+ 显示/隐藏开关;**任何 tab 均不支持重命名与删除**;R-027 静态化后策略 tab 恒存在(不随任何 CRUD 增删),故「策略改名跟随 / 新增追加末尾 / 删除联动」等条款失效;顺序与显隐唯一数据源 = settings.tabs(由内置常量序列 + 存量偏好归一化得出) | 2026-09-02 | 生效 | - | **变更2026-09-10 R-027 策略静态化)**:策略 tab 与内置 tab 完全同权且恒存在;原「策略命名/删除在策略分组子 tab」及联动增删条款失效(设置页「策略分组」子 tab 已移除) |
| 产品约束-011 | 持仓页数据(策略持仓 / 全部持仓 / 未分配)以服务端 10s 内存快照为准,**接受最多 10s 滞后**(同步时间前端不显示,老师拍板);QMT 抖动/掉线时持仓页面显示**最后一次快照**而非空白;**幽灵自动清仓退役(变更 1,2026-09-08 R-018 数据域分界)**:同步机制不再把本地策略持仓自动转历史——策略持仓份额只由「交易关联(归属=账本写操作)」与「手动份额操作」驱动,账本转历史唯一途径 = 卖出单关联份额减至 0;对账单码消失仅表现为快照无此行 + **漏关联软提示**(只读提示去关联/移出,不自动写账本);部分减持仅表现为「未分配为负」,不做自动修正 | 2026-09-02 | 生效 | - | 新增(2026-09-02 R-014 定稿);**变更 12026-09-08 R-018/迭代 16 拍板)**:幽灵自动清仓退役(同步机制不写账本),策略持仓由交易关联 + 手动份额操作驱动;漏关联软提示替代自动归档 |
| 产品约束-012 | 行情数据服务形态(R-015):现价/昨收/涨停/跌停统一由盘口内存快照提供(≤5s 更新),价格单一入口;QMT 抖动时页面价格保持旧值不空白;会话头部(QMT 健康灯旁)提供**持仓/盘口同步指示灯**——绿=同步正常(持仓 30s/盘口 15s 内)、黄=同步失败中快照陈旧、灰=从未同步;悬停显示同步时间/快照量/失败数/错误摘要;点击灯 = 立即触发该域同步;不做个股停牌标识(另议) | 2026-09-02 | 生效 | - | 新增(2026-09-02 R-015 定稿):老师提出指示灯,采纳 AI 推荐三态/点击即同步/取消 PriceCell 灰点 |
| 产品约束-013 | 策略 tab 静态化(R-027):会话 tab 中的策略 tab 恒为**插件内置两项**(网格超市 / 手动做T),与「全部持仓 / 交易记录」同类的内置 tab —— **不可新增、不可删除、不可重命名**;仍支持 Tab 设置中的显隐与拖拽排序;设置页不再有「策略分组」子 tab(设置页 = Tab 设置 / QMT 连接配置);策略字段配置入口 = 策略 tab 顶部「列设置」**旁**的「字段配置」按钮(独立弹层),每个策略 tab 各管本策略字段 | 2026-09-10 | 生效 | - | 新增(2026-09-10 R-027 定稿 Q1-Q4/Q6 老师拍板):静态化程度=固定不可增删改名但保留显隐/排序;入口形态=列设置旁独立按钮;范围=每策略管自己的字段 |
| 产品约束-014 | 策略计算字段(R-026):字段类型新增「计算(formula)」——用户在字段配置中写**公式**(**中文变量名**,如 `(现价 - 成本价) × 份额`),公式引用**服务端缓存数据集**(行情盘口 / 合约信息(涨停跌停)/ 持仓账本(份额·成本价·最后成交价)/ 本行手填字段值),由服务端按**持仓行实时计算**得出列值;计算字段**不落库**(不写 values 列)、在持仓表中**只读**(不可内联编辑);变量**只能引用非计算字段**(零循环依赖);缺数据 / 除零 / 无法计算 → 该格显示 `—`(不显示 NaN、不报错、不炸表);历史持仓行显示 `—`(不做历史时点计算);**结果类型两种(R-028 扩展)**:`resultKind = number | boolean` —— **数字**(默认,按小数位+单位格式化)与**判定**(比较运算结果:满足显示 `✓ <字段名>`(success 绿)、不满足或数据缺失显示 `—`),结果类型在字段表单**显式声明**且必须与公式形态一致(服务端校验);公式能力边界 = 四则运算 + 括号 + 比较(`> < >= <= == !=`+ 逻辑组合(`and / or`,含 `&& ||` 与全角 `≥ ≤ ≠`+ round/abs/min/max,禁止任意代码执行 | 2026-09-10 | 生效 | - | 新增(2026-09-10 R-026 定稿 Q1-Q3 + Q4-Q10 + D-1~D-7 老师拍板):数据集首批三组、中文变量名、仅行级、服务端现算、只读列、缺值 —;**变更(2026-09-10 R-028)**:原「首批结果只做数字」扩展为「数字 / 判定」双类型 + 逻辑组合,首个落地用例 = 网格超市「可下空单 / 可下多单」 |
<!-- 示例条目(确认格式后删除):
| 产品约束-001 | 示例:求签功能必须保证抽取结果的不可预测性 | 2026-08-26 | 生效 | - | 讨论确认:为保证公平性,抽取必须不可预测 |
-->
+8 -2
View File
@@ -23,13 +23,19 @@
| 技术约束-009 | 会话头部快捷切换控件挂载 DSH 开放 slot `conversation.session.header.actions`(多实例挂载点,按 order 排序多插件共存):客户端插件以独立 id 并排注册(DSH 内置 PTC 标签 order=-10,本控件 order=-9),不改动 DSH 宿主;控件经 ConnectionProvider 包装复用现有 RPC 通道与 /odl/api/* 端点 | 2026-08-29 | 生效 | - | R-004 定稿(2026-08-29):Q9 确认;宿主代码审查核实 slot 机制与内置插件注册方式 |
| 技术约束-013 | 交易记录本地存储(R-009):QMT 当日交易数据(委托/成交)由服务端 TradeSync 定时同步落 SQLite(启动预热 + 60s 定时 + UPSERT 幂等,只同步当日);trade_orders(委托主行,order_id 主键 + insert_ts 派生时间列 + **strategy_id/holding_id 手动归属列**+ trade_fills(成交明细,trade_id 主键、order_id 外键)两表;**委托归属由用户在交易记录 tab 手动设置**(候选 = 该 code 当前持仓策略 + 未关联,全手动选、可随时改、以最终为准);**UPSERT 不覆盖归属列**(手动指定为插件逻辑);本地历史查询走 trades/history 端点(策略过滤 = 用户设置的归属);今日实时仍走 QMT Bridge;QMT 委托/成交 code 无后缀、持仓带后缀 —— 数据源映射层统一 normalizeInstrumentCode 归一化;委托交易日 = insertDatetradeDate 兜底) | 2026-09-01 | 生效 | - | 新增(2026-09-01 R-009 定稿 + 迭代 07 实施):两表 + 定时同步 + 本地历史查询;变更 12026-09-01):+code 归一化 + tradeDate 兜底;变更 22026-09-01 老师二次定稿):归属改**手动设置**trade_orders 冗余 strategy_id+holding_idUPSERT 不覆盖归属列),弃算法推导 |
| 技术约束-015 | 策略自定义字段存储(R-013,2026-09-02):字段定义随策略定义存 settings.strategies 扩展 configSchema[{key,label,type,enum?,def}]type ∈ text|number|boolean|enum,旧项缺省空数组);字段值落 strategy_holdings 新增 values TEXTJSON 键值对,key 对齐 configSchema.key,允许额外键=可扩展,NULL=未配置);补列用幂等 ALTER(沿用 _ensureTradeAttributionColumns 模式,只读连接容忍);APIstrategy-positions 每行附 values,新增 holdings/values-update {holdingId, values} 写回,服务端按 configSchema 校验(number=有限数、enum=在选项内、boolean=布尔),空值/缺省可写入;持仓生命周期操作(openHolding/addShares/reduceShares/closeHolding)不碰 values 列 | 2026-09-02 | 生效 | - | 新增(2026-09-02 R-013 定稿 + PLAN-012):定义 settings + 值 SQLite 列 + 幂等补列 + 类型校验 |
| 技术约束-014 | 会话 tab 注册与顺序显隐(R-011):客户端注册统一读 **settings.tabs 有序数组**(唯一顺序与显隐来源,内置条目 refKey + 策略条目 refId),按 order 排序、过滤 visible 后注册(builtin 走内置 render、strategy 走 StrategyTab,移除硬编码 order 间隔(原内置 10/11/12、策略 13+);settings.tabs 从布尔对象升级为有序数组,读取时对旧格式(布尔对象 + 策略自带 order/visible)静默归一化迁移(旧隐藏策略迁移后显示),写入即落库;策略定义表收窄为 {id,name}(去除 visible/order);tabs/update 语义改为整表更新(顺序 + 显隐),strategies/add 联动追加 tab 条目(末尾),strategies/remove 联动删除对应 tab 条目,废弃 strategies/move | 2026-09-02 | 生效 | - | R-011 定稿(2026-09-02 Q1-Q5 确认):统一 tabs 有序数组 + 自动迁移 + 联动增删 |
| 技术约束-015 | 策略自定义字段存储(R-013**R-027 真相源迁移**):字段定义存 **settings.strategyFields**`{ [strategyId]: [{ key, label, type, enum?, def?, unit?, formula?, decimals? }] }`type ∈ text|number|boolean|enum|**formula**);`settings.strategies` 收窄为**内置身份表**(仅兼容读取旧 `configSchema`strategyFields 缺失时按内置 id 回填视图,首次保存即落新键,无迁移脚本、不动库数据);写路径唯一 = `strategies/schema-update { strategyId, configSchema }`(单策略整份覆盖,未知 strategyId 抛 strategy-not-found,校验见 技术约束-024);`normalizeFields` 归一化(类型白名单回退 text、decimals 夹取 0-4、formula 字段不带 def);字段值落 strategy_holdings 新增 values TEXTJSON 键值对,key 对齐 configSchema.key,允许额外键=可扩展,NULL=未配置);补列用幂等 ALTER(沿用 _ensureTradeAttributionColumns 模式,只读连接容忍);APIstrategy-positions 每行附 values,新增 holdings/values-update {holdingId, values} 写回,服务端按 configSchema 校验(number=有限数、enum=在选项内、boolean=布尔),空值/缺省可写入;持仓生命周期操作(openHolding/addShares/reduceShares/closeHolding)不碰 values 列 | 2026-09-02 | 生效 | - | 新增(2026-09-02 R-013 定稿 + PLAN-012):定义 settings + 值 SQLite 列 + 幂等补列 + 类型校验**变更(2026-09-10 R-027/R-026**:定义真相源迁 strategyFieldsstrategies 只读兼容)、类型白名单加计算类型、写路径改单策略端点 |
| 技术约束-014 | 会话 tab 注册与顺序显隐(R-011**R-027 静态化**):客户端注册统一读 **settings.tabs**(唯一顺序与显隐来源,内置条目 refKey + 策略条目 refId),按 order 排序、过滤 visible 后注册(builtin 走内置 render、strategy 走 StrategyTab**R-027 后**tab 序列由**内置常量派生**(3 内置 tab + 2 内置策略 tab),`normalizeTabs` 只保留常量表内条目的 visible/order 偏好、**丢弃**未知条目(自建策略 tab / 已退役 tab)、补齐缺失条目;`updateTabs` 只接受内置 id(未知条目过滤),不再有联动增删(`appendStrategyTab/removeStrategyTab` 退役);旧布尔对象格式(迭代 02)继续兼容 | 2026-09-02 | 生效 | - | R-011 定稿(2026-09-02 Q1-Q5 确认):统一 tabs 有序数组 + 自动迁移 + 联动增删**变更(2026-09-10 R-027**:策略静态化,tabs 由常量派生、联动增删与 strategies CRUD 一并退役 |
| 技术约束-016 | src 目录按功能域归类(2026-09-02 结构优化):服务端代码**禁止平铺**,按职责域分目录 —— src/data-source/QmtBridgeRestDataSource + data-source-types + QmtHealthMonitor,数据源与连接健康)、src/storage/SqliteStore + DataStore,存储层)、src/position/PositionManager,分仓逻辑)、src/market/MarketDataHub + MarketFeed,行情)、src/trades/TradeSync,交易同步);api/ 按领域拆分子文件(positions/strategies/qmt-connections/market/trades),client/ 仅放 UIviews/ 组件 + market/ provider);文件命名 = 类名(PascalCase+ .js/.jsx;新增服务端模块必须先落对应域目录,无合适域时先讨论补域,不得回退平铺 | 2026-09-02 | 生效 | - | 新增(2026-09-02 结构审查 + 优化落地):component/ 平铺还原为语义分域,删除死代码 AllocationStorage、DataStore.setDataset/removeDataset |
| 技术约束-017 | 持仓内存快照(R-0142026-09-02**变更 12026-09-08 R-018 数据域分界**):全量实盘持仓由服务端 PositionSync **进程内内存快照**管理(启动预热 + 10s 定时全量拉 /trade/positions → 校验 → 整体替换),**不落库**(账本与对账单分离,holding_id 交易锚点不掺易变快照);PositionManager.getAllPositions 以快照为准(strategy-positions / unallocated / summary 三接口不再请求时穿透 QMT),快照为空读穿透兜底;同步失败保留上次快照;空快照双重确认(/health 可用 + getAsset 账户身份可识别)才接受为真清仓;syncNow 允许手动调用。**幽灵自动清仓**(R-014 原条款:快照连续 3 轮消失 → 本地全部策略当前持仓 closeHolding 转历史)**退役**——同步机制只作用于对账单域,不再写 strategy_holdings 账本(R-018 老师拍板:幽灵清仓本就是一个同步机制,不可以让幽灵把爪子伸太长);账本行转历史唯一途径 = 卖出单关联减至 0 closeHolding(R-018);对账单域「码消失」仅表现为快照无此行 + 漏关联软提示(positions/orphan-hints 只读提示) | 2026-09-02 | 生效 | - | 新增(2026-09-02 R-014 定稿 + 迭代 12 实施);**变更 12026-09-08 R-018/迭代 16 拍板)**:幽灵自动清仓退役(不再 closeHolding 本地账本),PositionSync 只同步对账单快照;漏关联由只读软提示承担 |
| 技术约束-018 | 盘口内存快照(R-0152026-09-02):行情数据由 QuoteSync(取数:启动 prime + 5s REST 定时刷 watch 集合 + 涨停跌停经 /data/instrument 按**交易日**内存缓存)与 QuoteHub(存查:内存快照 Map + watchCodes Set + 读穿透走 dataSource.getTicks + getQuote/getByCodes 对外)管理,**替换并删除 MarketFeed/MarketDataHub**(方案 A,不留兼容壳);**WS 数据通路移除**(ingest 入口带 source 标签留回归口子);**market_quotes_cache 表退役 DROP**(幂等),价格单一入口 = QuoteHubDataStore 行情方法(loadMarket/getMarketQuote(s)/setMarketQuotes)删除;同步失败保留内存旧值;watchCodes 维持只进不出无上限;涨停/跌停/昨收不落库 | 2026-09-02 | 生效 | - | 新增(2026-09-02 R-015 定稿 + 迭代 13 实施):老师五拍板(替换/WS 移除/纯内存 DROP/watch 现状/指示灯);warmup bug 复现(loadMarket return this 残迹)为不落库关键证据 |
| 技术约束-019 | 历史持仓查询(R-0162026-09-07):策略 tab 历史持仓展示走**本地库只读查询**SqliteStore.getHoldingsHistorystrategy_id + closed_at IS NOT NULL + closed_at ≥ sinceMsclosed_at DESC),经独立端点 strategy-holdings/history 暴露;**当前持仓路径(strategy-positions / PositionSync 快照语义)不掺历史数据**(两份结果前端合并渲染);closeHolding 置 shares=0 语义维持不变(Q2 老师拍板:历史行份额显示 0,重点在追溯该持仓的历史操作而非清仓时份额);范围换算服务端做(week=7d/month=30d/quarter=90d/halfYear=182d/year=365d 自然日近似)——**变更 12026-09-07 二轮补充 Q8-Q9 老师拍板)**range 新增 'today' = **本地自然日 00:00 起**(特判零点,不落回溯毫秒档),供前端「今日已清仓默认层」(恒显示、不受历史持仓开关控制);历史范围层语义收窄为**今天之前**;其余不变 | 2026-09-07 | 生效 | - | 变更 12026-09-07 R-016 二轮补充 Q8-Q9 老师拍板):+range=today 自然日边界,历史开关只控今天之前;首轮新增(Q1-Q7):历史行=追溯操作锚点,不动存储写路径 |
| 技术约束-020 | QMT 连接健康自适应探测(R-0222026-09-09):QmtHealthMonitor 探测节奏由固定 5 分钟改为**结果驱动自适应**——健康 → intervalMs(默认 5 分钟,稳态低开销);异常/未知(含从未探测成功或 baseUrl 解析失败)→ retryMs(默认 10 秒)快速重试,与前端 sync-status 10s 轮询对齐,QMT Bridge 启动/恢复后指示灯 ≤20s 自动回绿(原固定 5 分钟导致恢复感知滞后,老师反馈「自动检查没生效」);间隔经构造参数 { intervalMs, retryMs } 注入(测试用短间隔);setTimeout 自适应链 + _inFlight 防并发,手动探测/配置热切换共用 probe() 并重排下轮节奏;stop() 即停;resolveActiveBaseUrl 失败也落 healthy:false 缓存走快速重试;前端读缓存秒回与 sync-status/qmt-health 端点语义不变 | 2026-09-09 | 生效 | - | 新增(2026-09-09 R-022 讨论 + 迭代 19 实施):健康灯恢复感知修复;稳态间隔维持原 5 分钟设计不变 |
| 技术约束-021 | MCP 状态自适应探测(R-0232026-09-09):QmtMcpManager 状态缓存增加**结果驱动自适应探测**(原无定时回路,只在挂载/切换/手动探测时刷新)——connected → intervalMs(默认 5 分钟,稳态低开销);非 connected → retryMs(默认 10 秒)快速重试,MCP 服务(激活连接 baseUrl + /mcp)恢复后状态灯 ≤20s 自动回绿;dsh-mcp-client 自带 reconnect 只恢复真实连接、不刷新状态缓存,本类缓存必须自带刷新回路;无激活连接(url 为空)不探测不调度,dispose()/unmount() 停止调度;探测走 SDK 独立只读握手(Client connect + initialize + tools/list),与 dsh-mcp-client 自管连接互不干扰;间隔经构造参数 { intervalMs, retryMs } 注入(测试用短间隔);_probing 防并发与手动/热切换探测共用 probe() 重排节奏;前端 getStatus 读缓存与 mcp-status/sync-status 端点语义不变 | 2026-09-09 | 生效 | - | 新增(2026-09-09 R-023 讨论 + 迭代 19 实施):MCP 状态灯恢复感知修复(与 R-022 同模式);挂载/重连逻辑零改动 |
| 技术约束-022 | 公式引擎与变量目录(R-0262026-09-10):`src/formula/evaluator.js` **自写小型表达式引擎**(零依赖,**禁止 `eval/Function`**):词法支持 Unicode 标识符(**中文变量名**)与全角归一(()+-×÷),语法 = 四则 + 括号 + 一元负 + 函数 round/abs/min/max(递归下降 + 深度≤32 + 长度≤500 上限);**R-028 扩展**:比较 `> < >= <= == !=` 与逻辑 `and / or`(含 `&& ||`、全角 `≥ ≤ ≠ `),优先级 `or < and < 比较 < 加减 < 乘除`,判定结果缺值短路;新增 `inferResultKind(ast)` 静态推断结果类型(顶层比较/逻辑 → boolean,`neg` 下钻);`validateFormula(expr, allowedVars)` 返回 { ok, vars } 或 { ok:false, error:{ code: empty|syntax|unknown-var|unknown-func|arity, message, position } };求值语义 = **缺值短路**(任一变量 null/undefined/NaN → 整式 null)、除零/非有限 → null、**最终结果浮点归一**|v|<1e12 时四舍五入 10 位,消除 0.7000000000000002 类噪声,中间步骤不归一再保精度);`src/formula/variables.js` 为**变量目录单一入口**(行情 7 项 / 合约 2 项 / 持仓账本 3 项 + 运行时按策略非 formula 字段展示名展开「自定义字段」组),`allowedVarNames` 排除 formula 字段(零循环依赖) | 2026-09-10 | 生效 | - | 新增(2026-09-10 R-026 定稿 + 技术方案 §2.2/§2.3):中文标识符需自有词法(第三方库均不支持中文变量),该层即 T-006 数据池的表达式层;**变更(2026-09-10 R-028**:新增比较与逻辑运算 + `inferResultKind`(判定型计算字段);**复核(2026-09-10 老师提问「有没有现成的库」)**:对比 expr-evalCVE-2025-12735 变量对象注入 → RCE)、mathjs(标识符不含 CJK)、Jexl(ASCII 标识符)、CEL、解析器工具包后**维持自写引擎**,论证见 迭代 23 技术方案 §4.5 |
| 技术约束-023 | 计算字段现算口径(R-0262026-09-10):`src/formula/FormulaService.js#computeRows`**API 层**为 `strategy-positions` 返回行附加 `computed: { [fieldKey]: number|null }`(不改 PositionManager 份额语义);取数**只读 QuoteHub 内存缓存**`quotes` / `instruments`,与 api/market.js#projectQuote 同口径),**不做读穿透**(高频读路径零网络;miss → 相关变量 null → 该格 `—`,等 QuoteSync 下轮 ≤5s 覆盖);**无 formula 字段的策略直接返回原数组(零开销)**;值**不落库**(公式存 settings,值每次现算);`strategy-holdings/history` 不计算(历史行前端显示 `—` | 2026-09-10 | 生效 | - | 新增(2026-09-10 R-026 技术方案 §2.4):计算位置与缓存口径由 AI 定并写入方案,老师验收覆盖缺数据边界 |
| 技术约束-024 | 字段定义 API 与校验(R-027/R-0262026-09-10):写路径唯一 `strategies/schema-update { strategyId, configSchema }`(返回该策略对象);校验顺序 = 结构(展示名/key 非空、类型白名单、枚举选项非空)→ 唯一性(key 唯一、展示名唯一)→ **内置变量名冲突**(字段展示名不得等于现价/份额/…等内置变量名)→ 公式(`validateFormula`,变量集合 = 内置名 ∪ 本策略非 formula 字段展示名);失败统一抛 `code='field-validation'`(前端 Toast + 公式框描红);`formula/variables { strategyId? }` 提供变量目录、`formula/trial { strategyId, formula }` 试算(取份额>0 首行,返回 { code, name, value, reason: no-holding|no-data|null });**退役端点** `strategies/add|remove|update`(调用返回 not-found | 2026-09-10 | 生效 | - | 新增(2026-09-10 R-027/R-026 定稿 + 技术方案 §1.2/§2.5):单策略写入避开整表覆盖,校验规则服务端唯一;**变更(2026-09-10 R-028)**:公式字段增加「结果类型一致性」校验——`resultKind`(默认 number)必须等于 `inferResultKind(compileFormula(formula).ast)`,不一致抛 `field-validation`(双向提示)|
<!-- 示例条目(确认格式后删除):
| 技术约束-001 | 示例:技术栈以 Node.js / TypeScript 为准,不引入未讨论的新框架 | 2026-08-26 | 生效 | - | 讨论确认:优先复用 DSH 既有能力,新框架需论证 |
-->
@@ -42,3 +42,5 @@
1. **部分减持的自动修正**(未分配负数仍靠 UI 暴露,人工「移出」修正):如需「检测账实不符 → 提示一键按实盘修正」可另立需求;
2. **同步时间前端显示**:老师拍板本轮不做;如持仓滞后感知需要,可加「更新于 HH:mm:ss」小标注(后端 syncedAt 已就绪);
3. **既有观察点未动**(另行登记评估):getStrategyPositions 的 N+1 委托查询(可改 IN 一次查);src/api/trades.js orders 端点绕 DataStore 门面直摸 sqlite.db(层级破洞);calcOrderFees 数据源/前端双实现(可收敛);数据存储设计.md §9.1 库文件名(one-divine-lot.db vs 实际 store.db)等历史偏差。
## 验收状态更新(2026-09-08
> 老师 2026-09-08 归档指令:确认验收并归档;R-014 已按归档清单移入 05-需求池/已完成/。
@@ -39,3 +39,5 @@
2. **WS 回归路径**ingest 已留 source 标签;T-005 草稿维持起草,需要亚秒级行情时再议订阅契约;
3. **指示灯扩展位**sync-status 已按域结构化(position/quote/qmt),未来交易域(TradeSync 60s)可加第三盏灯,前端加一行渲染;
4. **前端 getQuote 消费扩展**:涨停/跌停已随 market-snapshot 下发,UI 尚无展示列(如需「接近涨停提示」另立需求)。
## 验收状态更新(2026-09-08
> 老师 2026-09-08 归档指令:确认验收并归档;R-015 已按归档清单移入 05-需求池/已完成/。
@@ -36,3 +36,5 @@
2. **历史行成本价/收益推导**:本期不做(边界定稿);若后续要做,数据源 = 关联委托成交记录,属展示层推导,不动存储;
3. **清仓方式标记**(手动 vs 幽灵自动):数据未区分,本期不做;若要做需 closeHolding 增加来源标记(涉及写路径,另立需求);
4. **R-015 / 迭代 13 人工验收**仍待老师确认(与本迭代无依赖)。
## 验收状态更新(2026-09-08
> 老师 2026-09-08 归档指令:确认验收并归档;R-016 已按归档清单移入 05-需求池/已完成/。
@@ -32,3 +32,5 @@
1. **候选窗口(7 天)与 R-016 范围档是两套口径**:当前均满足场景;若老师后续要统一(候选也用 5 档范围),另议小改;
2. **自动归属推导**_resolveStrategyAttribution 时间窗)仍在(技术约束-013),但 Q3 全手动原则下仅为内部能力,未暴露 UI;维持现状;
3. R-015 / 迭代 13、R-016 / 迭代 14 的人工验收仍待老师确认。
## 验收状态更新(2026-09-08
> 功能被 R-018(迭代 16)语义取代退役,按取代归档(无需独立验收);R-017 已按归档清单移入 05-需求池/已完成/。
@@ -0,0 +1,164 @@
# 迭代 22 UI 交互设计:字段配置入口迁移 + 策略计算字段
> 依据:**R-027(重构·第一阶段,已定稿)** + **R-026(计算字段·第二阶段,已定稿)** + PLAN-019 日期:2026-09-10
> 状态:**已定稿**(D-1D-7 老师逐项拍板;2026-09-10 因入口迁移(R-027)同步修订;D-8~D-9 为 AI 默认项,老师可否决)
> 视觉基线:UI约束-004--dsw-* token)、UI约束-006ExpandChevron)、UI约束-007(间隙高亮线);UI约束-005 待随本次修订
> 复用:ColumnSettingsPopover(列设置)、StrategyFieldsEditor(改造为弹层内容)、ToastFieldCellEditor 不参与(formula 不可编辑)
## 1. 交互总览(一句话)
策略 tab 顶部**「字段配置」+「列设置」**两个按钮:字段配置管**字段定义**(含计算字段的公式),列设置管**列的显隐与顺序**;
字段定义保存后表格列自动跟随;计算字段列**只读**展示服务端实时算出的值(缺数据 `—`)。
> 本次修订要点(R-027):字段配置入口从「设置页 → 策略分组 → 策略行展开」**迁到策略 tab 内**,与列设置并列;设置页不再有「策略分组」子 tab。
## 2. 控件布局(示意)
### 2.1 策略 tab 顶栏
```
┌─ 策略 tab:网格超市 ─────────────────────────────────────────────┐
│ 持仓 13 只 · 今日盈亏 … [历史持仓 ▾] [字段配置] [列设置] │
│ ┌ 持仓表 ─────────────────────────────────────────────────────┐ │
│ │ | | 代码 | 名称 | 现价 | ƒ 盈亏比例 | … | 操作 | │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
- 两按钮同层级、同视觉族(现有「列设置」按钮样式),**字段配置**在左、**列设置**在右(先定义字段、再排列,顺序符合操作动线)。
### 2.2 字段配置弹层(列表 + 表单)
```
┌─ 字段配置:网格超市 ─────────────────────────────────────────┐
│ 自定义字段(5) [+ 添加字段] │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 展示名 key 类型 公式/默认 单位 操作 │ │
│ │ 网格上边界 grid_ceiling 数字 默认 11 元 编辑 删除│
│ │ 基准值 current_base 数字 默认 10 元 编辑 删除│
│ │ ƒ 盈亏比例 pctProfit 计算 (现价 ÷ 成本价 - 1)×100 % 编辑 删除│
│ └──────────────────────────────────────────────────────────┘ │
│ 字段增删后,持仓表列自动跟随(新增列默认追加末尾、删除列自动移除)│
│ [关闭] [保存字段] │
└────────────────────────────────────────────────────────────────┘
```
- 弹层形态(D-9):锚定弹层(与列设置同源视觉),宽度约 520px、最大高度约 70vh、内容超出内部滚动;
- **保存字段**按钮在弹层底部(右对齐,主色实心);**关闭**在左(次按钮);
- 有未保存改动时点「关闭」/ 外点/Esc → 二次确认「有未保存的字段改动,确定放弃?」(D-8)。
### 2.3 字段表单 · 类型 = 计算(第二阶段)
```
┌─ 添加字段 / 编辑字段 ────────────────────────────────────────────┐
│ 展示名 [盈亏比例 ] 标识 [pctProfit ] 类型 [计算 ▾] │
│ 单位 [% ] 小数位 [2] │
│ ┌ 公式 ───────────────────────────────────────────────────────┐ │
│ │ (现价 ÷ 成本价 - 1) × 100 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ [试算] 试算:平安银行 000001.SZ → 6.67 % │
│ 行情 [现价] [昨收] [今开] [最高] [最低] [成交量] [成交额] │
│ 合约 [涨停价] [跌停价] 持仓 [份额] [成本价] [最后成交价] │
│ 自定义 [网格大小] [基准值] [网格交易量] ← 点 tag 插入公式 │
│ 可用变量 行情:现价 昨收 今开 最高 最低 成交量 成交额 │
│ 合约:涨停价 跌停价 │
│ 持仓:份额 成本价 最后成交价 │
│ 自定义:网格大小 基准值 网格交易量 ← 本策略手填字段 │
│ [取消] [确定] │
└─────────────────────────────────────────────────────────────────┘
```
> 其余四类(文本/数字/布尔/枚举)表单形态不变,仅「类型」下拉多一项「计算」;选「计算」时隐藏默认值、显示公式区与小数位。
### 2.4 策略持仓表(只读计算列)
```
| | 代码 | 名称 | 现价 | 成本价 | 份额 | ƒ 盈亏比例 | ƒ 浮动盈亏 | 操作 |
|▸| 000001.SZ | 平安银行 | 11.20 | 10.50 | 1000 | 6.67 % | 700.00 元 | 全部移入/移出 |
```
- 表头 `ƒ`tertiary 色,`title="计算字段(只读)"`)+ 列名;悬停列头或单元格 title 显示公式原文(`= (现价 ÷ 成本价 - 1) × 100`);
- 单元格点击**不进入内联编辑**;手填字段列的行为不变(点击即编辑)。
### 2.5 列设置弹层(ColumnSettingsPopover
- 计算字段与手填字段同列混排(名称显示 `ƒ 盈亏比例`);显隐勾选 / 拖拽排序 / 间隙高亮线 / 立即持久化**零改动**。
## 3. 字段配置弹层状态机
| 状态 | 外观 | 交互 |
|---|---|---|
| 初始 | 列表(含公式原文列)+ 底部「保存字段」(无改动时禁用/次色) | 可添加/编辑/删除字段 |
| 有未保存改动 | 「保存字段」主色可点;关闭类动作触发放弃确认 | 保存 → 调 `strategies/schema-update` |
| 保存中 | 「保存字段」disabled(文案「保存中…」) | 防重入 |
| 保存成功 | Toast「字段已保存」+ 弹层关闭(或保持打开并刷新)+ 表格列同步跟随 | — |
| 保存失败 | Toast 错误 + 弹层不关闭 + 改动保留 | 可重试 |
| 公式校验失败(计算字段) | 表单内公式框**描红** + 错误行(`语法错误:第 8 字符处缺少运算符` / `未知变量「网格距」` | 表单不关闭,光标回公式框 |
| 试算中 | 「试算」disabled(「试算中…」) | 防重入 |
| 试算成功/失败 | 行内 `试算:平安银行 000001.SZ → 6.67 %` / error 色原因(`该策略当前无持仓` | 不阻塞保存 |
## 4. 交互细节
1. **打开入口**:点策略 tab 顶栏「字段配置」→ 弹层打开并显示**本策略**字段定义(各自 tab 管自己,Q4);数据来源沿用 `strategies` 端点(取当前策略 configSchema)。
2. **字段列表**:列 = 展示名 / key / 类型 / 公式或默认值 / 单位 / 操作(编辑、删除);计算字段无默认值,公式列等宽、单行省略、悬停全文。
3. **添加/编辑字段**:沿用现有表单(展示名 → 自动生成 key、类型下拉、枚举选项、默认值、单位)**新增**「计算」分支(见 2.3);`类型` 下拉:文本 / 数字 / 布尔 / 枚举 / **计算**
4. **删除字段**:列表内即时移除(弹层内草稿),**保存**后生效;保存后该列自动消失(归一化跟随),行内残留旧值按 R-013 既有防御隐藏。
5. **保存**:调新增单策略端点 `strategies/schema-update { strategyId, configSchema }`;成功后 Toast + 刷新当前策略数据(列与字段同步);**不写 values 列**、不动份额账本。
6. **变量插入**(计算字段,2026-09-10 老师反馈改为标签平铺):公式框下方按分组(行情 / 合约 / 持仓 / 自定义字段)**平铺变量 tag**(胶囊形、悬停显示释义),点选即插入到公式框光标处(无光标信息则追加末尾并补空格);不自弹下拉菜单。
7. **变量命名规则**(D-1 已拍板):内置变量固定中文名(现价/昨收/今开/最高/最低/成交量/成交额/涨停价/跌停价/份额/成本价/最后成交价);自定义字段以其**展示名**作变量名。
8. **保存校验(服务端)**:① 公式非空且语法合法;② 变量全部存在;③ 同策略字段展示名唯一、且不与内置变量名冲突;④ 仅引用**非公式字段**(目录层排除)。失败 → Toast + 公式框描红 + 定位文案。
9. **试算**:服务端取该策略一行真实持仓(优先份额 > 0 首行)→ 变量目录取数 → 求值 → 返回 {`code`,`name`,`value`};前端按单位/小数位格式化显示。
10. **持仓表只读渲染**:按小数位 + 单位格式化为 `6.67 %` / `700.00 元`;悬停显示公式原文。
11. **刷新语义**(已知取舍):计算列随 `strategy-positions` 轮询刷新(行情服务端 5s 更新,列值刷新粒度 = 接口轮询周期);不与 `PriceCell` 实时高亮联动。
12. **历史持仓行**:计算列显示 `—`(清仓行无「现价」语义,不做历史时点计算)。
13. **旧公式引用已删变量**D-4 已拍板):字段列表给 `⚠` 标记 + 悬停原因;运行期该列显示 `—`
14. **设置页**(第一阶段):不再有「策略分组」子 tab;Tab 设置里策略行与内置行同权(显隐 + 拖拽排序 + 间隙高亮线),**不提供**新增/重命名/删除任何入口。
15. **键盘/可及**:Esc / 外点关闭弹层(有未保存改动则确认);`ƒ``⚠` 标记均带 title 文本(不单靠颜色表意)。
## 5. 空态与边界
| 场景 | UI |
|---|---|
| 本策略尚无字段 | 弹层列表空态:「尚未配置字段 —— 点击「+ 添加字段」为本策略添加(该策略下每个持仓按此定义填写值)」 |
| 该策略无手填字段(配计算字段时) | 变量菜单「自定义」分组空态:「本策略暂无手填字段」 |
| 该策略当前无持仓 | 试算行内:「暂无可试算的持仓数据(保存不受影响)」 |
| 行情缺失 | 计算列单元格 `—`tertiary 色,不报错) |
| 计算失败(除零/非有限) | 单元格 `—` |
| 公式为空 / 未知变量 / 变量名冲突 | 拒绝保存 + 公式框描红 + 具体文案(见 §4.8) |
| 弹层有未保存改动时关闭 | 二次确认「有未保存的字段改动,确定放弃?」(D-8,AI 默认) |
| 未分配持仓 / 无 holding 锚点行 | 计算列照常显示(公式只用行情 + 持仓账本,不依赖 holding 锚点) |
| 设置页 | 仅「Tab 设置 / QMT 连接配置」两个子 tab |
## 6. 组件落点(技术方案阶段细化)
- `src/client/views/StrategyTab.jsx`:顶栏新增「字段配置」按钮(列设置旁)+ 弹层开关;计算列只读渲染(`ƒ` 表头 + title + 格式化 + `—`);
- `src/client/views/FieldConfigDialog.jsx`(**新增**):字段配置弹层容器(宽度/滚动/底部按钮/放弃确认),内容复用改造后的 StrategyFieldsEditor
- `src/client/views/StrategyFieldsEditor.jsx`(**改造**):作为弹层内容;保存回调改单策略端点;类型下拉加「计算」+ 公式分支(公式框 / 变量选择器 / 试算 / 小数位);
- `src/client/views/SettingsSection.jsx`(**改造**):移除「策略分组」子 tab(含 CRUD 与确认弹窗);
- `src/client/views/ColumnSettingsPopover.jsx`:列名 `ƒ` 前缀(仅文案);
- 服务端(技术方案阶段):`strategies` 常量 + tabs 策略条目固定 + 存储归一化;`strategies/schema-update` 单策略字段端点;变量目录 + 表达式引擎 + `strategy-positions` 现算 + 校验/试算端点;
- 零改动面:`FieldCellEditor`(formula 不进入)、列配置持久化与归一化、历史持仓路径、份额账本与交易归属。
## 7. 已拍板决策(2026-09-10 老师逐项确认)
| # | 问题 | 结论 |
|---|---|---|
| D-1 | 变量命名细则 | 内置固定中文名;自定义字段用**展示名**(新增唯一性 + 不与内置名冲突校验) |
| D-2 | 计算列视觉区分 | 表头 `ƒ` 前缀 + 悬停看公式;不加背景色 |
| D-3 | 试算预览 | 本期做 |
| D-4 | 已删变量的提示 | 字段列表 `⚠` 标记 + 悬停原因;运行期 `—` |
| D-5 | 小数位 | 数字输入 0-4,默认 2 |
| D-6 | 公式框能力 | 等宽字体 + 选择器插入;不做实时高亮/自动补全 |
| D-7 | 约束文档更正 | 一并更正 UI约束-005 + 新增计算字段约束条目 |
| D-8 | 弹层未保存改动的关闭行为 | **AI 默认**:二次确认放弃;老师可否决(备选:直接丢弃 / 禁止关闭) |
| D-9 | 弹层形态 | **AI 默认**:锚定弹层(与列设置同源视觉),~520px 宽、70vh 高、内部滚动;老师可否决(备选:右侧抽屉 / 居中模态) |
## 8. 约束与历史需求联动(实施时执行)
- **约束修订**R-027):产品约束-002/003/004(标签体系 / 标签自定义 + 显示开关场景收敛为「固定两策略 + 字段自定义」)、产品约束-009(Tab 统一管理中「策略条目动态化」条款)、UI约束-002(设置页子 tab 构成:移除策略分组)、UI约束-003(Tab 设置:策略行不再随 CRUD 增删)、**UI约束-005**(字段配置 UI 入口迁移 + 更正为「表格列化 + 单元格内联编辑」现状);
- **约束新增**R-026):拟 UI约束-008(计算字段表单 + 只读计算列)、产品功能约束(计算字段语义:不落库 / 只读 / 仅非公式变量)、技术方案约束(变量目录 + 表达式引擎 + 服务端现算);
- **历史需求取代**:R-003(策略 CRUD)→ 退役;R-011(Tab 统一管理)→ 策略条目动态化部分取代、显隐排序机制保留;R-013 → 字段模型/列机制保留、入口条款取代(均由 R-027 取代,索引标注追溯)。
## 9. 待确认/顺带发现
- 无(D-8 / D-9 为 AI 默认项,老师如有异议直接改判)。
@@ -0,0 +1,250 @@
# 技术实现方案:22-策略计算字段与策略 tab 静态化
> 依据:R-027(重构·第一阶段)+ R-026(计算字段·第二阶段)+ PLAN-019 + `UI交互设计.md`(已定稿)
> 日期:2026-09-10 | 状态:**方案稿(待老师过审后进入实现)**
> 约束依据:技术约束-003(REST 直连)/011(回归独立数据目录)/012(策略定义存储)/015configSchema)、UI约束-004/005/006/007;本次修订与新增见 §7
## 0. 总览
| 阶段 | 需求 | 一句话 | 主要落点 |
|---|---|---|---|
| 一 | R-027 | 策略 tab 静态化 + 字段配置入口迁到策略 tab 内 | `settings.js` / `api/strategies.js` / `SettingsSection.jsx` / `StrategyTab.jsx` / `StrategyFieldsEditor.jsx` |
| 二 | R-026 | 新增「计算(formula)」字段类型:中文变量公式引用服务端缓存数据集现算 | `formula/`(新)/ `PositionManager` 或 API 层附着 / `StrategyTab.jsx` / `StrategyFieldsEditor.jsx` |
**关键设计决策(本方案定)**
1. **计算在服务端 API 层附着**`strategy-positions` 返回前),不改 `PositionManager` 的份额语义;
2. **聚合口径只读内存缓存**QuoteHub.quotes / instruments),即使 miss 也**不做读穿透**——`strategy-positions` 是高频读路径,不引入网络往返;行情新鲜度由 QuoteSync 5s 覆盖持仓 code 保证;
3. **无 formula 字段的策略零开销**(不取行情、不进引擎);
4. 存储真相源单一:字段定义迁到 `strategyFields``strategies` 收窄为内置身份表(读取兼容旧定义)。
## 1. 第一阶段:静态化与入口迁移(R-027)
### 1.1 数据模型(src/settings.js
**常量(新增/收窄)**
```js
/** 内置策略(静态,R-027):身份固定,不可增删改名 */
export const BUILTIN_STRATEGIES = [
{ id: 'grid-supermarket', name: '网格超市' },
{ id: 'manual-t', name: '手动做T' },
];
```
- `DEFAULT_STRATEGIES` 退役(由 `BUILTIN_STRATEGIES` 取代,内部先行别名过渡)。
**schema 变更**
```js
strategies: z.array(z.object({ id, name, configSchema })).default([]), // 收窄:仅身份表,改为内置两项(兼容读取)
strategyFields: z.dict(z.array(fieldSchema)).default({}), // 新增:策略字段定义真相源 { [strategyId]: configSchema }
strategyColumns: // 不变
```
- `fieldSchema``{ key, label, type(text|number|boolean|enum|formula), enum[], def, unit, formula, decimals }``formula`/`decimals` 为第二阶段新增,见 §2.1)。
**读取归一化(单一真相源 + 旧数据兼容)**
```js
export function getStrategies(scope) {
const v = scope?.get() ?? {};
const legacy = new Map((v.strategies ?? []).map(s => [s.id, s.configSchema])); // 旧:定义挂 strategies[]
return BUILTIN_STRATEGIES.map(s => ({
...s,
configSchema: Array.isArray(v.strategyFields?.[s.id])
? v.strategyFields[s.id]
: (Array.isArray(legacy.get(s.id)) ? legacy.get(s.id) : []), // 旧值回填视图(首次保存即落到 strategyFields
}));
}
export function getStrategyFields(scope, strategyId) { /* 同上单策略版,未知 id → [] */ }
export async function updateStrategyFields(scope, strategyId, configSchema) {
assertBuiltinStrategy(strategyId); // 非内置 → 抛 strategy-not-found
const cur = scope.get() ?? {};
await scope.update({ ...cur, strategyFields: { ...(cur.strategyFields ?? {}), [strategyId]: normalizeFields(configSchema) } });
}
```
- 兼容:存量 `strategies[].configSchema`(本机 = 网格超市 5 字段 / 手动做T 1 字段)读取时回填,前端表单提交**全量** → 首次保存即写入 `strategyFields`**无需迁移脚本**
- 自建策略(本机实测无):读取时忽略,其 `strategy_holdings` 行保留在库(不删数据)。
**tabs 归一化(`normalizeTabs` 收窄)**
- 默认序列 = 内置 tab(3)+ 内置策略 tab(2);
- 读取:按存量 `tabs``visible/order` 应用偏好,**丢弃** `kind='strategy'``refId` 不在 `BUILTIN_STRATEGIES` 中的条目(自建策略 tab 退役);
- 新增策略 tab 条目若存量缺失(旧数据无策略 tab)→ 按默认序补齐;
- 删除:`appendStrategyTab` / `removeStrategyTab`(不再有增删场景)。
**退役清单(实施确认)**`addStrategy` / `removeStrategy` / `renameStrategy` / `updateStrategies` / `appendStrategyTab` / `removeStrategyTab` / `DEFAULT_STRATEGIES` 已删除;`generateStrategyId` / `PINYIN_MAP` **保留**QMT 连接 id 生成 `generateQmtConnectionId` 仍依赖,本轮不动)。
### 1.2 API 变更(src/api/strategies.js
| 端点 | 处理 |
|---|---|
| `strategies` | 保留(返回内置两项 + 各自 configSchema |
| `strategies/add` | **退役** |
| `strategies/remove` | **退役** |
| `strategies/update` | **退役**(整表写入不再需要;重命名随静态化取消) |
| `strategies/schema-update` | **新增**`{ strategyId, configSchema }` → 服务端校验(§2.5)→ `updateStrategyFields` → 返回该策略 `{ id, name, configSchema }` |
| `tabs` / `tabs/update` | 保留(显隐 + 排序为唯一可变项) |
| `strategy-positions` | 保留 + 第二阶段附 `computed`(§2.4 |
| `formula/trial` | **新增**(第二阶段,§2.5 |
- `STRATEGY_METHODS` 集合同步增删;`api/index.js` 头部注释同步。
- `handleStrategy` 解构补 `marketHub, dataSource`(计算字段取数用)。
### 1.3 设置页收敛(SettingsSection.jsx
- 删除子 tab 项 `{ key:'strategies', label:'策略分组' }` 与渲染分支、`StrategyGroupSettings` 组件、`StrategyFieldsEditor` 引入、新增/重命名/删除逻辑与删除确认弹窗(L208–436 区块);
- 子 tab 仅剩 `tabs` / `qmt``activeTab` 初值维持 `'tabs'`
### 1.4 字段配置弹层(前端)
**`src/client/views/FieldConfigDialog.jsx`(新增)**
- props`{ strategyId, strategyName, configSchema, onSaved, onClose }`
- 形态:锚定弹层(`width 520``maxHeight 70vh`、内部滚动),视觉沿用 `ColumnSettingsPopover`
- 底部动作:`[关闭]`(次) + `[保存字段]`(主,saving 时 disabled);
- **未保存改动**`dirty`)时 关闭/外点/Esc → 二次确认「有未保存的字段改动,确定放弃?」(D-8)。
**`src/client/views/StrategyFieldsEditor.jsx`(改造)**
- 由「设置页整表保存」改为「弹层内容 + 单策略保存」:`onSave(fields)` → 调 `one-divine-lot/strategies/schema-update { strategyId, configSchema: fields }`
- 类型下拉增 `计算`(第二阶段);`dirty` 状态上报父弹层(用于放弃确认)。
**`src/client/views/StrategyTab.jsx`(改造)**
- 顶栏「列设置」左侧新增「字段配置」按钮 → 打开 `FieldConfigDialog``onSaved``load()`(列与数据同步刷新);
- configSchema 复用既有加载(L236239)。
## 2. 第二阶段:计算字段(R-026)
### 2.1 字段模型扩展(settings.js fieldSchema
```js
{ key, label, type: '…'|'formula', enum: [], def, unit: '',
formula: z.string().default(''), // 仅 type='formula':表达式串(中文变量)
decimals: z.number().default(2) } // 仅 type='formula':显示小数位 0-4
```
- `normalizeFields``decimals` 夹取 04 整数;`formula` 去首尾空白;`type='formula'` 时忽略 `def`
- 值不落库:`strategy_holdings.values` 与 formula 字段无关(读写路径零改动)。
### 2.2 公式引擎(`src/formula/evaluator.js`,纯函数零依赖)
```js
validateFormula(expr, allowedVars) { ok: true, vars: Set<string> }
| { ok: false, error: { code: 'empty'|'syntax'|'unknown-var'|'unknown-func'|'arity', message, position } }
evaluateFormula(expr, ctx) number | null // ctx: { [中文变量名]: number|null }
evaluateFormulaCompiled(ast, ctx) number | null // 编译一次多行复用(性能)
```
- **词法**:数字(整数/小数)、标识符(`\p{L}` 起头,含中文,可含数字/下划线)、运算符 `+ - * / × ÷ ( ) ,``×``*``÷``/` 归一化);
- **语法优先级**:括号 > 一元负号 > `* / / ÷` > `+ -`;函数调用 `name(arg, …)`
- **内置函数**`round(x[, n=0])``abs(x)``min(a, b, …)``max(a, b, …)`
- **求值语义**:引用的任一变量为 `null/undefined/NaN` → 整式返回 `null`(缺值短路,不产出 NaN);除零、非法运算、结果非有限 → `null`
- **安全**:自写解析,不使用 `eval/Function`(技术约束新增条目,§7)。
### 2.3 变量目录(`src/formula/variables.js`
```js
export const VARIABLE_GROUPS = [
{ group: '行情', source: 'quote', items: [
{ name: '现价', field: 'lastPrice', desc: '最新成交价' },
{ name: '昨收', field: 'lastClose' }, { name: '今开', field: 'open' },
{ name: '最高', field: 'high' }, { name: '最低', field: 'low' },
{ name: '成交量', field: 'volume' }, { name: '成交额', field: 'amount' } ] },
{ group: '合约', source: 'instrument', items: [
{ name: '涨停价', field: 'upStopPrice' }, { name: '跌停价', field: 'downStopPrice' } ] },
{ group: '持仓', source: 'row', items: [
{ name: '份额', field: 'shares' }, { name: '成本价', field: 'avgPrice' }, { name: '最后成交价', field: 'lastTradePrice' } ] },
{ group: '自定义字段', source: 'values', items: [] }, // 运行时按策略非 formula 字段的 label 动态展开
];
export const BUILTIN_VAR_NAMES = new Set([...行情/合约/持仓 items name]);
export function buildContext({ row, quote, instrument, fieldDefs }) { [中文名]: number|null }
```
- 自定义字段变量名 = 字段 `label`;值取 `row.values?.[key]`(缺省回退 `def`);非数字值(text/enum/boolean)→ `Number(...)` 失败即 `null`
- **不收录 formula 字段**(零循环依赖)。
### 2.4 计算服务与数据通路
**`src/formula/FormulaService.js`(新增)**
```js
computeRows({ strategyId, rows, fieldDefs, marketHub }) rows 附加 computed: { [fieldKey]: number|null }
```
1.`type='formula'` 字段 → **直接返回原 rows**(零开销);
2. `codes = rows.map(r => r.code)` → 行情**只读内存缓存**`marketHub.quotes.get(code)`、合约 `marketHub.instruments.get(code)`(与 `api/market.js#projectQuote` 同口径;**不读穿透**,miss → 相关变量 null);
3. 逐行 `buildContext`(含该策略非 formula 字段的值)→ 每个 formula 字段 `evaluateFormulaCompiled` 一次;
4. 结果:`computed[field.key] = number|null`(引擎返回原始数值,格式化交前端)。
**接线**`api/strategies.js``strategy-positions` 分支)
```js
const rows = await manager.getStrategyPositions(args.strategyId);
return await formulaService.computeRows({ strategyId: args.strategyId, rows,
fieldDefs: getStrategyFields(settings, args.strategyId), marketHub });
```
- 历史持仓端点(`strategy-holdings/history`**不计算**:前端对历史行显示 `—`
### 2.5 API(校验与试算)
**`strategies/schema-update`(含字段校验)**
- 结构校验:`key` 非空且唯一(同策略内)、`label` 非空且唯一(D-1)、`type` 合法;
- formula 字段:`validateFormula(formula, allowedVars)``allowedVars` = 内置变量名 本策略**非 formula** 字段的 label
- 名称冲突:`label ∈ BUILTIN_VAR_NAMES` → 拒绝(`variable-name-conflict`);
- 失败统一抛 `{ code:'field-validation', message }`(前端 Toast + 公式框描红 + 文案)。
**`formula/trial`(新增)**
```js
// args: { strategyId, formula, decimals?, unit? }
// 1) 校验公式;2) 取该策略首行份额 > 0 的持仓;3) 取缓存行情/合约 → buildContext → 求值
// → { code, name, value: number|null, reason?: 'no-holding'|'no-data'|null }
```
### 2.6 前端渲染(StrategyTab.jsx / ColumnSettingsPopover.jsx
- 列取值:`computed?.[key]``null``—`tertiary);数值 → `toFixed(decimals)` 去尾零 + `unit` 拼接;
- 表头:formula 字段列名前缀 `ƒ`tertiary`title="计算字段(只读)"`),列头/单元格 `title` = `= <公式原文>`
- 只读:formula 列**不挂** `FieldCellEditor`、不绑编辑 onClick`fieldDef.type === 'formula'` 分支直接渲染文本);
- 列设置弹层:列名前缀 `ƒ``normalizeStrategyColumns` 的字段列附 `type` 供 UI 判断;排序/显隐零改动);
- 历史行:无 `computed``—`
## 3. 涉及文件与改动清单
```
src/settings.js # BUILTIN_STRATEGIES / strategyFields / normalizeTabs 收窄 / 退役 CRUD 辅助
src/api/strategies.js # 退役 add|remove|update;新增 schema-update、formula/trialstrategy-positions 附着 computed
src/api/index.js # 头部端点注释同步
src/formula/evaluator.js # 新增:词法/语法/求值/校验
src/formula/variables.js # 新增:变量目录 + buildContext
src/formula/FormulaService.js # 新增:批量现算(缓存只读)
src/client/views/SettingsSection.jsx # 移除「策略分组」子 tab
src/client/views/FieldConfigDialog.jsx # 新增:字段配置弹层
src/client/views/StrategyFieldsEditor.jsx# 改造:弹层内容 + 单策略保存 + formula 分支(公式框/变量选择器/试算/小数位)
src/client/views/StrategyTab.jsx # 顶栏「字段配置」按钮 + 计算列只读渲染
src/client/views/ColumnSettingsPopover.jsx # 列名 ƒ 前缀(仅文案)
scripts/test-r027-static-strategies.mjs # 新增回归(第一阶段)
scripts/test-r026-formula-fields.mjs # 新增回归(第二阶段:引擎 + 目录 + 校验 + 附着 + 试算)
scripts/test-r011-tabs.mjs # 更新(策略 tab 静态化后断言调整)
scripts/test-r013-custom-fields.mjs # 更新(改用 updateStrategyFields
```
## 4. 回归与验证
- 回归脚本均用独立数据目录(`ODL_TEST_DATA_DIR`,技术约束-011),内存 mock settings scope(沿用 test-r013 惯例);
- **test-r027**:内置策略恒两项;旧 settingsstrategies[].configSchema)读取回填;`updateStrategyFields` 单策略写入且不污染另一策略;非内置 strategyId → 拒绝;`normalizeTabs` 丢弃自建策略条目且保留 visible/order`strategies/add|remove|update` 端点已退役(unknown method);
- **test-r026**:引擎(中文变量 / 优先级 / 括号 / 一元负 / 函数 arity / 语法错误位置 / 缺值短路 / 除零 → null);变量目录(三组内置名、自定义 label 动态、formula 字段不入目录);`schema-update` 校验(未知变量 / 名称冲突 / 重复 key / 重复 label);`strategy-positions` 附着 `computed`(有/无 formula 字段两条路径);`formula/trial`(正常 / 无持仓 / 缺数据);
- `pnpm typecheck` + `pnpm build` 通过;
- 老师人工验收(见 `验收标准.md`)。
## 5. 风险与取舍
| 项 | 取舍 / 缓解 |
|---|---|
| 现算开销 | 仅含 formula 字段的策略付出;行情只读内存(无网络);行数为数十级 → 毫秒内;未直接引入缓存 |
| 行情 miss | 不读穿透 → 该行算式 `—`(等 QuoteSync 下轮 5s 刷新);换取 `strategy-positions` 路径零网络 |
| 中文标识符 | 自写词法支持 `\p{L}`;不用第三方库(均不支持中文变量) |
| 旧数据 | 读取回填 + 首次保存落 `strategyFields`;不写迁移脚本、不删库数据 |
| 自建策略 | 读取忽略 + tab 丢弃;其持仓数据保留在库(不自动清份额),需要时人工处理 |
| 双份真相源风险 | `strategies[].configSchema` **只读兼容**、写路径唯一走 `strategyFields`(避免双写打架) |
## 6. 实施顺序
1. settings 层(常量 / strategyFields / tabs 归一化 / 退役)+ test-r027
2. API 层(端点退役 + schema-update+ 设置页收敛 + 字段配置弹层 + test-r013 更新;
3. 公式引擎 + 变量目录 + FormulaService + test-r026(引擎部分);
4. schema-update 字段校验 + formula/trial + strategy-positions 附着;
5. 前端 formula 分支(公式框/选择器/试算/小数位)+ 只读计算列;
6. typecheck + build + 全量回归 → 老师人工验收 → 复盘。
## 7. 约束落地(实施时执行)
- **修订**:产品约束-002/003/004(标签体系 → 固定两策略 + 字段自定义)、产品约束-009(Tab 统一管理中去掉策略条目动态化)、UI约束-002(设置页子 tab 构成)、UI约束-003(Tab 设置策略行不再随 CRUD 变化)、UI约束-005(字段配置入口迁移 + 更正为「表格列化 + 单元格内联编辑」现状);
- **新增**:UI约束-008(计算字段表单 + 只读计算列:`ƒ` 标记 / 悬停公式 / 小数位 / `—`)、产品功能约束(计算字段语义:不落库、只读、仅非公式变量)、技术方案约束(公式引擎自写零依赖不用 eval、变量目录单一入口、现算只读内存缓存)。
@@ -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 复盘就被记录却未处理,
本轮顺手清偿,避免「红灯常态化」。
@@ -0,0 +1,45 @@
# 迭代目标:22-策略计算字段与策略 tab 静态化
## 目标
两条腿,一个迭代走完:
- **第一阶段 · 重构(R-027)**:策略 tab 静态化为插件内置 tab(不可增删改名,保留显隐/排序);设置页「策略分组」移除;
字段编辑功能迁到**策略 tab 内、「列设置」旁的「字段配置」弹层**;
- **第二阶段 · 计算字段(R-026)**:在迁移后的入口上新增第五种字段类型「**计算(formula)**」——用户写中文变量公式
引用服务端缓存数据集,服务端按持仓行**实时计算**列值(不落库、列只读、缺数据 `—`)。
## 目标描述
- **第一阶段背景**:2026-09-10 老师提出重构想法——「策略 tab 不想做成动态可增减修改的了,改成静态的,和全部持仓、交易记录这些一样,
都是插件内置的 tab」「这两个策略 tab 原有的字段编辑功能,切换到主窗口的策略 tab 下面,列设置旁边,作为统一的字段配置管理」。
实机核实:仅两个内置策略(网格超市 / 手动做T)、**无自建策略**(store.db 13/3 行),静态化零数据迁移风险;需求定稿 R-027。
- **第二阶段背景**:2026-09-10 老师提出字段配置优化的下一步——新增**可计算**的字段类型(公式引用服务端缓存数据集得出结果);
需求定稿 R-026。
- **合并理由**:公式表单必须落在字段配置入口上;若先按旧入口(设置页策略分组)实现,再随重构搬迁 = 白做一遍(老师 Q6 拍板:一次改到位)。
- **本迭代节奏(老师指令 2026-09-10)**:先出计划 → 启动迭代 → **本轮只做交互设计**(不做技术方案与编码);交互设计过审后补技术方案 + 验收标准,再进入实现。
- **验收线**
- 重构:设置页无「策略分组」;策略 tab 恒两个且不可增删改名;Tab 设置仍可显隐/排序;字段配置弹层可用并落库到正确策略;
- 计算字段:四式用例(浮动盈亏 / 盈亏比例 / 距涨停 / 网格占用)在策略 tab 列上算出;计算字段不落库、列只读、非法公式被拒、缺数据不炸表。
## 目标分解
1. **交互设计**(本步):① 重构部分——策略 tab 顶栏两个按钮(字段配置 / 列设置)、字段配置弹层(列表 + 表单 + 保存)、
设置页收敛;② 计算字段部分——弹层内公式分支(公式框 + 变量选择器 + 试算 + 单位/小数位)、持仓表只读计算列、空态与错误态;
2. **技术实现方案**(交互过审后):两阶段规格——tab/存储归一化、单策略字段端点、变量目录、表达式引擎、现算通路、校验与试算、回归脚本;
3. **验收标准**(同上):两阶段各自的验收方法与判定线;
4. **实现与验收**:第一阶段(重构)→ 回归 → 第二阶段(计算字段)→ 回归 → 老师人工验收 → 复盘。
## 目标讨论过程
1. 2026-09-10 老师提出计算字段方向 → AI 摸清 R-013 现状 + 服务端缓存数据集清单 → Q1-Q10 问题清单 → 老师「定」+ 三项选择题确认 → 定稿 **R-026**
2. AI 出迭代 22 `UI交互设计.md`(公式表单落在设置页策略分组)→ 老师拍板 D-1~D-7 → 交互设计定稿;
3. 2026-09-10 老师提出**重构想法**(策略 tab 静态化 + 字段配置入口迁移)→ AI 核实实机数据(无自建策略、零迁移风险)+ 影响面分析
+ 撞位提示(公式表单落在设置页,入口一搬即废)→ 老师拍板 Q1~Q4、Q6(全采建议),Q5/Q7 由 AI 定 → 定稿 **R-027**
4. 迭代 22 结构调整为「第一阶段=重构、第二阶段=计算字段」,交互设计同步修订(公式表单移入「字段配置」弹层)。
## 对老师(项目主理人)的配合需求
- **本轮(已完成 2026-09-10**:拍板 D-1D-7(计算字段交互)与 Q1~Q6(重构方案)—— 全部采纳 AI 建议;
- **后续**:技术方案过审后,按两阶段做页面人工验收——① 设置页无策略分组、策略 tab 不可增删改名、字段配置弹层可用;
② 公式配置与试算、持仓表只读计算列、缺数据/非法公式边界。
@@ -0,0 +1,98 @@
# 验收标准:22-策略计算字段与策略 tab 静态化
> 依据:R-027(第一阶段)+ R-026(第二阶段)+ PLAN-019 + `技术实现方案.md` 日期:2026-09-10 状态:**待实施后验收**
> 验收线在哪里:本文件(分阶段清单 + 判定线);配套证据:回归脚本输出、typecheck/build 输出、老师人工页面确认
## 1. 验收目标
- **第一阶段**:策略 tab 变成**插件内置静态 tab**(不可增删改名,保留显隐/排序),设置页「策略分组」消失,字段配置入口落到**策略 tab 内、列设置旁**;
- **第二阶段**:「计算(formula)」字段类型可用——用户写中文变量公式(引用行情/合约/持仓账本/本行手填字段),服务端按持仓行**实时算**出列值,**不落库、列只读、缺数据 `—`**
- 既有能力(四类手填字段、列设置、历史持仓、份额账本、交易归属)**零回归**。
## 2. 验收标准线 · 第一阶段(R-027 重构)
| # | 验收项 | 判定 |
|---|---|---|
| A1 | 设置页子 tab 仅「Tab 设置 / QMT 连接配置」 | 页面无「策略分组」入口 |
| A2 | 无任何策略新增/重命名/删除入口 | 设置页与策略 tab 均无该入口;服务端调用 `strategies/add\|remove\|update` 返回 unknown method |
| A3 | 策略 tab 恒为「网格超市 / 手动做T」 | 重启、改配置、增删数据后仍恒两项 |
| A4 | Tab 设置中策略行与内置行同权 | 可拖拽排序(间隙高亮线)、可显隐开关;现有偏好(策略置顶 / 全部持仓·关注列表隐藏)保留 |
| A5 | 策略 tab 顶栏有「字段配置」与「列设置」两个按钮 | 位置:字段配置在左、列设置在其右 |
| A6 | 字段配置弹层显示**本策略**现有字段 | 网格超市 5 个(网格上边界/下边界/基准值/网格大小/网格交易量);手动做T 1 个(T仓成本价) |
| A7 | 弹层内可添加/编辑/删除字段并保存 | 保存走 `strategies/schema-update`;成功 Toast;另一策略定义不受影响 |
| A8 | 字段增删后表格列自动跟随 | 新增列出现在表格(列设置中可勾选/排序);删除字段后该列消失 |
| A9 | 未保存改动时关闭弹层 | 出现「确定放弃?」二次确认(D-8) |
| A10 | 重启后字段定义仍在 | 存储落 `strategyFields`(旧 `strategies[].configSchema` 首次保存后被取代,无迁移脚本) |
| A11 | 数据零影响 | `strategy_holdings` 行数(13 / 3)、交易归属、份额不变 |
## 3. 验收标准线 · 第二阶段(R-026 计算字段)
| # | 验收项 | 判定 |
|---|---|---|
| B1 | 字段类型下拉含「计算」 | 选中后隐藏默认值,出现公式框 + 变量选择器 + 试算 + 小数位(0–4,默认 2) |
| B2 | 变量选择器按分组列出可用变量 | 行情(现价/昨收/今开/最高/最低/成交量/成交额)、合约(涨停价/跌停价)、持仓(份额/成本价/最后成交价)、自定义字段(本策略手填字段的**展示名**) |
| B3 | 公式用**中文变量**且可算出结果(四式用例) | ① `(现价 - 成本价) × 份额``(现价 ÷ 成本价 - 1) × 100``(涨停价 - 现价) ÷ 现价 × 100``现价 × 份额`(或 × 网格交易量)——每行值正确(与手工核算一致) |
| B4 | 试算可用 | 点「试算」用该策略一行真实持仓算出并显示(含代码/名称/格式化结果);无持仓 → 「暂无可试算的持仓数据」 |
| B5 | 保存校验生效 | 语法错误(如 `现价 ×`)→ 拒绝 + 公式框描红 + 定位文案;未知变量 → 拒绝;字段展示名与内置变量冲突(如叫「现价」)→ 拒绝;同策略展示名重复 → 拒绝 |
| B6 | 计算列只读且可辨识 | 表头 `ƒ` 前缀;点击单元格**不进入**编辑;悬停列头/单元格显示公式原文 |
| B7 | 格式化正确 | 按小数位 + 单位显示(如 `6.67 %``700.00 元` |
| B8 | 缺数据不炸表 | 无行情 / 除零 / 非有限 / 手填字段值非数字 → 该格 `—`(不显示 NaN、不报错、其余列正常) |
| B9 | 计算字段**不落库** | `strategy_holdings.values` 无 formula 字段键;重启后公式仍在(存 settings)、值仍为实时计算 |
| B10 | 历史持仓行 | 已清仓行的计算列显示 `—`(不做历史时点计算) |
| B11 | 列设置联动 | 计算列与手填列同列混排(名称带 `ƒ` 前缀),显隐/拖拽排序正常 |
| B12 | 零回归 | 四类手填字段编辑、列设置、历史持仓开关与范围、份额操作、交易归属、Tab 设置全部照旧 |
## 4. 边界与异常用例
| 场景 | 期望 |
|---|---|
| 公式引用已删除的字段 | 保存时被拒(未知变量);已存在的旧公式运行时该列 `—`,字段列表 `⚠` 标记 + 悬停原因(D-4 |
| 公式引用另一个计算字段 | 被拒(变量目录不含 formula 字段,零循环依赖) |
| 公式为空 / 只有空格 | 拒绝保存「请填写公式」 |
| 极长公式 / 深层括号 | 不崩溃(解析器深度限制,超出 → 语法错误提示) |
| 手填字段值为文本(非数字) | 参与计算时按 `null` 处理 → `—`(不报错) |
| QMT 断开 | 该策略计算列大面积 `—`;表格其余功能正常 |
| 无 formula 字段的策略 | 行为与重构前完全一致(无额外取数开销) |
| 弹层打开时保存失败(服务端异常) | Toast 错误、弹层不关闭、改动保留可重试 |
## 5. 验收方法
1. **回归脚本**(独立数据目录,技术约束-011
- `node scripts/test-r027-static-strategies.mjs` → 全绿(内置策略常量 / 旧定义兼容 / 单策略写入 / tabs 归一化 / 端点退役);
- `node scripts/test-r026-formula-fields.mjs` → 全绿(引擎 / 目录 / 校验 / 附着 computed / 试算);
- `node scripts/test-r011-tabs.mjs``node scripts/test-r013-custom-fields.mjs`(已更新)→ 全绿;
2. **静态检查**`pnpm typecheck` + `pnpm build` 通过;
3. **老师人工验收**(按 §6 步骤逐条确认,A1–A11 + B1B12)。
## 6. 老师配合项(人工验收步骤)
1. 打开设置页 → 确认只有「Tab 设置 / QMT 连接配置」(A1/A2);
2. Tab 设置 → 拖动两个策略 tab、切换显隐(A3/A4);
3. 进入策略 tab(网格超市)→ 顶栏 `[字段配置] [列设置]` → 打开字段配置 → 看到原有 5 个字段(A5/A6);
4. 添加计算字段(例:展示名「网格占用」、类型「计算」、单位「元」、小数位 2、公式 `现价 × 份额`)→ 插入变量 → 试算 → 保存(B1/B2/B4);
5. 表格中该列出现并显示数值;悬停看公式;点击不可编辑(B3/B6/B7);
6. 再配两个计算字段验证其余用例:`(现价 - 成本价) × 份额``(现价 ÷ 成本价 - 1) × 100`(单位 %)(B3);
7. 故意写坏公式(`现价 ×`)、引用未知变量、把字段命名为「现价」→ 均应被拒绝并提示(B5);
8. 造缺数据场景(如某 code 无行情)→ 该格 `—`,表格正常(B8);
9. 清仓一只持仓 → 历史行计算列 `—`B10);
10. 重启插件 → 字段定义仍在、值重新现算(A10/B9);
11. 抽查数据库:`strategy_holdings.values` 不含 formula 键(B9)。
## 7. 判定线
**通过(全部满足)**
- A1A11 与 B1B12 全部通过;
- 回归脚本全绿 + typecheck/build 通过;
- 老师人工验收确认。
**任一即不通过**
- 计算字段写入 `values` 列(或落任何库表);
- 公式可执行表达式以外的行为(无 `eval/Function`、不可触达进程/文件能力);
- 无 formula 字段的策略出现额外取数开销或行为变化(回归);
- 既有四类字段编辑、列设置、历史持仓、份额/归属任一回归失败;
- 策略仍可被增删改名(静态化未达成)。
## 8. 明确不在本次验收范围
- 策略级聚合(跨行求和)、QMT 账户域/交易记录作为变量源、公式引用公式、历史时点计算、布尔型公式结果、公式框实时高亮/自动补全、完整 T-006 数据池;
- 自建策略的历史数据清理(本机无自建策略;如未来出现,另行讨论)。
@@ -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` 一元取反、判定与数字混用的单字段、策略级聚合(跨行求和)、
历史持仓行的时点判定。
+127
View File
@@ -0,0 +1,127 @@
# 需求:R-026 策略计算字段(公式字段)· 已定稿
> 登记:2026-09-10 来源:老师指令(2026-09-10 功能讨论「策略组字段配置优化」)| 状态:**已定稿(2026-09-10 老师确认)**
> 归属:**22-策略计算字段与策略tab静态化 · 第二阶段**(PLAN-019)| 实现状态:**已实现(2026-09-10 完成,typecheck/build/17 个回归脚本全绿),待老师人工验收**
> **入口条款修订(2026-09-10,R-027 取代)**:字段配置入口由「设置页 → 策略分组 → 策略行展开」**迁至「策略 tab 内、列设置旁的『字段配置』弹层」**;本文档 §9 配置 UI 的入口位置随 R-027 变更,字段模型 / 公式语义 / 只读列 / 列机制均不变。
> 前身:草稿 T-010(已转正,草稿文件已清理)
## 诉求(老师,2026-09-10
> 策略组字段配置优化:添加的字段,目前仅是支持用户指定值;接下来添加新的字段类型,就是**可以计算**,
> 目标是让用户**写一个公式**,公式中支持来自几个**服务端缓存的数据集**,然后通过计算得出结果。
## 背景(现状 R-013/迭代 11
- 策略字段定义 = `settings.strategies[].configSchema``{ key, label, type(text|number|boolean|enum), enum[], def, unit }`
- 字段值 = `strategy_holdings.values` JSON 列(**用户手工填写**,单元格内联编辑);
- 展示 = 持仓表列化(列显隐/顺序由 `strategyColumns` 每策略独立配置);
- 本次扩展:新增**第五种字段类型「计算(formula)」**——值不是填的,是**算的**。
## 定稿结论(2026-09-10 老师逐项确认)
### 1. 字段类型与存储语义
- `configSchema.type` 增加 `'formula'`;新增 `formula` 表达式串字段(与 `unit` 同级,随策略定义存 settings);
- **计算字段不落库**:不读写 `strategy_holdings.values`,每次读 `strategy-positions` 时**逐行实时现算**
- 表格内**只读**(点击不进内联编辑);列配置兼容零改动(formula 字段自动进列集,新增默认追加末尾)。
### 2. 公式可引用的数据集(首批 = 行情/合约 + 持仓账本 + 本行自定义字段)
| 数据集 | 来源(服务端缓存) | 可引用字段 | 刷新 |
|---|---|---|---|
| 行情盘口 | QuoteHub.quotes(内存) | 现价 / 昨收 / 今开 / 最高 / 最低 / 成交量 / 成交额 | 5s REST |
| 合约信息 | QuoteHub.instruments(内存) | 涨停价 / 跌停价 / 昨收 | 按交易日 |
| 持仓账本 | SQLite strategy_holdings | 策略份额 / 成本价 / 最后一笔成交价 | 交易关联驱动 |
| 本行自定义字段值 | strategy_holdings.values | 本行手填字段(如网格间距、目标价) | 内联编辑 |
**后补(本期不做)**:QMT 账户域持仓(全账户 volume/可用/市值,跨策略视角)、交易记录聚合(当日已成交量/买卖笔数)——用例尚不明确。
### 3. 变量命名:**中文变量名**
- 公式形如 `(现价 - 成本价) × 份额`,可读性优先;
- 配置时校验**变量名唯一**;配置 UI 提供**变量选择器**(按数据集分组,点击插入);
- 引擎需支持中文标识符(自写解析器,见 §4)。
### 4. 公式引擎:自写小型表达式解析器(零依赖)
- 能力:四则运算 + 括号 + 常见函数(round / abs / min / max);
- 选型理由:中文标识符需自定义词法,第三方库(expr-eval/mathjs/jexl)均不支持中文变量;该层未来即 T-006 数据池的表达式层,值得自有。
### 5. 计算位置与粒度:**服务端计算 + 仅行级**
- 服务端在 `strategy-positions` 返回时逐行现算随行带回(行情单一入口 QuoteHub 在服务端);前端仅渲染;
- 行级上下文 = 本行行情 + 本行持仓账本 + 本行自定义字段值 → 一个结果;
- **策略级聚合(跨行求和,如本策略总盈亏)留二期**。
### 6. 循环引用边界:公式**只允许引用非公式字段**
- 变量目录直接排除 formula 字段 → 不存在循环依赖(不做检测与拓扑排序)。
### 7. 结果类型与格式
- 首批结果**只做数字**(布尔型公式后续再议);
- 复用现有 `unit`(展示拼在值后);新增**小数位**配置(默认 2 位)。
### 8. 缺数据与错误处理
- **保存时校验**:语法 + 变量存在性(服务端编译试算一次),非法公式直接拒绝保存并提示;
- **运行期**:缺行情 / 除零 / 无法计算 → 该单元格显示 `—`(不显示 NaN、不报错、不炸整表)。
### 9. 配置 UI(设置页「策略分组」字段表单)
- 类型下拉增加「计算」→ 出现公式输入框 + 变量选择器(分组:行情 / 合约 / 持仓 / 自定义字段)+ 变量速查;
- 加分项:**试算预览**(取一行真实持仓数据现场算出结果)。
### 10. 历史持仓行(已清仓)
- 历史 tab 的公式列统一显示 `—`:清仓行无「现价」语义,**不做历史时点计算**(属另一需求,不混入本期)。
### 11. 与服务端数据池(T-006)的关系
- 本期先落**轻量变量目录**(服务端声明:数据集 → 字段清单 → 取数函数),公式引擎跑在目录之上;
- 该目录即 T-006「数据池中间层」的第一块砖,**不一次性建完整数据池**(避免过度设计)。
## 典型用例(老师视角验收样例)
| 字段 | 公式 |
|---|---|
| 浮动盈亏 | `(现价 - 成本价) × 份额` |
| 盈亏比例 | `(现价 ÷ 成本价 - 1) × 100` |
| 距涨停 | `(涨停价 - 现价) ÷ 现价 × 100` |
| 网格占用(手填+计算组合) | `现价 × 份额 × 网格间距` |
## 决策记录
| 项 | 结论 | 讨论 |
|---|---|---|
| 字段类型 | configSchema.type 加 `formula` + `formula` 串 | 老师指令(新字段类型) |
| 存储语义 | **不落库**,读接口时逐行现算 | 「计算」的本质;values 列不参与 |
| 数据集范围 | 首批 行情+合约 / 持仓账本 / 本行自定义字段值 | 选择题确认(1+2+3);QMT账户域、交易聚合后补 |
| 变量命名 | **中文变量名** + 选择器插入 | 选择题确认;配置时校验唯一 |
| 计算粒度 | **仅行级**;策略级聚合二期 | 选择题确认 |
| 计算位置 | 服务端(strategy-positions 现算随行返回) | 老师方向「引用服务端缓存数据集」;行情单一入口在服务端 |
| 公式引擎 | 自写小型解析器(零依赖,支持中文标识符) | 第三方库不支持中文变量;未来即数据池表达式层 |
| 循环引用 | 只允许引用非公式字段 | 目录层排除,无循环问题 |
| 结果类型 | 数字 + unit + 小数位(默认 2) | 布尔型后议 |
| 错误处理 | 保存校验拒绝 + 运行期显示 `—` | 不炸整表 |
| 配置 UI | 公式输入 + 变量选择器(+试算预览) | 计算字段只读不内联编辑 |
| 历史行 | 公式列显示 `—` | 不做历史时点计算 |
| 数据池关系 | 轻量变量目录先行(T-006 第一块砖) | 避免一次性建完整中间层 |
## 关联
- 上游:R-013(自定义字段配置,字段类型与列机制)/ 迭代 11(列配置 strategyColumns);
- 数据来源:R-015QuoteHub 盘口内存快照)、R-014PositionSync 账户域)、R-010(最后一笔成交价)、R-018(份额账本);
- 延展:T-006(数据池中间层草稿)——本需求的变量目录为其起点;
- 约束条目待实施时补充(拟:产品功能约束 / 技术方案约束 / UI交互约束 各一条,沿用 R-013 惯例)。
## 讨论过程
- 2026-09-10 老师提出功能方向(策略组字段配置优化 → 可计算公式字段);
- AI 摸清 R-013 现状 + 盘活服务端缓存数据集清单,提出 Q1-Q10 设计问题清单(各附建议);
- 老师「定」:Q4-Q10 按 AI 建议;
- 选择题三项确认:Q1 数据集 1+2+3、Q2 中文变量名、Q3 仅行级计算;
- 转正定稿:T-010 草稿 → R-026(草稿文件已清理,索引同步);
- 2026-09-10 迭代 22 交互设计过审:老师逐项拍板 D-1~D-7(全采纳 AI 建议)→ `docs/04-迭代记录/22-策略计算字段与策略tab静态化/UI交互设计.md` 定稿;
- 2026-09-10 老师提出重构想法(策略 tab 静态化 + 字段配置入口迁移)→ 定稿 **R-027**:本需求的**字段配置入口**随之从设置页迁到「策略 tab 内、列设置旁的『字段配置』弹层」(迭代 22 调整为「第一阶段=重构、第二阶段=本需求」);
下一步:技术实现方案 + 验收标准(本轮按老师指令未做技术方案与编码)。
+85
View File
@@ -0,0 +1,85 @@
# 需求:R-027 策略 tab 静态化 + 字段配置入口迁移 · 已定稿
> 登记:2026-09-10 来源:老师指令(2026-09-10 功能讨论「重构想法」)| 状态:**已定稿(2026-09-10 老师逐项确认)**
> 归属:**22-策略计算字段与策略tab静态化 · 第一阶段**(PLAN-019)| 实现状态:**已实现(2026-09-10 完成,typecheck/build/17 个回归脚本全绿),待老师人工验收**
> 前身:草稿 T-011(已转正,草稿文件已清理)
> **取代**R-003(策略 CRUD)、R-011(Tab 统一管理中的「策略条目动态化」部分)、R-013(字段定义入口在设置页部分)
## 诉求(老师,2026-09-10
> 1. 策略 tab 我不想做成动态可增减修改的了,改成静态的,和全部持仓、交易记录这些一样,都是插件内置的 tab;
> 2. 这两个策略 tab 原有的字段编辑功能,切换到主窗口的策略 tab 下面,列设置旁边,作为统一的字段配置管理。
## 现状核实(2026-09-10 实机,代码 + settings.yaml + store.db
- 策略仅两个内置:`grid-supermarket`(网格超市,5 字段:网格上边界/下边界/基准值/网格大小/网格交易量)、`manual-t`(手动做T1 字段:T仓成本价);
- `store.db``strategy_holdings` 13 / 3 行,`trade_orders``trade_order_attributions` 均只涉及这两个策略 → **无自建策略,零数据迁移风险**
- `settings.tabs` 现状:网格超市(0) → 手动做T(1) → 交易记录(2) → 关注列表(3,隐藏) → 全部持仓(4,隐藏);
- 现状实现:`addStrategy/removeStrategy` 联动 `appendStrategyTab/removeStrategyTab`(R-011);字段编辑 = 设置页「策略分组」子 tab 内 `StrategyFieldsEditor`,保存走 `strategies/update` 整表。
## 定稿结论
### 1. 策略 tab 静态化(Q1
- 策略**集合与名称固定**(插件内置两项,语义等同 `BUILTIN_TABS`):**不可新增、不可删除、不可重命名**;
- **保留**「Tab 设置」里的**显隐开关 + 拖拽排序**(策略 tab 与内置 tab 同权混排)——老师现有偏好(策略 tab 置顶、全部持仓/关注列表隐藏)继续有效;
- 策略 CRUD 能力与 `addStrategy / removeStrategy / appendStrategyTab / removeStrategyTab / generateStrategyId`(拼音 slug 一整套)**整体退役**。
### 2. 设置页「策略分组」子 tab 移除(Q2)
- 设置页收敛为两个子 tab:**Tab 设置 / QMT 连接配置**
- 移除策略「新增 / 重命名 / 删除」入口与删除确认弹窗。
### 3. 字段配置入口迁移(Q3 + Q4)
- 入口 = 主窗口**策略 tab 顶部、「列设置」旁的「字段配置」按钮**;每个策略 tab 管理**本策略**字段(符合「字段定义随策略」语义);
- 点击打开**独立弹层**:字段列表(展示名 / key / 类型 / 公式或默认值 / 单位 / 操作)+ 添加/编辑/删除字段 + **保存**
- 与「列设置」职责分离:列设置 = 轻量即时持久化(显隐/顺序);字段配置 = 重表单 + 保存按钮;
- 字段定义增删 → 表格列自动跟随(沿用 R-013 归一化机制,零改动)。
### 4. 保存机制(Q5,技术项 AI 定)
- 新增**单策略字段端点**(拟 `strategies/schema-update { strategyId, configSchema }`):只改本策略字段定义,避开 `strategies/update` 整表覆盖风险;
- 保存成功 → Toast + 刷新该策略数据(列与字段同步跟随)。
### 5. 存储结构简化(Q7,技术项 AI 定,技术方案阶段定形)
- `strategies` **常量化为插件内置两项**id/name 常量);
- settings 只保留可变部分:**字段定义**(按 strategyId+ **strategyColumns** 列配置覆盖;
- 存量 settingsstrategies 数组 + configSchema)读取时**归一化迁移**(不写独立迁移脚本);本次实机无自建策略,无需数据处理。
### 6. 排期(Q6
- **并入迭代 22 作第一阶段**(静态化 + 入口迁移),计算字段(R-026)作**第二阶段**——一次改到位,公式表单直接落在新入口,避免「先按设置页做一遍再整体搬迁」的返工;
- 迭代 22 的 `UI交互设计.md` **同步修订**(公式表单移入「字段配置」弹层)。
## 影响面 / 约束联动
- **约束修订**(实施时执行并记录):产品约束-002/003/004(标签体系 / 标签自定义 + 显示开关的场景收敛)、产品约束-009(Tab 统一管理中「策略条目动态化」条款)、UI约束-002(设置页子 tab 构成:移除策略分组)、UI约束-003(Tab 设置:策略行不再随 CRUD 增删)、UI约束-005(字段配置 UI 入口迁移);
- **历史需求追溯**:R-003(策略 CRUD)→ 被取代退役;R-011(Tab 统一管理)→ 策略条目动态化部分被取代,显隐/排序机制保留;R-013 → 字段模型与列机制保留,**入口条款**被取代。
## 决策记录
| 项 | 结论 | 讨论 |
|---|---|---|
| 静态化程度 | 固定不可增删改名 + **保留**显隐/排序 | Q1(采纳建议 A)|
| 设置页「策略分组」 | 整体移除 | Q2(A)|
| 入口形态 | 「列设置」旁独立「字段配置」按钮 + 独立弹层 | Q3(A)|
| 「统一」范围 | 每个策略 tab 管自己的字段 | Q4(A)|
| 保存机制 | 单策略字段端点(AI 定,技术项)| Q5 |
| 排期 | 并入迭代 22 第一阶段,一次改到位 | Q6(A)|
| 存储结构 | strategies 常量 + settings 存字段/列配置(AI 定,方案阶段定形)| Q7 |
## 讨论过程
1. 2026-09-10 老师提出重构想法(策略 tab 静态化 + 字段编辑功能迁到策略 tab 内、列设置旁);
2. AI 核实实机现状(settings.yaml 策略与字段、store.db 数据归属、tabs 顺序)→ 结论:仅两个内置策略、无自建策略,**零数据迁移风险**;
3. AI 给出影响面分析(tab 体系 / 设置页 / 字段配置 / 约束与历史需求)+ 与迭代 22 的撞位提示(公式表单落在设置页,入口一搬即废)+ 排期建议;
4. 老师逐项拍板 Q1~Q4、Q6(全采建议);Q5/Q7 为技术项由 AI 定;
5. 转正定稿:T-011 草稿 → R-027(草稿文件已清理,索引同步);迭代 22 结构与交互设计同步修订。
## 关联
- 上游:T-011 草稿、实机核实(`settings.yaml` + `store.db`);
- 联动:**R-026 / 迭代 22**(入口迁移后,计算字段的公式表单落在「字段配置」弹层内);R-013(字段模型 / 列机制沿用)、R-011(Tab 显隐排序机制沿用);
- 取代:R-003、R-011(部分)、R-013(入口条款)。
+54
View File
@@ -0,0 +1,54 @@
# 需求:R-028 判定型计算字段 + 网格超市「可下空单 / 可下多单」· 已定稿
> 登记:2026-09-10 来源:老师指令(2026-09-10)| 状态:**已定稿(2026-09-10 老师逐项确认 Q1-Q5**
> 归属:**23-计算字段判定型扩展** | 实现状态:**已实现(2026-09-10),待老师人工验收**
> 前身:草稿 T-012(已转正,草稿文件已清理)| **能力扩展**:R-026(计算字段,原「首批结果只做数字」)→ 视同修订 产品约束-014
## 诉求(老师,2026-09-10
> 我想给网格超市添加一个计算字段,即当天涨停值 > 当前基准值 + 1 ,即可以下空单;当天跌停值 < 当前基准值 - 1,可以下多单。
## 现状限制(R-026 能力边界)
计算字段原**只产出数字**(产品约束-014「首批结果只做数字」),公式能力 = 四则 + 括号 + round/abs/min/max
**没有比较运算** → 上述两条规则无法直接表达为判定列。可用变量已覆盖全部原料
`涨停价`/`跌停价` = QuoteHub 合约信息按交易日;`基准值` = 网格超市手填字段)。
## 定稿结论(2026-09-10 老师拍板)
1. **方案 A|判定型计算字段**(Q1):引擎新增比较运算与逻辑组合,字段表单新增「结果类型:数字 / 判定」;
2. **判定列显示**Q2):满足 → `✓ <字段名>`(success 绿);不满足 / 缺值 → `—`tertiary 灰);
3. **支持 and / or 组合**Q3,含 `&&` / `||` 与全角 `≥ ≤ ≠`);
4. **结果类型由字段表单显式声明**(Q4),服务端校验「声明与公式形态一致」(判定=顶层比较/逻辑,数字=其余);
5. **阈值引用「网格大小」字段**Q5):公式写 `涨停价 > 基准值 + 网格大小`(阈值可配;当前各行网格大小=1,与写死 1 等价)。
**网格超市落地字段(两条)**
| 展示名 | 类型 | 结果 | 公式 | 单位 |
|---|---|---|---|---|
| 可下空单 | 计算 | 判定 | `涨停价 > 基准值 + 网格大小` | — |
| 可下多单 | 计算 | 判定 | `跌停价 < 基准值 - 网格大小` | — |
## 可行性验证(2026-09-10,真实持仓 + QMT 合约信息)
13 只网格超市当前持仓实算:**可下空单 5 只**(积成电子 8.22>8、天海防务 7.9>7、万顺新材 7.73>7、
大连热电 8.51>8、华智数媒 9.28>8);**可下多单 2 只**(中国化学 6.68<7、TCL中环 8.2<9);其余 6 只两条均不满足。
## 决策记录
| 项 | 结论 | 讨论 |
|---|---|---|
| 结果形态 | 判定型计算字段(方案 A) | Q1(老师选 A) |
| 判定显示 | 满足 `✓ 字段名`(绿)/ 不满足 `—`(灰) | Q2 |
| 组合条件 | 支持 and / or | Q3 |
| 结果类型 | 表单显式声明,服务端校验一致性 | Q4 |
| 阈值 | 引用「网格大小」字段 | Q5 |
| 存储/现算 | 结果可为布尔(`computed[key]` 布尔直通);仍不落库 | 沿用 R-026 语义 |
| 成本 | 引擎 + 校验 + 表单 + 渲染四处小改;无新增数据通路 | — |
## 关联
- 上游:R-026 / 迭代 22(计算字段:引擎、变量目录、现算、只读列);
- 修订:产品约束-014(结果类型扩展为 数字 / 判定)、技术约束-022(引擎运算集)、技术约束-024(校验规则)、UI约束-008(判定 UI);
- 迭代:23-计算字段判定型扩展(含网格超市两字段落地);
- 使用说明(用户手册):`docs/99-其他材料/计算字段公式说明.md`(变量表 / 运算符 / 函数 / 示例 / 报错对照 / 常见问题)。
+17 -6
View File
@@ -25,14 +25,22 @@
| R-011 | Tab 设置:统一管理所有 tab(内置 + 策略分组混排,显示/隐藏 + 拖动排序) | 设置页「通用设置」升级为「Tab 设置」,统一管理所有会话 tab(系统内置 + 策略分组)的唯一入口:两类混排、**拖动排序**(原生 HTML5 DnD,落点立即持久化)+ **显隐开关**;**任何 tab 均不支持重命名/删除**(策略命名/删除仍在「策略分组」子 tab);顺序/显隐统一为一份数据源(tabs 有序数组,策略行名 join strategies 自动跟随改名),删除策略联动删除对应条目;新增策略追加末尾;老配置自动迁移(旧隐藏策略迁移后显示)。**2026-09-02 定稿(Q1-Q5 确认)并完成(迭代 09 验收通过),已归档至 已完成/R-011.md** | 老师指令(2026-09-02 | P1 | 已定稿 | 2026-09-02 | 09-Tab设置统一管理 | **已实现(已归档)** |
| R-012 | UI 适配 DSH 主题(浅色 / 深色 / 跟随系统) | 神之一手 UI 适配 DSH 浅色/深色/跟随系统主题:141 处硬编码色值替换为宿主 `--dsw-*` token,随主题自动切换;不自行维护主题偏好(**暂定跟随系统**);涨跌红涨绿跌 → 宿主 state-error/success;仅色值 token 化不动布局。**2026-09-02 定稿(暂定跟随系统)并完成(迭代 10),已归档至 已完成/R-012.md** | 老师指令(2026-09-02 | P1 | 已定稿 | 2026-09-02 | 10-UI主题适配 | **已实现(已归档)** |
| R-013 | 策略自定义字段配置(定义随策略,值落库) | 策略可自定义、可扩展:每个策略在设置页「策略分组」子 tab 配置自定义字段(字段名/类型文本·数字·布尔·枚举/枚举项/单位 unit/默认值,configSchema 随策略定义存 settings);每个持仓行按所属策略的定义存一份键值对值(strategy_holdings 新增 values TEXT(JSON) 列);自定义字段作为持仓表列展示,列显隐/排序每策略独立配置(strategyColumns),单元格点击内联编辑。**2026-09-02 定稿(Q1-Q4 + D6)并完成(迭代 11 验收通过),已归档至 已完成/R-013.md** | 老师指令(2026-09-02 | P1 | 已定稿 | 2026-09-02 | 11-策略自定义字段配置 | **已实现(已归档)** |
| R-014 | 持仓内存快照(服务端 10s 定时同步,请求不再穿透 QMT) | 服务端建全量持仓**内存快照**(不落库):PositionSync 启动预热 + 每 10s 全量拉 QMT → 校验 → 整体替换;同步失败保留上次快照(QMT 抖动不再白屏);空快照双重确认(/health + getAsset 账户身份)才接受为真清仓;快照为空读穿透兜底(当场拉一次并回填);**幽灵持仓自动清仓**:QMT 连续 3 轮(约 30s)消失的 code,本地全部策略当前持仓自动转历史(账户身份守卫:accountId 未知跳过、切换重置跳过;部分减持不触发);strategy-positions / unallocated / summary 三个接口改读快照,前端零改动。**2026-09-02 定稿(内存不落库 / 读穿透兜底 / 幽灵自动清仓 / 同步时间不显示,老师逐项拍板),2026-09-02 完成(回归 34/34 + r013 回归 21/21 + typecheck + build 通过),迭代 12 验收待老师人工确认** | 架构梳理讨论引出 + 老师指令(2026-09-02 | P1 | 已定稿 | 2026-09-02 | 12-持仓内存快照 | 已实现(待验收) |
| R-015 | 盘口数据内存化(QuoteSync/QuoteHub 统一管理)+ 数据同步指示灯 | QuoteSync(取数:prime + 5s REST 刷新 + 涨停跌停按交易日缓存)/ QuoteHub(存查:内存快照 + watch 集合 + 读穿透 + 价格单一入口)替换 MarketFeed/MarketDataHub(方案 A 替换不包壳);WS 通路移除;market_quotes_cache 表退役 DROP;涨停价/跌停价字段(/data/instrument 按交易日缓存不落库);会话头部新增持仓/盘口同步指示灯(绿/黄/灰 + 悬停详情 + 点击即 syncNow+ sync-status 端点(吸收临时诊断 market-stats);策略持仓表行情列扩展(涨停/跌停/今开/最高 4 列,默认隐藏)。**2026-09-02 定稿(五项老师逐项拍板),2026-09-02 完成(test-quote-sync 全绿 + 存量回归 34/34、21/21 + typecheck + build),迭代 13 验收待老师人工确认** | 架构演进讨论引出 + 老师指令(2026-09-02 | P1 | 已定稿 | 2026-09-02 | 13-盘口内存快照 | 已实现(待验收) |
| R-016 | 策略 tab 历史持仓展示(显示/隐藏已清仓持仓 + 清仓时间范围筛选) | 两个策略 tab(做T/网格超市)title 旁添加:① 显示/隐藏已清仓历史持仓的开关;② 显示历史时默认展示近一周清仓的持仓,并提供 近1个月/近3个月/近半年/近1年 范围选项。背景:卖出清仓后幽灵清仓(R-014)把持仓行转历史,策略 tab 当场少行(2026-09-07 大连热电做T卖出实际发生);历史数据一直在库(strategy_holdings closed_at 非空),只缺展示入口;历史行可复用 R-010 行展开做交易复盘。**2026-09-07 定稿(Q1-Q7 老师拍板:Q2 改判「份额显示 0 就好,不改 closeHolding 语义」,其余按 AI 建议;核心目标=追溯该持仓相关的历史操作)**;历史行复用 R-010 展开做交易复盘;新增 strategy-holdings/history 端点,当前持仓路径零改动。**2026-09-07 二轮补充(Q8-Q9 老师拍板):当天(本地自然日 00:00 起)已清仓的持仓默认显示(range='today' 恒显示层),「历史持仓」开关只控制今天之前的历史(历史持仓关≠当天清仓被藏起);层间按 holdingId 去重,计数不含已清仓行维持;零写路径变更维持**。详见 R-016.md | 老师指令(2026-09-07 优化功能需求讨论) | P1 | 已定稿 | 2026-09-07 | 14-策略tab历史持仓展示 | 已实现(待验收) |
| R-017 | 委托归属候选纳入近期清仓持仓 | 交易记录 tab 归属候选只回当前持仓——清仓后当日委托失去下拉锚点无法补关联(R-016 关联观察立项,2026-09-07 大连热电实际发生:盘中归属后被清为未关联,下拉已无做T候选)。**2026-09-07 定稿(老师指令确认方向)**:候选 = 当前持仓 + 近 7 天清仓持仓(closed 标记,选项文本「已清仓」后缀),下拉 value 改 holdingId 键控(同策略多轮不撞值),排序当前在前/清仓按 closed_at DESC;当日数据修复经 set-attribution 通道完成(非固化功能)。**2026-09-08 R-018 取代标注:7 天清仓候选退役(R-018 T2 拍板)**——归属候选改策略级 attribution-targets(迭代 16 实施),本需求「清仓后补关联」痛点根因(幽灵清仓抢先归档)已由数据域分界消除;holdingId 键控/占位安全逻辑保留。详见 R-017.md | 老师指令(2026-09-07 | P1 | 已定稿 | 2026-09-07 | 15-归属候选纳入清仓持仓 | 已实现(被 R-018 语义取代 |
| R-014 | 持仓内存快照(服务端 10s 定时同步,请求不再穿透 QMT) | 服务端建全量持仓**内存快照**(不落库):PositionSync 启动预热 + 每 10s 全量拉 QMT → 校验 → 整体替换;同步失败保留上次快照(QMT 抖动不再白屏);空快照双重确认(/health + getAsset 账户身份)才接受为真清仓;快照为空读穿透兜底(当场拉一次并回填);**幽灵持仓自动清仓**:QMT 连续 3 轮(约 30s)消失的 code,本地全部策略当前持仓自动转历史(账户身份守卫:accountId 未知跳过、切换重置跳过;部分减持不触发);strategy-positions / unallocated / summary 三个接口改读快照,前端零改动。**2026-09-02 定稿(内存不落库 / 读穿透兜底 / 幽灵自动清仓 / 同步时间不显示,老师逐项拍板),2026-09-02 完成(回归 34/34 + r013 回归 21/21 + typecheck + build 通过),迭代 12 验收待老师人工确认** | 架构梳理讨论引出 + 老师指令(2026-09-02 | P1 | 已定稿 | 2026-09-02 | 12-持仓内存快照 | **已实现(已归档至 已完成/,2026-09-08 老师归档指令确认验收)** |
| R-015 | 盘口数据内存化(QuoteSync/QuoteHub 统一管理)+ 数据同步指示灯 | QuoteSync(取数:prime + 5s REST 刷新 + 涨停跌停按交易日缓存)/ QuoteHub(存查:内存快照 + watch 集合 + 读穿透 + 价格单一入口)替换 MarketFeed/MarketDataHub(方案 A 替换不包壳);WS 通路移除;market_quotes_cache 表退役 DROP;涨停价/跌停价字段(/data/instrument 按交易日缓存不落库);会话头部新增持仓/盘口同步指示灯(绿/黄/灰 + 悬停详情 + 点击即 syncNow+ sync-status 端点(吸收临时诊断 market-stats);策略持仓表行情列扩展(涨停/跌停/今开/最高 4 列,默认隐藏)。**2026-09-02 定稿(五项老师逐项拍板),2026-09-02 完成(test-quote-sync 全绿 + 存量回归 34/34、21/21 + typecheck + build),迭代 13 验收待老师人工确认** | 架构演进讨论引出 + 老师指令(2026-09-02 | P1 | 已定稿 | 2026-09-02 | 13-盘口内存快照 | **已实现(已归档至 已完成/,2026-09-08 老师归档指令确认验收)** |
| R-016 | 策略 tab 历史持仓展示(显示/隐藏已清仓持仓 + 清仓时间范围筛选) | 两个策略 tab(做T/网格超市)title 旁添加:① 显示/隐藏已清仓历史持仓的开关;② 显示历史时默认展示近一周清仓的持仓,并提供 近1个月/近3个月/近半年/近1年 范围选项。背景:卖出清仓后幽灵清仓(R-014)把持仓行转历史,策略 tab 当场少行(2026-09-07 大连热电做T卖出实际发生);历史数据一直在库(strategy_holdings closed_at 非空),只缺展示入口;历史行可复用 R-010 行展开做交易复盘。**2026-09-07 定稿(Q1-Q7 老师拍板:Q2 改判「份额显示 0 就好,不改 closeHolding 语义」,其余按 AI 建议;核心目标=追溯该持仓相关的历史操作)**;历史行复用 R-010 展开做交易复盘;新增 strategy-holdings/history 端点,当前持仓路径零改动。**2026-09-07 二轮补充(Q8-Q9 老师拍板):当天(本地自然日 00:00 起)已清仓的持仓默认显示(range='today' 恒显示层),「历史持仓」开关只控制今天之前的历史(历史持仓关≠当天清仓被藏起);层间按 holdingId 去重,计数不含已清仓行维持;零写路径变更维持**。已归档至 已完成/R-016.md | 老师指令(2026-09-07 优化功能需求讨论) | P1 | 已定稿 | 2026-09-07 | 14-策略tab历史持仓展示 | **已实现(已归档至 已完成/,2026-09-08 老师归档指令确认验收)** |
| R-017 | 委托归属候选纳入近期清仓持仓 | 交易记录 tab 归属候选只回当前持仓——清仓后当日委托失去下拉锚点无法补关联(R-016 关联观察立项,2026-09-07 大连热电实际发生:盘中归属后被清为未关联,下拉已无做T候选)。**2026-09-07 定稿(老师指令确认方向)**:候选 = 当前持仓 + 近 7 天清仓持仓(closed 标记,选项文本「已清仓」后缀),下拉 value 改 holdingId 键控(同策略多轮不撞值),排序当前在前/清仓按 closed_at DESC;当日数据修复经 set-attribution 通道完成(非固化功能)。**2026-09-08 R-018 取代标注:7 天清仓候选退役(R-018 T2 拍板)**——归属候选改策略级 attribution-targets(迭代 16 实施),本需求「清仓后补关联」痛点根因(幽灵清仓抢先归档)已由数据域分界消除;holdingId 键控/占位安全逻辑保留。已归档至 已完成/R-017.md(被 R-018 取代退役) | 老师指令(2026-09-07 | P1 | 已定稿 | 2026-09-07 | 15-归属候选纳入清仓持仓 | **已实现(已归档至 已完成/被 R-018 语义取代退役,2026-09-08** |
| R-018 | 交易关联驱动持仓份额动态调整(归属 = 账本写操作) | 从交易记录出发给委托关联策略/持仓时自动同步调整该策略下持仓份额(关联即账本写操作):策略级候选 + 统一「份额分配器」(一笔委托拆 0..N 段 × 策略×量,余额默认全量可改小,允许部分关联)+ 买入加仓/建新仓(不复活旧行)、卖出减仓/归零清仓、仓不足自动截断续分、无仓关联失败;撤段逆操作(撤建仓买入=持仓作废第三态不产生假清仓历史、撤加仓=减回归零作废、撤卖出=恢复活动仓+份额加回,粒度=段、改归属原子);**数据域分界(老师拍板):幽灵清仓等同步机制只作用于「全部持仓」对账单域,不再写 strategy_holdings 账本**——账本生命周期只由交易关联 + 手动份额操作驱动,账本转历史唯一途径=卖出单关联 closeHoldingR-017 7 天清仓候选退役(holdingId 键控/占位保留);漏关联软提示(账本有活动行+QMT 无此 code)。影响既有定稿:技术约束-017/产品约束-011/R-014/迭代12 幽灵清仓条款修订。详见 R-018.md | 老师指令(2026-09-08 新优化需求讨论) | P1 | 已定稿 | 2026-09-08 | 16-交易关联驱动持仓份额动态调整 | **已实现(已归档至 已完成/R-018.md2026-09-08 验收通过)** |
| R-019 | 策略会话:会话以策略当前数据为依据做复盘/分析讨论 | 让 AI 会话以策略为 Workspace(老师原话)→ **已定稿(2026-09-08F1/F2 拍板)**:改造现有 DSH 对话窗口,对话视图首条消息前提供「讨论策略」选择(仿 DSH workspace chip Choose 体验:依据:做T ▾),选策略即把该策略**当前数据摘要**预注入会话上下文开始聊,会话中可换策略/刷新再注入;**不用 DSH 宿主工作区**(R3);注入 = 当前持仓明细 + 全历史关联交易聚合摘要(含已清仓,F2);只读不开放账本写(Q4=A);产物不落盘(R2-Q4,复盘管理/落库归未来 T-002)。宿主注入通道等技术调研列计划阶段待办。详见 R-019.md | 老师指令(2026-09-08 | P1 | 已定稿 | 2026-09-08 | 17-策略会话 | 已立项(迭代 17 设计阶段) |
| R-019 | 策略会话(历史条目,已并入 R-021) | 原“会话以策略为 Workspace(数据依据版)”需求(2026-09-08 定稿)——**2026-09-08 老师指令与 R-021 合并,逻辑整合入合并后的 R-021(策略会话复盘)**;本条目保留为合并前讨论史存档,文件已归档至 已完成/R-019.md | 老师指令(2026-09-08 | P1 | 已定稿 | 2026-09-08 | 17-策略会话 | **已归档(并入 R-021 合并需求,2026-09-08** |
| R-020 | QMT Bridge MCP 能力内建(神之一手自注册 MCP,去三方插件依赖) | 当前 DSH 的 QMT Bridge MCP 由三方插件 dsh-skill-mcp-panel 写入 profile cordis.patch.yml 受管块。**2026-09-08 定稿(Q1-Q6 老师逐项拍板)**:神之一手 apply 内动态挂载 **DSH 官方 dsh-mcp-client**(**只挂当前激活连接**serverName 固定 QMT_Bridge_MCPurl 自动派生 baseUrl+/mcp;挂载/切换/卸载/状态由神之一手插件内管理),MCP 能力内建;**移除对象 = 三方 dsh-skill-mcp-panel**2026-09-08 老师澄清:卸载其插件 + 移除其宿主 cordis.patch.yml 受管块,官方 dsh-mcp-client 可依赖);MCP 生命周期随连接 CRUD/激活切换联动;状态出口 = 设置页连接卡片检查 + **会话头部指示灯区新增 MCP 状态灯**;失败重连对齐 failOnStartupError:false+reconnect**不做通用 MCP 管理**。REST 直连不变(技术约束-003)。详见 已完成/R-020.md | 老师指令(2026-09-08 | P1 | 已定稿 | 2026-09-08 | 18-QMTBridgeMCP内建 | **已实现(已归档,2026-09-08 验收通过)** |
| R-021 | 神之一手数据加入会话(对象目录 + 添加机制;复盘为第一用例;合并原 R-019) | **需求本质(2026-09-08 老师再定调)**:会话(agent)可获取神之一手数据,复盘只是其用法之一;本需求确定 **①数据怎么添加(暂定=A 人挑对象→只读数据上下文快照注入会话,不挂按需工具)②哪些数据可以加(对象目录候选:账户/策略/持仓(当前·历史)/轮次 round/单笔交易/持仓↔交易/行情随行/汇总…)**;首批范围 P1 策略/P2 持仓/P3 持仓↔交易/P4 单笔委托/P5 轮次(待建模)/P6 行情随行(暂定草案,老师可增删);复用为复盘/策略依据等用例;不用宿主工作区、只读、结论不落盘(未来 Phase1-3 预留)。详见 R-021.md | 老师指令(2026-09-08 | P1 | 已定稿 | 2026-09-08 | 17-策略会话 | 已立项(设计阶段) |
| R-022 | QMT 连接健康灯自适应探测(Bridge 启动/恢复后快速回绿) | 老师反馈:启动 QMT Bridge 后持仓/行情/MCP 灯几秒点亮,QMT 灯一直不亮(自动检查没生效)。根因:QmtHealthMonitor 固定每 5 分钟探测一次 /health 并缓存,恢复感知滞后;其余灯有 5-10s 高频自愈回路。**方案(2026-09-09 AI 提议,老师确认):自适应探测节奏——健康 5 分钟稳态(不变)/ 异常未知 10s 快速重试**(与前端 10s 轮询对齐),Bridge 启动后灯 ≤20s 自动回绿;边界:API/前端零改动、不新增配置、健康稳态不空转。详见 R-022.md | 老师反馈(2026-09-09 | P1 | 讨论中(AI 提议定稿,待老师确认) | 2026-09-09 | 19-指示灯状态自适应探测 | 已实现(待验收,验收通过后定稿归档) |
| R-023 | MCP 状态灯自适应探测(MCP 服务恢复后自动回绿) | 老师反馈:R-022 修复后复查发现 MCP 灯同样不自动恢复。根因:QmtMcpManager 状态缓存只在 挂载/切换/手动探测 时刷新、无定时回路;dsh-mcp-client 自带 reconnect 只恢复真实连接(fiber 层),不刷新插件状态缓存。**方案(2026-09-09 AI 提议,与 R-022 同模式):connected → 5 分钟稳态 / 非 connected → 10s 快速重试**MCP 服务恢复后灯 ≤20s 自动回绿;无激活连接/dispose 不调度;探测本体与挂载/重连逻辑零改动。详见 R-023.md | 老师反馈(2026-09-09 | P1 | 讨论中(AI 提议定稿,待老师确认) | 2026-09-09 | 19-指示灯状态自适应探测 | 已实现(待验收,验收通过后定稿归档) |
| R-024 | 策略 tab 列设置改为拖拽排序(列设计细节优化) | 老师反馈:策略 tab 列设置弹层目前 **↑↓ 箭头排序**,希望改成**鼠标选中后拖动排序**。**2026-09-10 定稿(老师确认方案 A)**:策略 tab「列设置」弹层内实现 **原生 HTML5 拖拽排序**(复用 R-011 Tab 设置已验证模式:≡ 拖拽手柄 + drop 重排);**落点立即持久化**strategy-columns/update 整表覆盖,弹层去掉「保存」按钮只留「关闭」);**↑↓ 箭头保留作兜底**;固定列不参与排序维持现状。**2026-09-10 验收反馈修订**:落点指示从整行高亮改为**行间间隙高亮线**(按鼠标在目标行内 Y 坐标判定插入前/后)。详见 已完成/R-024.md | 老师反馈(2026-09-10 | P2 | 已定稿 | 2026-09-10 | 20-策略列设置拖拽排序 | **已实现(已归档至 已完成/R-024.md2026-09-10 老师确认验收)** |
| R-025 | 设置页「Tab 设置」排序落点统一为间隙高亮线 | 老师指令(迭代 20 复盘遗留第 2 项):设置页「Tab 设置」分组 tab 排序目前仍是整行高亮(overId 行淡蓝底),与列设置弹层的**行间间隙高亮线**(R-024/UI约束-007)观感不一致,统一为同款形式。**2026-09-10 定稿(老师指令)**:拖动悬停按目标行内 Y 坐标上下半区判定,间隙线画目标行上缘(插前)/下缘(插后);drop 事件内按坐标重算插入位置;拖回原位置跳过写库;onDragEnd 清理;移除整行高亮;≡ 手柄/显隐开关/立即持久化/徽标/saving 禁用全部维持不变;服务端零改动。详见 R-025.md | 老师指令(2026-09-10 | P2 | 已定稿 | 2026-09-10 | 21-Tab设置排序间隙高亮统一 | 未开始 |
| R-026 | 策略计算字段(公式字段) | R-013 字段类型扩展:新增第五种类型「计算(formula)」——用户写公式(**中文变量名**,如 `(现价 - 成本价) × 份额`),公式引用**服务端缓存数据集**按持仓行实时算出列值。**2026-09-10 定稿(老师逐项确认)**:① 数据集首批=行情+合约信息/持仓账本(份额·成本价·最后一笔成交价)/本行自定义字段值(QMT账户域、交易记录聚合后补);② 计算位置=服务端(strategy-positions 现算随行返回);③ 粒度=仅行级(策略级聚合二期);④ 引擎=自写小型表达式解析器(零依赖、支持中文标识符、四则+括号+round/abs/min/max);⑤ 不落库(不写 values 列),列只读;⑥ 只允许引用非公式字段(零循环依赖);⑦ 结果=数字+unit+小数位(默认2);⑧ 保存时校验语法与变量(拒非法)+ 运行期缺数据显示 —;⑨ 配置UI=公式输入+变量选择器(+试算预览);⑩ 历史持仓行显示 —(不做历史时点计算);⑪ 轻量变量目录先行,作为 T-006 数据池第一块砖。已知用例:浮动盈亏/盈亏比例/距涨停/网格占用。详见 R-026.md | 老师指令(2026-09-10 功能讨论) | P1 | 已定稿 | 2026-09-10 | 22-策略计算字段与策略tab静态化(PLAN-019,第二阶段) | **已实现(2026-09-10),待老师人工验收** |
| R-027 | 策略 tab 静态化 + 字段配置入口迁移(重构) | ① 策略 tab 从「动态增删改」改为**插件内置静态 tab**(与全部持仓/交易记录同权;**不可增删改名**,但保留 Tab 设置里的显隐+拖拽排序);② 设置页「策略分组」子 tab **整体移除**(设置页收敛为 Tab 设置 / QMT 连接配置);③ 字段编辑功能迁到**主窗口策略 tab 顶部、「列设置」旁的「字段配置」按钮 + 独立弹层**(每个策略 tab 管自己的字段),保存走新增单策略字段端点(避开整表覆盖);④ strategies 常量化为内置两项,settings 只存字段定义 + 列配置覆盖(存量归一化,实测无自建策略、零迁移风险);⑤ **并入迭代 22 作第一阶段**(计算字段为第二阶段,公式表单直接落新入口,避免返工)。**取代**:R-003(策略 CRUD)、R-011(策略条目动态化部分)、R-013(字段定义入口条款)。详见 R-027.md | 老师指令(2026-09-10 重构想法) | P1 | 已定稿 | 2026-09-10 | 22-策略计算字段与策略tab静态化(PLAN-019,第一阶段) | **已实现(2026-09-10),待老师人工验收** |
| R-028 | 判定型计算字段 + 网格超市「可下空单 / 可下多单」 | R-026 能力扩展:计算字段新增**结果类型 = 数字 / 判定**(引擎加比较运算 `> < >= <= == !=` 与逻辑 `and / or`,含 `&& ||` 与全角 `≥ ≤ ≠`;字段表单显式声明结果类型,服务端校验「声明与公式形态一致」);判定列满足显示 `✓ <字段名>`(绿)、不满足/缺数据显示 `—`(灰)。**网格超市落地**:可下空单 = `涨停价 > 基准值 + 网格大小`;可下多单 = `跌停价 < 基准值 - 网格大小`(阈值引用字段可调)。**2026-09-10 定稿并实现**(老师拍板 Q1-Q5:方案 A 判定型 / ✓ 与 — / 支持 and·or / 结果类型显式 / 阈值引字段);真实验证:13 只持仓中可下空单 5 只、可下多单 2 只;回归 test-r026 48→71 条全绿。详见 R-028.md | 老师指令(2026-09-10 | P1 | 已定稿 | 2026-09-10 | 23-计算字段判定型扩展 | **已实现(2026-09-10),待老师人工验收** |
## 渐进明细规划素材
@@ -48,4 +56,7 @@
| T-006 | 数据池中间层 + 策略表格动态字段配置 | 服务端数据池(数据集中间层:按 code 聚合宽表、多源字段映射、填充器架构)+ 策略持仓表格动态字段配置(列配置×池行渲染)。源自 R003 讨论(2026-08-28)拆分独立,草稿文件:`草稿/数据池中间层与策略表格动态字段配置.md` | 起草 |
| T-007 | RSS 订阅管理 | 管理 RSS 订阅源(增删改查)+ 抓取聚合订阅内容,作为资讯/消息来源;支撑 目标-003 市场监控、目标-004 消息管理,与 T-004 同属消息链路。**2026-08-28 老师确认先放草稿**,草稿文件:`草稿/RSS订阅管理.md` | 起草 |
| T-008 | 数据存储管理(JSON → SQLite | 数据存储从 JSON data store 升级为 SQLitenode:sqlite)数据库 + 数据存储管理能力;**变更既有设计约束**(数据存储设计.md「无需数据库」决策)。**2026-09-01 定稿转正为 R-008 并完成(迭代 06** | 已转需求 R-008 |
| T-009 | 策略自定义字段配置(策略细节配置) | 每个策略支持自定义字段(键值对),不同策略按自身需要添加各自的细节配置字段(如网格超市配网格间距/单格金额、长线持有配目标价/仓位上限)。**2026-09-02 定稿转正为 R-013**,草稿文件已清理 | 已转需求 R-013 |
| T-009 | 策略自定义字段配置(策略细节配置) | 每个策略支持自定义字段(键值对),不同策略按自身需要添加各自的细节配置字段(如网格超市配网格间距/单格金额、长线持有配目标价/仓位上限)。**2026-09-02 定稿转正为 R-013**,草稿文件已清理 | 已转需求 R-013 |
| T-010 | 策略计算字段(公式字段) | R-013 字段类型扩展:新增「计算(formula)」类型,用户写公式引用服务端缓存数据集(行情快照/合约信息/持仓账本/自定义字段值),按持仓行实时计算列值(不落库)。源自 2026-09-10 功能讨论(老师指令),**2026-09-10 定稿转正为 R-026**,草稿文件已清理 | 已转需求 R-026 |
| T-011 | 策略 tab 静态化 + 字段配置入口迁移 | ① 策略 tab 改为与「全部持仓/交易记录」同类的**插件内置 tab**(不再动态增删改);② 原设置页「策略分组」的字段编辑功能迁到**主窗口策略 tab 内、「列设置」旁**,作为统一的字段配置管理。实机核实:当前仅两个内置策略、无自建策略(零数据迁移风险)。与迭代 22(计算字段)强联动。源自 2026-09-10 老师重构想法,**2026-09-10 定稿转正为 R-027**,草稿文件已清理 | 已转需求 R-027 |
| T-012 | 网格超市「可下空单 / 可下多单」判定字段 | 用计算字段把网格规则翻译成列判定:**可下空单** = 当天涨停价 > 基准值 + 1;**可下多单** = 当天跌停价 < 基准值 - 1。现状限制:R-026 计算字段**只产数字**(无比较运算)→ 需扩展「判定型」结果。已验证数据通路(涨跌停取自合约信息 + 基准值取自持仓行):13 只持仓中 5 只可下空单、2 只可下多单。源自 2026-09-10 老师指令,**2026-09-10 定稿转正为 R-028**,草稿文件已清理 | 已转需求 R-028 |
@@ -0,0 +1,151 @@
# 计算字段公式说明
> 适用:策略 tab →「字段配置」→ 添加/编辑字段 → 类型选「**计算**」
> 版本:2026-09-10R-026 计算字段 + R-028 判定型扩展)| 本文档与实现行为逐条对齐(已实机核对)
## 一、一句话
计算字段 = **你写一个公式**,服务端按**每个持仓行**实时算出一个值填到该列:公式里可以引用
**行情 / 合约信息 / 持仓账本 / 本策略手填字段**四类数据。值**不落库**、列**只读**、数据缺失时显示 `—`
## 二、结果类型(先选这个)
字段表单「**结果**」下拉决定列的呈现方式,且**必须与公式形态一致**(服务端校验,不一致直接拒绝保存):
| 结果类型 | 公式形态 | 列上显示 | 可配项 |
|---|---|---|---|
| **数字**(默认) | 算式,如 `(现价 - 成本价) × 份额` | `700.00 元`(小数位 + 单位) | 小数位 0–4(默认 2)、单位 |
| **判定** | 顶层是比较,如 `涨停价 > 基准值 + 网格大小` | 满足 → `✓ 可下空单`(绿);不满足/缺数据 → `—`(灰) | 单位、小数位不适用(自动隐藏) |
- 判定列文字 = 该字段的**展示名**(所以展示名建议直接写成规则名,如「可下空单」);
- 「不满足」与「数据缺失」目前**都显示 `—`**(避免把「没数据」误读成「不满足」)。
## 三、可用变量(写中文名即可;点「+ 插入变量」自动插入)
| 分组 | 变量 | 含义 / 来源 |
|---|---|---|
| 行情 | `现价` `昨收` `今开` `最高` `最低` `成交量` `成交额` | 盘口内存快照(服务端 5s 刷新) |
| 合约 | `涨停价` `跌停价` | 合约信息(按交易日,当天不变) |
| 持仓 | `份额` `成本价` `最后成交价` | 持仓账本:本策略分配份额 / QMT 成本价 / 最后一笔实际成交价 |
| 自定义字段 | 本策略**手填字段的展示名**(如 `基准值` `网格大小` `网格交易量` | 该持仓行手填的值 |
规则与注意:
1. **变量名 = 内置名(上表固定 12 个)或手填字段的展示名**。手填字段**改名后**,引用它的公式会失效
(该列变 `—`,再保存字段时会提示「未知变量」),把公式里的变量名同步改成新名字即可;
2. 字段展示名**不得与内置变量名冲突**(不能建一个叫「现价」的字段),同策略内展示名不得重复;
3. **计算字段不能被另一个公式引用**(零循环依赖):公式里写另一个计算字段的名字会报「未知变量」;
4. 手填字段的**非数字值**参与运算按「无值」处理(空文本 → 该行 `—`;布尔按 1/0)。
## 四、运算符
| 类别 | 写法 | 说明 |
|---|---|---|
| 四则 | `+ - * / ` | 全角 `× ÷ ` 同样可用 |
| 括号 | `( )` | 全角 ` ` 同样可用 |
| 比较 | `> < >= <= == !=` | 全角 `≥ ≤ ≠ ` 同样可用;`==` 是**精确相等**(无容差) |
| 逻辑 | `and` / `or`(也可 `&&` / `||` | 判定型专用,如 `A > 1 and B < 2` |
| 一元负 | `-现价` | — |
**优先级**(低 → 高):`or``and` → 比较 → 加减 → 乘除 → 一元负 → 括号。拿不准就加括号,最稳。
## 五、函数(只有这 4 个)
| 函数 | 参数 | 例子 | 结果 |
|---|---|---|---|
| `round(值, 位数)` | 1–2 个;位数 0–10(负数按 0 处理) | `round(106.666, 2)` | `106.67` |
| `abs(值)` | 恰好 1 个 | `abs(成本价 - 现价)` | `0.7` |
| `min(...)` | ≥1 个 | `min(涨停价, 现价 * 1.05)` | 取小 |
| `max(...)` | ≥1 个 | `max(0, 现价 - 成本价)` | 取大(不小于 0 |
## 六、计算规则与「缺值」语义
- **缺值短路**:公式里**任一变量没有数据**(无行情、无合约信息、手填字段为空)→ 整行结果为空 → 列显示 `—`
(不会显示 0 或 NaN,也不会报错、不会炸表);
- **除零 / 结果非有限数** → `—`
- **浮点归一**:自动消除浮点噪声(不会出现 `0.7000000000000002` 这种值);
- **限制**:公式 ≤ **500 字符**、嵌套 ≤ **32 层**(超出提示「公式过长 / 过于复杂」);
- **安全**:自写解析引擎,不支持 `eval`,除四则/比较/逻辑/这 4 个函数外没有任何能力(不能读写文件或网络)。
## 七、列上怎么显示
| 场景 | 显示 |
|---|---|
| 数字型有值 | 按「小数位 + 单位」显示(如 `6.67 %``700.00 元`;单位留空则只显示数字) |
| 判定型满足 | `✓ <字段名>`(绿),如 `✓ 可下空单` |
| 判定型不满足 / 数字型无值 | `—`(灰) |
| 已清仓的历史持仓行 | `—`(不做历史时点计算) |
| 表头 | 列名前带 `ƒ` 标记;悬停列头/单元格可见公式原文(`= 公式` |
| 刷新 | 随策略持仓接口轮询刷新(行情服务端 5s 更新;列值是「计算快照」,不做价格闪烁) |
## 八、常用公式(可直接抄)
### 网格超市
| 用途 | 结果类型 | 公式 |
|---|---|---|
| 可下空单 | 判定 | `涨停价 > 基准值 + 网格大小` |
| 可下多单 | 判定 | `跌停价 < 基准值 - 网格大小` |
| 触到上边界 | 判定 | `现价 >= 网格上边界` |
| 跌破下边界 | 判定 | `现价 <= 网格下边界` |
| 距上边界 % | 数字(单位 %) | `(网格上边界 - 现价) ÷ 现价 × 100` |
| 每格金额 | 数字(单位 元) | `网格交易量 × 网格大小` |
| 本行网格占用 | 数字(单位 元) | `现价 × 份额` |
| 空单且未触上边界(组合) | 判定 | `涨停价 > 基准值 + 网格大小 and 现价 < 网格上边界` |
### 手动做T / 通用
| 用途 | 结果类型 | 公式 |
|---|---|---|
| 浮动盈亏 | 数字(元) | `(现价 - 成本价) × 份额` |
| 盈亏比例 | 数字(% | `round((现价 ÷ 成本价 - 1) × 100, 2)` |
| 市值 | 数字(元) | `现价 × 份额` |
| 距涨停 % | 数字(% | `(涨停价 - 现价) ÷ 现价 × 100` |
| 成本上方空间 | 数字(元) | `max(0, 现价 - 成本价) × 份额` |
| 盈利中 | 判定 | `现价 > 成本价` |
| 成本上方且未涨停 | 判定 | `现价 > 成本价 and 现价 < 涨停价` |
| 相对 T 仓成本盈亏 | 数字(元) | `(现价 - T仓成本价) × 份额` |
## 九、保存校验与报错对照
保存字段时服务端会**先校验语法与变量**,不通过直接拒绝(公式框描红 + 提示,表单不关闭):
| 提示 | 原因 | 怎么办 |
|---|---|---|
| `公式不能为空` | 公式为空 | 填公式 |
| `公式不完整` / `缺少右括号` / `意外的符号` / `无法识别的字符` | 语法错 | 检查括号与运算符是否写全 |
| `未知变量『xx』——该字段已删除或名称有误` | 变量名不对(或引用了计算字段) | 用「+ 插入变量」插入,或核对字段展示名 |
| `未知函数『xx』(可用:round/abs/min/max` | 用了未实现的函数 | 只用这 4 个 |
| `函数『xx』参数个数不对` | 参数多/少 | 见第五节 |
| `变量名『现价』与内置变量冲突,请修改字段展示名` | 字段名撞内置变量 | 改展示名 |
| `字段展示名重复: xx` | 同策略两字段同名 | 改其一 |
| `公式过长(上限 500 字符)` / `公式过于复杂(嵌套过深)` | 超限 | 拆成两个字段 |
| `结果类型选了「判定」,但公式没有比较运算(如 涨停价 > 基准值 + 1)` | 结果类型与公式形态不符 | 改结果类型为「数字」,或把公式写成比较 |
| `公式结果是判定,请把「结果类型」改为「判定」` | 同上(反向) | 把结果类型改为「判定」 |
配置时可点「**试算**」:服务端拿该策略**一行真实持仓**当场算给你看
(判定型显示 `✓ 满足 / — 不满足`;无持仓提示「暂无可试算的持仓数据」)。
## 十、常见问题
**Q:为什么整列都是 `—`**
按序排查:① 该 code 暂无行情/合约快照(等 5s 同步,或点会话头部行情灯立即同步);
② 公式用到的手填字段该行没填值;③ 公式里有除零;④ 公式引用了已删除/改名的字段。
**Q:能不能写条件分支(三态:空 / 多 / 观望)?**
目前不支持 `if`/三元表达式;判定型只覆盖「满足 / 不满足」两态。要一列表达三态时,
可先用两个判定字段(`可下空单` + `可下多单`)代替 —— 两个都为 `—` 即观望。
**Q:能不能跨行求和(本策略总盈亏)?**
不能——公式是**逐行**计算,只看得到本行数据。
**Q:格子能手动改吗?**
不能,计算字段**只读**;手填字段才能在格子里点击编辑。
**Q:单位参与计算吗?**
不参与,只影响显示(拼在值后);判定型不需要单位。
## 十一、暂不支持(明确边界)
条件分支(`if`/三元)、`not` 取反、判定与数字混用的单字段、跨行聚合(求和/平均)、历史持仓行时点计算、
策略以外的数据集(账户域持仓、交易记录聚合——尚未接入为变量)。