迭代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:
@@ -0,0 +1,164 @@
|
||||
# 迭代 22 UI 交互设计:字段配置入口迁移 + 策略计算字段
|
||||
|
||||
> 依据:**R-027(重构·第一阶段,已定稿)** + **R-026(计算字段·第二阶段,已定稿)** + PLAN-019 | 日期:2026-09-10
|
||||
> 状态:**已定稿**(D-1~D-7 老师逐项拍板;2026-09-10 因入口迁移(R-027)同步修订;D-8~D-9 为 AI 默认项,老师可否决)
|
||||
> 视觉基线:UI约束-004(--dsw-* token)、UI约束-006(ExpandChevron)、UI约束-007(间隙高亮线);UI约束-005 待随本次修订
|
||||
> 复用:ColumnSettingsPopover(列设置)、StrategyFieldsEditor(改造为弹层内容)、Toast;FieldCellEditor 不参与(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(策略定义存储)/015(configSchema)、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 复用既有加载(L236–239)。
|
||||
|
||||
## 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` 夹取 0–4 整数;`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/trial;strategy-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**:内置策略恒两项;旧 settings(strategies[].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/21(R-022/R-023/R-024/R-025)的未提交改动,
|
||||
本轮未触碰其逻辑,仅在其上叠加;建议老师确认后一并提交。
|
||||
|
||||
## 遗留与风险
|
||||
|
||||
- **行情 miss 显示 `—`**:计算列取数只读内存缓存、不读穿透(技术约束-023);某 code 无快照时该格为空,
|
||||
等 QuoteSync 下轮(≤5s)覆盖。若老师更希望「宁可慢也要立刻有价」,可改判为读穿透。
|
||||
- **需要重载插件才能人工验收**:`~/.dsh/profiles/web/node_modules/one-divine-lot` 是指向本工作区的软链,
|
||||
lib 已构建完成;老师侧需重载插件(或重启 dsh web)后在页面验收(未由我启动任何服务)。
|
||||
- **旧 `strategies[].configSchema` 仅只读兼容**:本机字段定义在其首次保存后落入 `strategyFields`
|
||||
(不写迁移脚本、不动库数据)。
|
||||
- 自建策略若未来出现:读取忽略、tab 丢弃,其 `strategy_holdings` 数据保留在库(本机无此情况)。
|
||||
|
||||
## 待老师确认事项(人工验收)
|
||||
|
||||
按 `验收标准.md`:**A1–A11**(重构)+ **B1–B12**(计算字段),重点四条:
|
||||
1. 设置页无「策略分组」;策略 tab 恒两个、不可增删改名,但 Tab 设置仍可显隐/拖拽排序;
|
||||
2. 策略 tab 顶栏「字段配置 / 列设置」可用,字段增删保存后表格列跟随、重启后定义仍在;
|
||||
3. 计算字段可用:中文变量公式 + 变量选择器 + 试算 → 持仓列只读显示结果(`ƒ` 标记 + 悬停看公式);
|
||||
4. 边界:非法公式/未知变量/名称冲突被拒 + 描红;缺数据与历史行显示 `—`;`strategy_holdings.values` 不含计算字段键。
|
||||
|
||||
## 经验沉淀(候选)
|
||||
|
||||
- **并行委派的契约必须在开工前冻结**:本轮前端由子代理并行实现,期间服务端 `settings.js` 仍在演进
|
||||
(子代理观察到 mtime 变化、并在报告里提出「疑似工作区未冻结」)——契约冻结 + 文件集互斥是并行可行性的前提。
|
||||
- **真相源迁移要全量搜读点**:本次 `normalizeStrategyColumns` 差点漏改,靠既有回归脚本的红灯暴露;
|
||||
遗留的旧键兼容读取(而非直接删键)让迁移零风险、零脚本。
|
||||
- **测试期望也需要跟着设计演进**:`test-r013-columns` 的过期期望在迭代 20 复盘就被记录却未处理,
|
||||
本轮顺手清偿,避免「红灯常态化」。
|
||||
@@ -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-1~D-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 + B1–B12)。
|
||||
|
||||
## 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. 判定线
|
||||
|
||||
**通过(全部满足)**
|
||||
- A1–A11 与 B1–B12 全部通过;
|
||||
- 回归脚本全绿 + typecheck/build 通过;
|
||||
- 老师人工验收确认。
|
||||
|
||||
**任一即不通过**
|
||||
- 计算字段写入 `values` 列(或落任何库表);
|
||||
- 公式可执行表达式以外的行为(无 `eval/Function`、不可触达进程/文件能力);
|
||||
- 无 formula 字段的策略出现额外取数开销或行为变化(回归);
|
||||
- 既有四类字段编辑、列设置、历史持仓、份额/归属任一回归失败;
|
||||
- 策略仍可被增删改名(静态化未达成)。
|
||||
|
||||
## 8. 明确不在本次验收范围
|
||||
|
||||
- 策略级聚合(跨行求和)、QMT 账户域/交易记录作为变量源、公式引用公式、历史时点计算、布尔型公式结果、公式框实时高亮/自动补全、完整 T-006 数据池;
|
||||
- 自建策略的历史数据清理(本机无自建策略;如未来出现,另行讨论)。
|
||||
Reference in New Issue
Block a user