Compare commits
32 Commits
cba31428dc
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| ec02a791ee | |||
| 3dfa39b8bf | |||
| 8782aa9c50 | |||
| c700f2f760 | |||
| 9382ace3a0 | |||
| 6a578cc598 | |||
| 1223e2e74b | |||
| 22ec732607 | |||
| 51c70c48fb | |||
| ed43ecae39 | |||
| d793d2cf35 | |||
| 245fcad95c | |||
| 39b135fa36 | |||
| 329ac67a71 | |||
| aac16b904c | |||
| 6f74ed4a79 | |||
| a4902bb730 | |||
| f1e7e785a1 | |||
| 4ac8a29805 | |||
| 35f2ad1a23 | |||
| d122d88bce | |||
| 2aacd8a25d | |||
| 859938c4f1 | |||
| 2790b92378 | |||
| b3526c7693 | |||
| dd98b4d45b | |||
| 9b719f13ba | |||
| aef27dcd3f | |||
| 08c4ad3c58 | |||
| 8caeb769ed | |||
| bb3f8bd28d | |||
| 5a723d514e |
@@ -26,6 +26,10 @@ Thumbs.db
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Temp artifacts (AI session diagrams / scratch)
|
||||
.tmp-diagrams/
|
||||
.tmp-*/
|
||||
|
||||
# DSH / runtime
|
||||
.dsh/
|
||||
*.tmp
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
# 计划:QMT Bridge MCP 能力内建(神之一手自注册 MCP,去三方插件依赖)(阶段航点)
|
||||
|
||||
> 编号:PLAN-018 | 粒度:阶段航点 | 创建:2026-09-08 | 状态:待实施(拟迭代 18)
|
||||
> 派生自终极目标:目标-001(DSH 插件形态·复用宿主 MCP 能力)、目标-008(真实交易系统接入)
|
||||
> 依据需求:**R-020(已定稿,2026-09-08,Q1-Q6 老师拍板)** —— 符合入范围门槛
|
||||
> 设计约束:技术约束-003(REST 直连不变)、技术约束-008(连接配置存储/热切换)、产品约束-012(会话头部指示灯体系)、UI约束-002(连接卡片);沿用技术约束-004/005/006/007
|
||||
|
||||
## 目标
|
||||
|
||||
让神之一手插件自带 QMT Bridge 的 MCP 能力:插件在自己的 apply 内按 QMT 连接配置动态挂载 **DSH 官方 @deepseek-ai/dsh-mcp-client**(url 自动派生自激活连接 baseUrl + /mcp,serverName 固定 QMT_Bridge_MCP,工具以 mcp__QMT_Bridge_MCP__* 注册给模型;挂载/切换/卸载/状态均由神之一手在插件内管理);连接 CRUD/激活切换与 MCP 实例全生命周期联动;**卸载三方 dsh-skill-mcp-panel 并移除其宿主 cordis.patch.yml 受管块**,不再依赖三方面板加入 MCP 能力。REST 直连架构不变(技术约束-003)。
|
||||
|
||||
## 范围
|
||||
|
||||
**做**(对应 R-020 定稿边界,决策编号 Q1-Q6):
|
||||
1. **依赖与可行性前置(R-020 计划阶段待办 #1,已实证 2026-09-08)**:@deepseek-ai/dsh-mcp-client@0.1.1-rc.2 已加为 one-divine-lot dependencies,从插件 lib 可 import(),模块导出 {name:'mcp-client', inject:['tools'], apply, Config},可用 ctx.plugin() 动态挂载(实证通过);
|
||||
2. **服务端 MCP 管理器(新增 src/mcp/QmtMcpManager.js)**:
|
||||
- 唯一 dsh-mcp-client 实例,serverName 固定 `QMT_Bridge_MCP`,url = 激活连接 baseUrl + `/mcp`(自动派生,Q3);
|
||||
- 启动:解析启动连接(默认→上次激活→第一条,空表回退 cordis qmtBaseUrl)后挂载(对齐 R-004 resolveStartupConnection);
|
||||
- 生命周期编排:activate/update(地址变)/remove(激活被删切默认)→ 先卸后挂(对齐 qmt-connections API 现有热切换点,Q1 仅激活连接挂载);
|
||||
- 失败语义(Q5):failOnStartupError:false + reconnect(对齐宿主现有受管块配置:initial 500ms / max 30s / maxAttempts 10);
|
||||
- 状态缓存:{ state: connected|connecting|disconnected|disabled, serverName, url, toolCount, error, checkedAt },供状态端点与指示灯读取;
|
||||
- 释放:ctx.effect dispose 时卸载实例;
|
||||
3. **服务端状态出口(api/qmt-connections.js 或 market.js 扩展)**:
|
||||
- mcp-status 端点(含 refresh 即时重查)→ 设置页连接卡片 MCP 状态行 + 会话头部指示灯读取;
|
||||
- sync-status 增加 mcp 域(Q4:头部指示灯数据源合并一处);
|
||||
4. **设置页「QMT 连接配置」卡片 MCP 状态(UI约束-002 形态内扩展)**:卡片显示 MCP 状态(绿=已连接 + N 工具 / 红=失败原因 / 灰=未启用),「检查 MCP」按钮(手动触发,Q4);
|
||||
5. **会话头部指示灯新增 MCP 状态灯(产品约束-012 体系扩展)**:SyncIndicators 三灯 → 四灯(QMT连接|持仓数据|行情数据|MCP),MCP 灯三态对齐(绿=已连接 / 黄=重连中 / 红=断开 / 灰=未启用),点击=refresh(Q4);
|
||||
6. **移除三方 dsh-skill-mcp-panel(Q2,2026-09-08 老师澄清)**:dsh plugin remove dsh-skill-mcp-panel(连带其「技能 / MCP」设置菜单);编辑 ~/.dsh/profiles/web/cordis.patch.yml 删除其受管块(QMT_Bridge_MCP 条目,块外内容逐字节保留)——属宿主组成变更,按 editing-cordis-compositions 技能流程执行 + 老师确认;
|
||||
7. **构建安装与验收**。
|
||||
|
||||
**不做**(本期,R-020 边界):通用 MCP 服务器管理面板(Q6,含自建任意 MCP 管理 UI);插件 UI/tab 数据源改走 MCP(技术约束-003);多 serverName 并存;自研 MCP 协议客户端(老师定:官方 dsh-mcp-client 可依赖,不重造);认证/鉴权增强(QMT Bridge 无鉴权)。
|
||||
|
||||
## 程序结构(新增/改动)
|
||||
|
||||
```
|
||||
src/
|
||||
├── index.js # 服务端入口(改):注入 mcpManager,启动挂载 + 释放卸载;把 mcpManager 传入 registerApi
|
||||
├── mcp/ # (新增域目录,技术约束-016 语义:无合适域先讨论,此处讨论定 = 新增 mcp 域)
|
||||
│ └── QmtMcpManager.js # MCP 实例管理(官方 dsh-mcp-client 动态挂载:唯一实例 + 生命周期编排 + 状态缓存)
|
||||
├── settings.js # (小改):暴露当前激活连接解析辅助(已有 getActiveQmtConnection 可复用,必要时补暴露)
|
||||
├── api/
|
||||
│ ├── index.js # (改):runtime 增 mcpManager,注册 mcp-status 方法分发
|
||||
│ ├── qmt-connections.js # (改):热切换点(activate/update/remove)联动 mcpManager 重挂;新增 mcp-status 处理
|
||||
│ └── market.js # (改):sync-status 增加 mcp 域
|
||||
└── client/views/
|
||||
├── QmtConnectionChip.jsx # (改):内置 SyncIndicators 传 mcp 状态读取
|
||||
├── SyncIndicators.jsx # (改):三灯 → 四灯(+MCP),轮询 sync-status 读 mcp 域,点击 refresh
|
||||
└── SettingsSection.jsx # (改):QmtConnectionCard 增 MCP 状态行 + 「检查 MCP」按钮
|
||||
package.json # (改,已完成):dependencies + @deepseek-ai/dsh-mcp-client@0.1.1-rc.2 + @modelcontextprotocol/sdk@1.30.0
|
||||
~/.dsh/profiles/web/cordis.patch.yml # (改):移除 dsh-skill-mcp-panel 受管块(Q2,宿主配置,独立步骤;插件本体 dsh plugin remove)
|
||||
```
|
||||
|
||||
## 实现步骤(建议顺序)
|
||||
|
||||
1. **依赖可行性实证**(任务 1 决策门):声明 peer + 构建后在宿主实测 import 解析;不可解析则停在这里与老师讨论替代通道;
|
||||
2. **服务端 mcpManager**:QmtMcpManager 骨架 + 启动挂载/卸载 + 状态缓存(先不接 API,日志验证挂载与工具注册);
|
||||
3. **热切换联动**:qmt-connections activate/update/remove 编排点追加 mcpManager.resync()(复用现有 dataSource.setBaseUrl 同点);
|
||||
4. **状态端点**:mcp-status + sync-status.mcp 域;本地 curl/HTTP 验证;
|
||||
5. **设置页卡片**:QmtConnectionCard 增 MCP 状态 + 检查按钮(复用测试连接交互模式与 Toast);
|
||||
6. **会话头部 MCP 灯**:SyncIndicators 四灯改造;验证与三灯并存样式一致(对齐 IndicatorDot/sep 布局与主题 token);
|
||||
7. **宿主受管块移除**:编辑 cordis.patch.yml(editing-cordis-compositions 流程)→ 重载宿主 → 验证 mcp__QMT_Bridge_MCP__* 工具仍可用(这次来自神之一手);
|
||||
8. **构建安装验收**:服务端+客户端构建,dsh plugin add / 重载;逐条核对验收标准。
|
||||
|
||||
## 验收标准
|
||||
|
||||
1. 移除宿主 cordis.patch.yml QMT_Bridge_MCP 受管块后,模型仍能使用 mcp__QMT_Bridge_MCP__qmt_* 工具(来源=神之一手动态挂载实例),工具集与移除前一致(同一 serverName/url);
|
||||
2. 激活连接切换 → MCP 实例 url 跟随切换(工具可用目标随激活配置变化,观察工具调用返回目标数据源变化);
|
||||
3. 编辑激活连接地址、删除激活连接(自动切默认)→ MCP 实例同步重挂,无残留旧连接工具;
|
||||
4. 连接列表为空(无激活)→ 不挂 MCP 实例(mcp-status=disabled),插件其它功能不受影响;
|
||||
5. QMT Bridge 不可达 → MCP 灯=红(断开),插件不崩、其余灯正常;恢复可达后自动重连至绿灯(Q5 语义);
|
||||
6. 设置页连接卡片显示 MCP 状态(已连接 + N 工具 / 失败原因 / 未启用),「检查 MCP」可手动触发并刷新;
|
||||
7. 会话头部出现第四个指示灯「MCP」:绿=已连接 / 黄=重连中 / 红=断开 / 灰=未启用,点击触发即时检查(Q4);
|
||||
8. 现有功能(分仓/策略/交易/设置子 tab/三灯)无回归;typecheck + build 通过。
|
||||
|
||||
## 验收方法
|
||||
|
||||
- 移除受管块前先完成 1-6 步并在旧块存在时验证(同名冲突预期:两实例同 serverName 后加载报错——因此**验证顺序必须先移除受管块再加载新实例**,步骤 7 与 1-6 的顺序需在实施时按 HMR 重载节奏小心编排,见实施时核查点);
|
||||
- 用 qmt 工具的 listTools/实际调用观察目标切换;断开 QMT 观察灯态与自动恢复;
|
||||
- settings 卡片与头部灯截图/人工核验。
|
||||
|
||||
> **实施注意(同名冲突时序)**:宿主现有受管块与神之一手新实例同用 serverName QMT_Bridge_MCP,存活实例重复会报错。安全顺序:① 插件侧完整实现并构建 → ② 移除宿主受管块并重载(此窗口工具短暂消失可接受)→ ③ 插件实例接管(工具恢复,来源变为神之一手)。禁止两实例并存重载。
|
||||
|
||||
## 关联
|
||||
|
||||
- 需求:R-020(已定稿,2026-09-08)| 参考:mcp_router 动态挂载实证、dsh-skill-mcp-panel 受管块现状
|
||||
- 设计约束新增(定稿时补充):技术约束:「插件内建 MCP 客户端注册规范」(唯一实例 serverName 固定 + 随激活连接 url 派生 + 生命周期联动);产品约束:「MCP 状态出口(设置卡片 + 会话头部 MCP 灯)」
|
||||
|
||||
## 记录
|
||||
|
||||
| 日期 | 变更 |
|
||||
|---|---|
|
||||
| 2026-09-08 | 依据 R-020 定稿(Q1-Q6)创建 PLAN-018 |
|
||||
@@ -0,0 +1,115 @@
|
||||
# 计划:QMT 连接配置(多配置管理 + 会话头部快捷切换)(阶段航点)
|
||||
|
||||
> 编号:PLAN-003 | 粒度:阶段航点(大粒度) | 创建:2026-08-29 | 状态:**已完成(2026-08-29 验收通过,R-004 已归档)**
|
||||
> 派生自终极目标:目标-001(DSH 插件形态)、目标-008(真实交易系统接入·统一数据源抽象)
|
||||
> 依据需求:**R-004(已定稿,2026-08-29)** —— 符合入范围门槛(已定稿方可进入计划)
|
||||
> 设计约束:产品约束-005/006、技术约束-003/008/009、UI约束-001/002、技术约束-004/005/006/007(沿用)
|
||||
|
||||
## 目标
|
||||
|
||||
为插件增加 QMT Bridge 连接的多配置管理能力:设置页「QMT 连接配置」子 tab 管理
|
||||
多个连接配置(CRUD / 激活 / 默认 / 测试连接),会话头部(PTC 模式标签旁)常驻
|
||||
快捷切换控件;激活配置**立即切换**数据源地址(无需重启 DSH),重启后回到默认配置。
|
||||
|
||||
## 范围
|
||||
|
||||
**做**(对应 R-004 定稿范围,决策编号 Q1-Q10):
|
||||
1. **设置页「QMT 连接配置」子 tab**(产品约束-005、UI约束-002):
|
||||
- 配置 CRUD:新增 / 编辑 / 重命名 / 删除(字段:名称 + HTTP 地址);
|
||||
- 单选激活(一次只能激活一个),激活立即生效;
|
||||
- 默认标记:启动时自动激活默认配置,重启后回到默认;
|
||||
- 测试连接:手动触发,请求 `/health` 返回可达性与延迟;
|
||||
- 删除边界:删激活配置→自动切默认;删默认→默认标记转列表第一条并激活之;禁删最后一条;
|
||||
2. **会话头部快捷切换**(产品约束-006、UI约束-001、技术约束-009):
|
||||
- slot `conversation.session.header.actions` 注册紧凑下拉 chip:`QMT: <激活配置名> ▾`;
|
||||
- 菜单列出全部配置(激活项勾选),点选即激活 + toast 反馈;
|
||||
- 头部仅切换,管理仍在设置页;
|
||||
3. **生效机制**(技术约束-008):
|
||||
- 激活/修改后立即切换(更新数据源实例 baseUrl,按请求读取已核实);
|
||||
- 启动时自动激活默认配置;列表为空时回退 cordis 注入的 qmtBaseUrl 兜底;
|
||||
4. **存储**(技术约束-008):复用 one-divine-lot settings namespace,新增 qmtConnections(list + activeId + defaultId)。
|
||||
|
||||
**不做**(本期):
|
||||
- 认证/多账户支持(QMT Bridge 当前无鉴权);
|
||||
- 连接状态监控与断线重连(后续);
|
||||
- 超时(timeoutMs)配置化 —— 保持代码默认 15000ms(Q5);
|
||||
- 宿主配置(cordis.patch.yml qmtBaseUrl)自动迁移 —— 首次使用手动录入(Q4);
|
||||
- 激活前强制连通性校验 —— 测试连接为手动操作(Q3);
|
||||
- 头部菜单管理能力(增删改/测试连接/设默认)—— 留在设置页(Q10)。
|
||||
|
||||
## 程序结构(改造后)
|
||||
|
||||
```
|
||||
src/
|
||||
├── index.js # 服务端入口(改)
|
||||
│ ├── 数据源创建后注册为可寻址组件;启动时按「默认配置」初始化激活地址(列表空→cordis qmtBaseUrl 兜底)
|
||||
│ └── inject + ['settings', 'webServer'](不变)
|
||||
├── settings.js # 设置管理(改)
|
||||
│ ├── schema 增加 qmtConnections: { list: [{id,name,baseUrl,order}], activeId, defaultId }
|
||||
│ ├── 新增 QMT 连接配置 CRUD(与策略 CRUD 同模式:列表读写 + 整表 update)
|
||||
│ │ ├── getQmtConnections(scope) # 读列表(按 order 排序)
|
||||
│ │ ├── addQmtConnection(scope, {name, baseUrl})
|
||||
│ │ ├── renameQmtConnection(scope, id, {name, baseUrl})
|
||||
│ │ ├── removeQmtConnection(scope, id) # 删除边界处理(产品约束-005)
|
||||
│ │ ├── activateQmtConnection(scope, id) # 激活(写 activeId)
|
||||
│ │ ├── setDefaultQmtConnection(scope, id) # 设默认(写 defaultId)
|
||||
│ │ └── resolveStartupConnection(scope, config) # 启动激活地址解析(默认→激活→cordis 兜底)
|
||||
│ └── generateQmtConnectionId(name, existingIds) # slug id(复用拼音映射与去重逻辑)
|
||||
├── data-source/qmt-bridge-rest.js # 数据源(改:小改)
|
||||
│ └── 新增 setBaseUrl(url) # 运行期更新实例地址(技术约束-008:按请求读取,改字段即生效)
|
||||
│ (timeoutMs 不暴露 setter —— Q5)
|
||||
├── api.js # 服务端 HTTP API(改)
|
||||
│ ├── METHODS 增加:qmt-connections / qmt-connections/add
|
||||
│ │ / qmt-connections/update / qmt-connections/remove
|
||||
│ │ / qmt-connections/activate / qmt-connections/set-default
|
||||
│ │ / qmt-connections/test(服务端代理 GET {baseUrl}/health,返回 {ok, latencyMs, status})
|
||||
│ └── 编排:activate/update/remove 成功后调用 dataSource.setBaseUrl(激活地址)
|
||||
├── client/
|
||||
│ ├── index.js # 客户端入口(改)
|
||||
│ │ └── slots.register({ name: 'conversation.session.header.actions', id: 'odl-qmt-switch', order: -9 }, QmtConnectionChip)
|
||||
│ └── views/
|
||||
│ ├── QmtConnectionChip.jsx # 新增:头部快捷切换控件(下拉 chip + 菜单 + toast 反馈)
|
||||
│ └── SettingsSection.jsx # 改:新增第三个子 tab「QMT 连接配置」
|
||||
│ └── 内部新组件 QmtConnectionsSettings:配置列表 + 行内操作 + 新增/编辑表单 + 删除确认弹窗
|
||||
└── cordis.patch.yml # 不改:qmtBaseUrl 保留为兜底种子(Q4 不迁移)
|
||||
```
|
||||
|
||||
## 改动说明(按文件)
|
||||
|
||||
| 文件 | 改动 | 依据 |
|
||||
|---|---|---|
|
||||
| src/settings.js | schema 增 qmtConnections;新增 7 个 CRUD/激活/默认/启动解析函数 | 技术约束-008、产品约束-005 |
|
||||
| src/data-source/qmt-bridge-rest.js | 新增 setBaseUrl(url);其余不动 | 技术约束-008(按请求读取地址) |
|
||||
| src/api.js | 新增 7 个 qmt-connections 端点;activate/update/remove 编排 setBaseUrl | 技术约束-004(webServer 自开路由)、-008 |
|
||||
| src/index.js | 启动时解析并设置激活地址;数据源实例传递给 api 注册 | 技术约束-008 |
|
||||
| src/client/index.js | 注册会话头部 slot(odl-qmt-switch,order=-9) | 技术约束-009 |
|
||||
| src/client/views/QmtConnectionChip.jsx | 新增头部切换控件(复用 ConnectionProvider/useRpc + Toast) | 产品约束-006、UI约束-001 |
|
||||
| src/client/views/SettingsSection.jsx | 新增「QMT 连接配置」子 tab 与配置管理界面 | 产品约束-005、UI约束-002 |
|
||||
| cordis.patch.yml | 不改动 | Q4(不迁移) |
|
||||
|
||||
## 实现步骤(建议顺序)
|
||||
|
||||
1. **服务端数据层**:settings.js 增 schema 与 CRUD/激活/默认/启动解析函数(纯函数,先行可测);
|
||||
2. **数据源热切换**:qmt-bridge-rest.js 增 setBaseUrl;index.js 启动时 resolveStartupConnection 并设置初始激活地址;把 dataSource 传给 registerApi;
|
||||
3. **服务端 API**:api.js 增 7 个端点 + 激活编排 setBaseUrl;本地 curl 验证 CRUD/激活/测试连接;
|
||||
4. **设置页 UI**:SettingsSection.jsx 增「QMT 连接配置」子 tab(列表/表单/确认弹窗,复用 ConfirmDialog/Toast);走通设置页全流程;
|
||||
5. **头部快捷切换**:client/index.js 注册头部 slot;新增 QmtConnectionChip.jsx(读列表/激活/失败反馈);验证与 PTC 标签并排显示与切换生效;
|
||||
6. **构建安装**:服务端+客户端构建,dsh plugin add 安装,重载页面;
|
||||
7. **验收**:对照下节验收标准逐条核验,记录迭代 03。
|
||||
|
||||
## 验收标准
|
||||
|
||||
1. 设置页出现「QMT 连接配置」子 tab,配置 CRUD 完整可用(名称 + HTTP 地址);
|
||||
2. 激活某配置后,数据源立即切到该地址(无需重启 DSH)——通过切换后刷新持仓/测试连接验证目标端点随激活配置变化;
|
||||
3. 默认配置行为正确:启动自动激活默认;重启后回到默认(即使运行中切过其他配置);列表为空时按 cordis qmtBaseUrl 兜底且页面/数据源可用;
|
||||
4. 测试连接返回可达性 + 延迟;激活不强制先测试;
|
||||
5. 删除边界符合产品约束-005:删激活→自动切默认;删默认→默认转移列表第一条并激活;删最后一条被禁止;
|
||||
6. 会话头部 PTC 标签旁出现 `QMT: <激活配置名>` chip,点选菜单项即激活并 toast 反馈;头部仅切换(无管理入口);
|
||||
7. 现有功能(分仓/策略/设置现有子 tab)不回归。
|
||||
|
||||
## 验收方法
|
||||
|
||||
- 建第二个配置指向任一可达地址,激活后通过「测试连接」与持仓加载观察目标地址变化;
|
||||
- 重启 DSH(或重载插件)验证回到默认配置;
|
||||
- 删除激活/默认配置验证自动切换与默认转移;删至最后一条验证禁止提示;
|
||||
- 头部 chip 切换后观察 toast 与数据源目标变化(配合测试连接)。
|
||||
@@ -0,0 +1,61 @@
|
||||
# 计划:Tab 设置——统一管理所有会话 tab(内置 + 策略分组,显示/隐藏 + 拖动排序)(阶段航点)
|
||||
|
||||
> 编号:PLAN-010 | 粒度:阶段航点(大粒度) | 创建:2026-09-02 | 状态:**执行中**
|
||||
> 派生自终极目标:目标-002(分仓管理工具)、目标-003(按策略监控市场)
|
||||
> 依据需求:**R-011(已定稿,2026-09-02,Q1-Q5 确认)** —— 符合入范围门槛
|
||||
> 设计约束:产品约束-009、UI约束-003、技术约束-014(R-011 新增沉淀)、技术约束-004/005/006(沿用)
|
||||
|
||||
## 目标
|
||||
|
||||
将设置页「通用设置」升级为 **「Tab 设置」**,成为所有会话 tab(系统内置 + 策略分组)**唯一的顺序与显隐入口**:两类 tab 混排一张表,每行 = 拖动手柄 + 名称 + 显示/隐藏开关;**任何 tab 均不支持重命名与删除**;拖动落点立即持久化。
|
||||
|
||||
## 范围
|
||||
|
||||
**做**:
|
||||
1. **数据模型统一**(src/settings.js):settings.tabs 从布尔对象 → 有序数组 [{id, kind: builtin|strategy, refKey|refId, name, visible, order}];strategies 收窄为 {id, name};读取时旧格式静默归一化(旧 tabs 布尔对象 → 三个内置条目保留原显隐;旧策略按 order 接续追加,旧隐藏策略迁移后 visible=true);新增整表更新(顺序 + 显隐);DEFAULT_TABS 升级为默认数组;
|
||||
2. **API 层**(src/api/strategies.js):tabs/update 语义改为整表更新(顺序 + 显隐);strategies/add 联动追加 tab 条目(末尾);strategies/remove 联动删除对应 tab 条目;废弃 strategies/move;strategies/update 仅剩重命名;
|
||||
3. **客户端注册**(src/client/index.js):合并 registerGeneralTabs + registerStrategyTabs 为统一注册(读 tabs 数组按 order 排序、过滤 visible;builtin 走内置 render、strategy 走 StrategyTab),移除硬编码 order 间隔(10/11/12 与 13+);
|
||||
4. **设置页 UI**(src/client/views/SettingsSection.jsx):
|
||||
- 「Tab 设置」子 tab 取代「通用设置」:混排表格,内置行「内置」徽标、策略行「策略」徽标;每行 = 拖动手柄 + 名称 + 显隐开关;无任何重命名/删除按钮;
|
||||
- 拖动:原生 HTML5 Drag & Drop,落点重排 → 立即调 tabs/update 持久化 → 提示「刷新页面后生效」;
|
||||
- 「策略分组」子 tab 瘦身:只留 新增/重命名/删除,移除排序箭头与显隐开关,加提示「顺序与显示请在「Tab 设置」中调整」。
|
||||
|
||||
**不做**:
|
||||
- 策略的命名/删除入口迁移(仍在「策略分组」子 tab);
|
||||
- 内置 tab 的重命名/删除(禁);
|
||||
- tab 注册热更新(沿用「刷新页面后生效」机制,无事件机制);
|
||||
- 其他设置子 tab(QMT 连接配置)改动(仅 UI约束-002 文案「通用设置」→「Tab 设置」对齐)。
|
||||
|
||||
## 程序结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── settings.js # 改:tabs 有序数组 + 归一化迁移 + 整表更新;strategies 收窄
|
||||
├── api/strategies.js # 改:tabs/update 整表;add/remove 联动 tabs;废弃 move
|
||||
src/client/
|
||||
├── index.js # 改:统一注册(读 tabs 数组)
|
||||
└── views/SettingsSection.jsx # 改:「Tab 设置」子 tab + 拖动排序 + 策略分组瘦身
|
||||
```
|
||||
|
||||
## 实现步骤
|
||||
|
||||
1. **文档骨架**:PLAN-010 + 迭代 09 文档(本步);
|
||||
2. **settings.js**:tabs 有序数组 schema + DEFAULT_TABS + 归一化(getTabs 旧格式转新)+ updateTabs 整表 + strategies 收窄(去 visible/order 读写);
|
||||
3. **api/strategies.js**:tabs/update 整表语义;strategies/add 追加 tab;strategies/remove 联动删 tab;废弃 move(API 表 + handler);
|
||||
4. **client/index.js**:统一注册函数(读 tabs 数组),移除硬编码 order;
|
||||
5. **SettingsSection.jsx**:「Tab 设置」子 tab(混排表格 + 徽标 + 显隐开关 + 拖动排序 + 立即持久化);策略分组子 tab 瘦身;
|
||||
6. **构建测试**:pnpm run build + typecheck + 独立数据目录回归(技术约束-011,注意只读端点可直连、写操作走临时目录);
|
||||
7. **验收 + 复盘**。
|
||||
|
||||
## 验收要点
|
||||
|
||||
- 「通用设置」子 tab 更名为「Tab 设置」,表格混排内置 + 策略全部 tab;
|
||||
- 内置行带「内置」徽标、策略行带「策略」徽标;
|
||||
- 每行可拖动排序,落点立即持久化(刷新页面后生效);
|
||||
- 每行有显示/隐藏开关,切换立即持久化;
|
||||
- 任何行均无重命名/删除按钮;
|
||||
- 策略分组子 tab 只留 新增/重命名/删除,无排序箭头/显隐开关,有指引提示;
|
||||
- 新增策略出现在 Tab 设置列表末尾;删除策略后对应 tab 条目消失;
|
||||
- 老配置(布尔对象 tabs + 策略 order/visible)读取时自动归一化,无报错、无数据丢失;
|
||||
- 会话 tab 栏按新顺序与显隐注册(内置与策略可交错);
|
||||
- 现有功能不回归(全部持仓/交易记录/策略持仓/QMT 切换)。
|
||||
@@ -0,0 +1,55 @@
|
||||
# 计划:UI 适配 DSH 主题(浅色 / 深色 / 跟随系统)(阶段航点)
|
||||
|
||||
> 编号:PLAN-011 | 粒度:阶段航点 | 创建:2026-09-02 | 状态:**执行中**
|
||||
> 派生自终极目标:目标-001(神之一手工具集)
|
||||
> 依据需求:**R-012(已定稿,2026-09-02,暂定跟随系统)** —— 符合入范围门槛
|
||||
> 设计约束:UI约束-003(Tab 设置)、UI约束-002(QMT 卡片)沿用;本次新增色值 token 化约定
|
||||
|
||||
## 目标
|
||||
|
||||
神之一手客户端 UI 全部硬编码色值替换为宿主 `--dsw-*` token,使各页面在 DSH 浅色 / 深色 / 跟随系统主题下均可读、协调,随主题切换自动适配,不自行维护主题偏好。
|
||||
|
||||
## 范围
|
||||
|
||||
**做**:
|
||||
1. 客户端 10 个文件 141 处硬编码颜色按语义映射表替换为 `var(--dsw-*)` 或 `color-mix`(随主题淡色底);
|
||||
2. 确认 QmtConnectionChip 已用 token 与硬编码混杂处统一;
|
||||
3. 涨跌色(红涨绿跌)→ 宿主 state-error/success;主按钮 / 徽标 / 提示底色 → 宿主语义 token + color-mix;
|
||||
4. 构建 + typecheck + 深浅主题人工验收。
|
||||
|
||||
**不做**:
|
||||
- 自定义 `--odl-*` 覆盖 token(D5 暂缓);
|
||||
- 布局 / 间距 / 圆角改动;
|
||||
- 服务端 / API / 数据改动;
|
||||
- 插件自维护主题偏好(跟随宿主)。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
```
|
||||
src/client/views/
|
||||
├── SettingsSection.jsx # 55 处(最大)
|
||||
├── StrategyTab.jsx # 24
|
||||
├── TradeRecordsTab.jsx # 21
|
||||
├── QmtConnectionChip.jsx # 13(部分已 token)
|
||||
├── RangeSelector.jsx # 7
|
||||
├── PriceCell.jsx # 5(涨跌色)
|
||||
├── AllPositionsTab.jsx # 4
|
||||
├── LoadState.jsx # 3
|
||||
├── Toast.jsx # 2
|
||||
└── PlaceholderTab.jsx # 1
|
||||
```
|
||||
|
||||
## 实现步骤
|
||||
|
||||
1. **文档骨架**:PLAN-011 + 迭代 10(本步);
|
||||
2. **批量替换**:按文件逐处替换(先小文件确认模式,再大文件);
|
||||
3. **验证**:build + typecheck;人工切 DSH 浅/深主题检查各页;
|
||||
4. **验收 + 复盘**。
|
||||
|
||||
## 验收要点
|
||||
|
||||
- 浅色主题下观感与现状基本一致(无突兀色差);
|
||||
- 深色主题下所有页面可读(背景/文字/边框/按钮/涨跌/徽标/提示/下拉菜单均协调);
|
||||
- 跟随系统切换实时生效;
|
||||
- 无残留硬编码色(#xxx / rgb / rgba 全部清理,注释可留);
|
||||
- 布局与交互不变(仅色值)。
|
||||
@@ -0,0 +1,51 @@
|
||||
# 计划:WS 盘中价格实时更新(前端直连 QMT Bridge WebSocket)(阶段航点)
|
||||
|
||||
> 编号:PLAN-004 | 粒度:阶段航点(大粒度) | 创建:2026-08-31 | 状态:**已完成(2026-09-01 迭代 04 验收通过,R-005 已实现)**
|
||||
> 派生自终极目标:目标-001(DSH 插件形态)、目标-008(真实交易系统接入·统一数据源抽象)
|
||||
> 依据需求:**R-005(已定稿,2026-08-31)** —— 符合入范围门槛(已定稿方可进入计划)
|
||||
> 设计约束:技术约束-001/003/008(沿用)、技术约束-010(新增:前端直连 WS)、产品约束-007(新增:现价列展示)
|
||||
|
||||
## 目标
|
||||
|
||||
在 3 个监控表格(全部持仓 / 手动做T / 网格超市)中实现**盘中价格实时更新**:前端直连 QMT Bridge WebSocket 订阅全市场 tick,事件驱动更新关注股票的现价(lastPrice),红涨绿跌着色 + 变化高亮。
|
||||
|
||||
## 范围
|
||||
|
||||
**做**:
|
||||
1. **WS 行情客户端**(前端):连接 `ws://<激活配置host>:8610/ws`,订阅 `whole` 全市场 tick;维护 code→lastPrice 实时价格 Map;
|
||||
2. **订阅生命周期**:订阅前 REST 拉一次 `/data/tick` 快照补初值(WS 只推增量);连接断开静默重连 + 状态提示;切换激活 QMT 配置时 WS 随之切换;
|
||||
3. **关注列表**:3 表格自动汇总出现的 code(全部持仓 + 手动做T + 网格超市),作为过滤集合;
|
||||
4. **现价展示**:3 表格新增「现价」列(lastPrice),红涨绿跌着色(对比昨收 lastClose),价格变化轻微高亮;
|
||||
5. **事件驱动**:收到 WS 推送 → 更新价格 Map → 通知订阅方(React state/事件)→ 表格行重渲染现价。
|
||||
|
||||
**不做**(本期):
|
||||
- 市值/盈亏/五档实时展示;
|
||||
- 插件服务端中转 WS;
|
||||
- 关注列表手动配置;
|
||||
- 断线重连 UI 引导(仅静默重连 + 状态提示)。
|
||||
|
||||
## 程序结构(改造后)
|
||||
|
||||
```
|
||||
src/
|
||||
├── index.js # 服务端入口(小改):新增 API 暴露激活连接 host(供前端建 WS)
|
||||
├── api.js # 服务端 API(改):新增 qmt-connections/active-host 端点(返回 { host, baseUrl, wsUrl })
|
||||
├── client/
|
||||
│ ├── index.js # 客户端入口(改):注册行情 Provider,包装 3 个表格
|
||||
│ ├── market/
|
||||
│ │ └── MarketDataProvider.jsx # 新增:WS 行情客户端 + 价格 Map + 订阅/重连/切换 + Context 下发
|
||||
│ └── views/
|
||||
│ ├── AllPositionsTab.jsx # 改:新增「现价」列 + 涨跌色 + 高亮
|
||||
│ ├── StrategyTab.jsx # 改:新增「现价」列 + 涨跌色 + 高亮
|
||||
│ └── PriceCell.jsx # 新增:现价单元格组件(格式化 + 涨跌色 + 高亮)
|
||||
└── cordis.patch.yml # 不改(QMT 配置已在 settings)
|
||||
```
|
||||
|
||||
## 实现步骤(建议顺序)
|
||||
|
||||
1. **文档骨架**:R-005 转正 + PLAN-004 + 迭代 04 子目录 + 迭代说明(本步);
|
||||
2. **服务端 API**:新增 `qmt-connections/active-host` 返回激活配置 host(前端据此建 WS);
|
||||
3. **WS 行情客户端**:MarketDataProvider(连接/订阅/价格 Map/重连/配置切换);
|
||||
4. **现价列 UI**:PriceCell + 3 表格接入;
|
||||
5. **构建安装**:dsh plugin add,重载页面;
|
||||
6. **验收**:对照验收标准逐条核验,记录迭代 04。
|
||||
@@ -0,0 +1,36 @@
|
||||
# 计划:交易关联驱动持仓份额动态调整(阶段航点)
|
||||
|
||||
> 编号:PLAN-017 | 粒度:阶段航点 | 创建:2026-09-08 | 状态:**已完成(迭代 16 验收通过 2026-09-08)**
|
||||
> 派生自终极目标:目标-006(交易复盘)/ 目标-007(人机合一)
|
||||
> 依据需求:**R-018(已定稿,2026-09-08,老师逐项拍板)** —— 符合入范围门槛
|
||||
> 设计约束:技术约束-017(变更:幽灵清仓条款)、产品约束-011(变更)、数据存储设计.md §11(变更);新增归属分段存储设计
|
||||
|
||||
## 目标
|
||||
|
||||
从交易记录出发的关联操作升级为**账本写操作**:策略级候选 + 统一份额分配器(一笔委托拆 0..N 段 × 策略)、买卖联动调整 strategy_holdings 份额(买入加仓/建新仓、卖出减仓/归零清仓、仓不足自动截断续分)、撤段逆操作回滚(作废第三态 / 恢复活动仓)、**数据域分界**(幽灵清仓等同步机制不再写账本,账本只由交易关联 + 手动份额操作驱动)。
|
||||
|
||||
## 范围
|
||||
|
||||
**做**:
|
||||
1. 数据域分界落地:PositionSync 删除幽灵清仓账本写逻辑(同步机制只维护对账单快照)+ 漏关联软提示检测(账本有活动行 + QMT 快照无此 code → 提示,不动账本);
|
||||
2. 存储地基:strategy_holdings 增 `void_at` 作废第三态(活动 = closed_at IS NULL AND void_at IS NULL,唯一索引收窄)+ 幂等补列;新增归属分段表 `trade_order_attributions`(order_id → strategy_id/holding_id/volume/direction/created_at)+ 存量归属迁移(trade_orders 两列 → 段表第一段);
|
||||
3. 归属服务(核心):份额分配器 apply/revoke——判定动作表(买/卖 × 活动仓状态 → open/add/reduce/close/截断/失败)+ 撤段逆操作(撤建仓买入=作废、撤加仓=减回归零作废、撤卖出=恢复活动+份额加回)+ 原子性(改归属 = 撤旧段 + 加新段);
|
||||
4. 策略级候选 + 分配器 API:候选回归「当前持仓 + 未关联」(R-017 7 天清仓候选退役,holdingId 键控/占位安全逻辑保留);
|
||||
5. UI:TradeRecordsTab 归属入口改造为份额分配器(策略下拉 + 段量输入 + 余额提示 + 部分关联);
|
||||
6. 回归:新脚本(分配器动作/撤段/迁移/软提示)+ 存量回归 + typecheck + build。
|
||||
|
||||
**不做**:自动归属推导暴露;全部持仓/未分配 tab 份额联动 UI;未分配余额自动兜底;账本写自动化兜底(软提示不改账本)。
|
||||
|
||||
## 实施步骤(阶段划分,供迭代跟踪)
|
||||
|
||||
1. 阶段A:PositionSync 幽灵清仓退役 + 软提示检测基础;
|
||||
2. 阶段B:存储地基(void_at + 段表 + 迁移);
|
||||
3. 阶段C:归属服务核心(apply/revoke 动作表 + 原子性);
|
||||
4. 阶段D:策略级候选 + API(含 R-017 退役);
|
||||
5. 阶段E:UI 份额分配器;
|
||||
6. 阶段F:回归 + typecheck + build + 存量回归;
|
||||
7. 迭代复盘 + R-018 索引状态更新 + 需求归档。
|
||||
|
||||
## 验收要点
|
||||
|
||||
见 `docs/04-迭代记录/16-交易关联驱动持仓份额动态调整/验收标准.md`。
|
||||
@@ -0,0 +1,63 @@
|
||||
# 计划:交易记录接入(订单与成交数据 + 日期导航)(阶段航点)
|
||||
|
||||
> 编号:PLAN-006 | 粒度:阶段航点(大粒度) | 创建:2026-09-01 | 状态:**进行中**
|
||||
> 派生自终极目标:目标-006(交易复盘)、目标-003(按策略监控市场)
|
||||
> 依据需求:**R-007(范围已定稿,2026-09-01)** —— 符合入范围门槛
|
||||
> 设计约束:技术约束-001/003/004/010(沿用)、产品约束-007 方向(着色习惯,红买绿卖为交易方向新语义)
|
||||
|
||||
## 目标
|
||||
|
||||
在「交易记录」tab 接入 QMT Bridge 当日订单(/trade/orders)与成交(/trade/trades)数据:**单表合并展示**(委托主行 + 按 m_strOrderSysID 展开成交明细)+ **基于交易日历的日期导航**(当日默认选中,历史接口占位,为老师完善 QMT Bridge 历史成交接口预留)。
|
||||
|
||||
## 范围
|
||||
|
||||
**做**:
|
||||
1. **数据源接入**:QmtBridgeRestDataSource 新增 getOrders() / getTrades() / getTradingDates()(语义化映射,核心字段见 R-007 2.1/2.2);
|
||||
2. **服务端 API**:新增 api/trades.js(orders / trades / trading-dates 端点),纳入领域分发;
|
||||
3. **交易记录 tab 落地**:占位 tab 替换为 TradeRecordsTab——**单表合并**:委托主行(聚合成交)+ 展开/折叠成交明细;
|
||||
4. **日期导航**:DateNav 基于 /data/calendar/trading_dates,前后切换交易日,当日默认选中,历史日期展示「接口开发中」占位;
|
||||
5. **排序**:委托主行默认降序(最新在前),支持表头点击手动排序;
|
||||
6. **数据刷新**:服务端中转 + 前端轮询(3-5s,复用 market-snapshot 模式)。
|
||||
|
||||
**不做**:
|
||||
- 历史(非当日)交易数据查询实现(老师完善 QMT Bridge 接口后接入);
|
||||
- 交易数据本地持久化(Q5 本期不做);
|
||||
- 按策略过滤交易(Q10 后续迭代);
|
||||
- 下单/撤单等交易操作(目标-007 控制权另议)。
|
||||
|
||||
## 程序结构(改造后)
|
||||
|
||||
```
|
||||
src/
|
||||
├── component/
|
||||
│ └── QmtBridgeRestDataSource.js # 改:+getOrders/getTrades/getTradingDates
|
||||
├── api/
|
||||
│ ├── trades.js # 新增:orders/trades/trading-dates 端点
|
||||
│ └── index.js # 改:合并 TRADE_METHODS
|
||||
├── client/
|
||||
│ ├── views/
|
||||
│ │ ├── TradeRecordsTab.jsx # 新增:交易记录 tab(日期导航 + 单表合并)
|
||||
│ │ ├── OrderRow.jsx # 新增:委托主行(聚合成交 + 展开)
|
||||
│ │ ├── TradeDetailRows.jsx # 新增:展开成交明细行
|
||||
│ │ └── DateNav.jsx # 新增:交易日历日期导航
|
||||
│ └── index.js # 改:tradeRecords tab 渲染 TradeRecordsTab
|
||||
```
|
||||
|
||||
## 实现步骤(建议顺序)
|
||||
|
||||
1. **文档骨架**:PLAN-006 + 迭代 05 子目录(本步);
|
||||
2. **数据源**:getOrders/getTrades/getTradingDates(语义化映射 + 方向判定);
|
||||
3. **服务端 API**:api/trades.js 三端点 + 领域分发注册;
|
||||
4. **客户端合并逻辑**:fetch 两接口 → 按 m_strOrderSysID 分组 → 聚合;
|
||||
5. **UI**:DateNav + OrderRow + TradeDetailRows + TradeRecordsTab;排序(默认降序 + 表头点击);
|
||||
6. **构建安装**:pnpm run build + 页面刷新(客户端)或宿主重启(服务端);
|
||||
7. **验收**:对照迭代 05 验收标准逐条核验,记录迭代复盘。
|
||||
|
||||
## 验收要点
|
||||
|
||||
- 交易记录 tab 显示当日委托(单表合并,展开可见每笔成交);
|
||||
- 委托主行:时间/代码/名称/方向/状态/委托量/成交量/委托价/成交均价/成交额/手续费;
|
||||
- 方向红买绿卖;状态中文映射;未成交列显示 —;
|
||||
- 日期导航基于交易日历,前后切换,当日默认,历史占位;
|
||||
- 排序默认降序 + 表头手动排序;
|
||||
- 现有功能(持仓/策略/行情/连接配置)不回归。
|
||||
@@ -0,0 +1,66 @@
|
||||
# 计划:交易记录本地存储(SQLite)+ 策略关联(阶段航点)
|
||||
|
||||
> 编号:PLAN-008 | 粒度:阶段航点(大粒度) | 创建:2026-09-01 | 状态:**已完成(2026-09-01 迭代 07 验收通过)**
|
||||
> 派生自终极目标:目标-006(交易复盘)、目标-003(按策略监控市场)
|
||||
> 依据需求:**R-009(已定稿,2026-09-01,Q1-Q8 全部确认)** —— 符合入范围门槛
|
||||
> 设计约束:技术约束-012(数据存储设计)、技术约束-001/003/004/010(沿用)、产品约束-007 方向语义
|
||||
|
||||
## 目标
|
||||
|
||||
在 SQLite 新增**交易记录表**(trade_orders 委托 + trade_fills 成交),将 QMT Bridge 当日交易数据**本地持久化**(跨日积累成本地历史库),并按**外键链**(成交→委托→策略→holding)建立与插件策略体系的关联,支持**按策略过滤 / 复盘**交易。
|
||||
|
||||
- 承接 R-007 的 Q5(本地持久化)与 Q10(按策略过滤);
|
||||
- 落实迭代 06 复盘遗留项 2(holding_id 关联锚点);
|
||||
- 解决 R-007 历史范围「接口开发中」占位(本地积累后历史可查)。
|
||||
|
||||
## 范围
|
||||
|
||||
**做**:
|
||||
1. **两表落地**:trade_orders(委托主行,order_id 主键,UPSERT 幂等)+ trade_fills(成交明细,trade_id 主键、order_id 外键关联委托)两表;**两表均不冗余 strategy_id / holding_id**(Q3 老师定稿:外键链推导);trade_orders 加派生列 insert_ts(insert_date+insert_time 合成毫秒时间戳,join 持仓窗口用);
|
||||
2. **定时同步**:服务端 TradeSync 模块 —— 定时(60s)拉当日 orders+trades → UPSERT 落库(幂等);插件启动同步一次(预热今日数据);前端今日轮询写穿(机会式);
|
||||
3. **策略关联(外键链推导)**:策略/持仓归属不在写入期计算,查询期由委托时间(insert_ts)JOIN strategy_holdings 生命周期窗口(created_at ≤ t < closed_at,closed_at 为 NULL=当前持仓)推导;同码多策略取份额最大持仓;无命中视为未关联;
|
||||
4. **本地历史查询端点**:trades/history —— 按 { start, end, code, strategyId, direction } 查本地 SQLite,返回 { orders, fills }(策略过滤走 FK 链 join);
|
||||
5. **前端**:TradeRecordsTab 加策略过滤下拉(全部/各策略/未关联);历史范围从「占位」切换为查本地库;今日仍走 QMT 实时 + 写穿本地。
|
||||
|
||||
**不做**:
|
||||
- 手动修正策略关联入口(后续迭代);
|
||||
- 导出/清理管理界面(后续迭代;复盘需要历史,不做自动清理);
|
||||
- QMT 历史接口接入(老师完善后再接);
|
||||
- 下单/撤单等交易操作(目标-007 另议)。
|
||||
|
||||
## 程序结构(改造后)
|
||||
|
||||
```
|
||||
src/
|
||||
├── component/
|
||||
│ ├── SqliteStore.js # 改:+trade_orders/trade_fills 建表 + 交易 UPSERT/查询 + 策略归属推导
|
||||
│ ├── DataStore.js # 改:委托交易记录方法
|
||||
│ └── TradeSync.js # 新增:服务端定时同步(60s + 启动预热 + UPSERT 幂等)
|
||||
├── api/
|
||||
│ └── trades.js # 改:+trades/history 端点
|
||||
└── index.js # 改:装配 TradeSync + storage 注入 api runtime
|
||||
src/client/views/
|
||||
└── TradeRecordsTab.jsx # 改:策略过滤下拉 + 历史范围查本地库
|
||||
```
|
||||
|
||||
## 实现步骤(建议顺序)
|
||||
|
||||
1. **文档骨架**:PLAN-008 + 迭代 07 子目录(本步);
|
||||
2. **设计约束落地**:数据存储设计.md §10 增补交易记录表设计 + 技术方案约束新增条目;
|
||||
3. **SqliteStore**:建表(trade_orders/trade_fills)+ 交易 UPSERT(幂等)+ 本地历史查询 + 策略归属推导(insert_ts join 持仓窗口);
|
||||
4. **DataStore**:委托交易记录方法;
|
||||
5. **TradeSync**:定时同步模块(60s + 启动预热 + 今日范围 UPSERT);
|
||||
6. **api/trades.js**:trades/history 端点(+ 现有 orders/trades 响应附加策略归属信息可选);
|
||||
7. **index.js**:装配 TradeSync(dispose 清理)+ storage 注入 api runtime;
|
||||
8. **前端**:TradeRecordsTab 策略过滤 + 历史范围本地展示;
|
||||
9. **构建测试**:pnpm run build + typecheck + 独立数据目录回归测试(技术约束-011);
|
||||
10. **验收**:对照迭代 07 验收标准逐条核验,记录迭代复盘。
|
||||
|
||||
## 验收要点
|
||||
|
||||
- trade_orders + trade_fills 两表落地(零冗余 strategy_id/holding_id,insert_ts 派生列);
|
||||
- 同步:启动预热 + 60s 定时 UPSERT 幂等(重复同步不重复行、状态覆盖更新);
|
||||
- 历史查询:按时间段/code/策略/方向查本地库,策略过滤 = 委托时间 join 持仓生命周期窗口(一码多策略取份额最大);
|
||||
- 前端:策略过滤下拉 + 历史范围展示本地数据(不再占位);今日实时 + 写穿;
|
||||
- 现有功能不回归(持仓/策略/行情/连接配置/交易记录今日实时);
|
||||
- 技术约束-011:回归测试用独立数据目录。
|
||||
@@ -0,0 +1,27 @@
|
||||
# 计划:委托归属候选纳入近期清仓持仓(阶段航点)
|
||||
|
||||
> 编号:PLAN-016 | 粒度:阶段航点 | 创建:2026-09-07 | 状态:**已实施(待验收)**
|
||||
> 派生自终极目标:目标-008(真实交易系统接入)
|
||||
> 依据需求:**R-017(已定稿,2026-09-07,老师指令确认方向)** —— 符合入范围门槛
|
||||
> 设计约束:技术约束-013 语义延伸(候选窗口 + holdingId 键控);不新增约束条目
|
||||
|
||||
## 目标
|
||||
|
||||
清仓后当日委托可补关联:归属候选 = 该 code 当前持仓 + 近 7 天清仓持仓(closed 标记),下拉锚点改 holdingId 键控。
|
||||
|
||||
## 范围
|
||||
|
||||
**做**:getAttributionCandidates 扩展(closed_at IS NULL OR ≥ 7 天前;closed/closedAt 字段;当前在前/清仓 closed_at DESC)+ AttributionSelect holdingId 键控 + 「(已清仓 MM-DD)」后缀 + 「当前归属」占位选项 + 回归 test-r017-candidates.mjs。
|
||||
**不做**:候选窗口可配置;自动归属;其他候选消费方调整;历史数据修复工具(一次性修复走 set-attribution)。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
src/storage/SqliteStore.js(getAttributionCandidates)、src/client/views/TradeRecordsTab.jsx(AttributionSelect)、scripts/test-r017-candidates.mjs(新增)。
|
||||
|
||||
## 实现步骤
|
||||
|
||||
1. R-017 + 本计划 + 三件套;2. 存储扩展;3. UI 重构;4. 回归 + typecheck + build + 存量回归;5. 复盘。
|
||||
|
||||
## 验收要点
|
||||
|
||||
见 `docs/04-迭代记录/15-归属候选纳入清仓持仓/验收标准.md`。
|
||||
@@ -0,0 +1,50 @@
|
||||
# 计划:持仓内存快照(服务端 10s 定时同步,请求不再穿透 QMT)(阶段航点)
|
||||
|
||||
> 编号:PLAN-013 | 粒度:阶段航点 | 创建:2026-09-02 | 状态:**已实施(待验收)**
|
||||
> 派生自终极目标:目标-008(真实交易系统接入)、目标-001(DSH 插件形态)
|
||||
> 依据需求:**R-014(已定稿,2026-09-02,老师逐项拍板 4 问)** —— 符合入范围门槛
|
||||
> 设计约束:技术约束-010(服务端中转 + 缓存模式沿用)、技术约束-012(存储设计)、技术约束-016(分域目录);本次新增 技术约束-017
|
||||
> 补充说明:本计划为「先实施后补档」——代码随架构梳理讨论当场落地(老师指令),文档链(本计划 + 迭代三件套 + 约束)事后补全,事实以迭代复盘为准
|
||||
|
||||
## 目标
|
||||
|
||||
服务端建立全量持仓**内存快照**(PositionSync,10s 定时与 QMT 同步),策略持仓 / 全部持仓 / 未分配三个接口改读快照为准,**消除「QMT 抖动 → 持仓页面白屏」**并去掉每次进 tab 的穿透 HTTP 调用;前端零改动。
|
||||
|
||||
## 范围
|
||||
|
||||
**做**:
|
||||
1. PositionSync(src/position/PositionSync.js):启动预热 + 10s 定时全量拉 QMT → 校验 → 整体替换内存快照;同步失败**保留上次快照**(不清空不报错);
|
||||
2. 空快照双重确认:/health 可用 + getAsset 账户身份可识别,二者兼备才接受为「真清仓」,否则视为异常保留旧快照;
|
||||
3. 读穿透兜底:PositionManager.getAllPositions() 快照为空 → 当场拉一次 QMT 并回填;未注入 positionSync 时保持旧穿透行为(兼容);
|
||||
4. 幽灵持仓自动清仓:QMT 快照连续 3 轮(约 30s)消失的 code,本地全部策略当前持仓 closeHolding 转历史;防抖护栏 = 账户身份守卫(accountId 未知当轮跳过;账户切换当轮重置计数并跳过);部分减持不触发;
|
||||
5. index.js 装配(start/dispose/注入 manager 与 api runtime);回归脚本(纯内存 mock,技术约束-011 不受影响)。
|
||||
|
||||
**不做**:
|
||||
- 不落库(无 positions_cache 表;不建表、不改 SQLite schema)——R-014 讨论 #1 明确;
|
||||
- 不复用 strategy_holdings(账本与对账单分离,holding_id 锚点语义不掺快照);
|
||||
- 前端改动(零改动;同步时间显示本轮不做);
|
||||
- 部分减持的自动修正(负数未分配仍由 UI 暴露,后续可另立需求);
|
||||
- N+1 委托查询、api/trades.js 绕门面等既有观察点(另行登记)。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
```
|
||||
src/
|
||||
├── position/PositionSync.js # 新增:持仓内存快照同步服务
|
||||
├── position/PositionManager.js # getAllPositions 改读快照 + 读穿透兜底(兼容旧构造)
|
||||
└── index.js # 装配 PositionSync + 注入
|
||||
scripts/
|
||||
└── test-position-sync.mjs # 回归脚本(纯内存 mock 34 项)
|
||||
```
|
||||
|
||||
## 实现步骤
|
||||
|
||||
1. PositionSync 模块(快照 + 定时 + 校验 + 幽灵清仓);
|
||||
2. PositionManager 读路径切换 + 读穿透 + 兼容分支;
|
||||
3. index.js 装配;
|
||||
4. 回归脚本(34 项)+ typecheck + build + r013 回归;
|
||||
5. 文档链补全(本计划 + 迭代三件套 + 技术约束-017 + 数据存储设计 §11)。
|
||||
|
||||
## 验收要点
|
||||
|
||||
- 见 `docs/04-迭代记录/12-持仓内存快照/验收标准.md`。
|
||||
@@ -0,0 +1,61 @@
|
||||
# 计划:数据存储管理(JSON → SQLite)(阶段航点)
|
||||
|
||||
> 编号:PLAN-007 | 粒度:阶段航点(大粒度) | 创建:2026-09-01 | 状态:**进行中**
|
||||
> 派生自终极目标:目标-003(按策略监控市场)、目标-005(市场复盘)、目标-006(交易复盘)——复杂查询/多数据集统一存储是复盘与监控的数据底座
|
||||
> 依据需求:**R-008(已定稿,2026-09-01,D1-D8 全部确认)** —— 符合入范围门槛
|
||||
> 设计约束:技术约束-012(变更中)、数据存储设计.md(第 9 节变更预告)
|
||||
|
||||
## 目标
|
||||
|
||||
将神之一手的数据存储从 **JSON data store**(store.json / store.market.json / store.schema.json)升级为 **SQLite 数据库**(node:sqlite):**仅存储引擎替换,对外行为不变**。引入 strategies + allocation + market_quotes 三表,策略定义仍存 DSH settings,JSON 迁移后废弃(迁移前自动备份)。
|
||||
|
||||
## 范围
|
||||
|
||||
**做**:
|
||||
1. **存储层封装**:新增 SqliteStore 模块,封装 node:sqlite(DatabaseSync)——隔离 experimental 风险,提供 init/migrate/upsert/query 能力;对外行为与 DataStore 兼容(D2);
|
||||
2. **表结构**:strategy_holdings(**持仓生命周期表**:自增 holding_id + strategy_id/code/shares/created_at/closed_at,一对多「多」侧单表)+ market_quotes_cache(code → last_price/last_close 两列,行情快照缓存)两表(D4;2026-09-01 讨论修正:不用 strategies JSON 列 + allocation 冗余双表;行情不用 snapshot JSON 列;持仓加生命周期为交易记录关联铺路);
|
||||
3. **策略定义位置**:仍存 DSH settings,不迁 SQLite(D5);
|
||||
4. **迁移**:一次性迁移脚本(scripts/)+ 启动检测自动迁移(旧 JSON 存在且 SQLite 空 → 自动迁移,幂等,迁移前自动备份)(D6);
|
||||
5. **JSON 废弃**:迁移后废弃 JSON 文件(迁移前备份)(D7);
|
||||
6. **DataStore 改造**:数据读写/迁移/schema 逻辑切换至 SQLite 后端,**存储层 API 升级为持仓生命周期语义**(openHolding/addShares/reduceShares/closeHolding/getCurrentHoldings/getHoldingHistory,废弃整策略重写的 setDataset/removeDataset);
|
||||
7. **MarketDataHub 适配**:行情持久化走 SQLite market_quotes_cache 表(落盘只投影 last_price/last_close 两列),防抖写回逻辑不变。
|
||||
|
||||
**不做**:
|
||||
- 不建 trades 表(R-007 交易记录时再建,D4);
|
||||
- 不做数据管理界面/导出/清理(D3:本期仅存储引擎替换);
|
||||
- 不引入新依赖(node:sqlite 为 Node 内置,零依赖分发,技术约束-005 不受影响);
|
||||
- 不改策略设置交互(D5);
|
||||
- 不实现复杂查询 API(本期仅保证现有行为不变,查询能力为后续功能铺路)。
|
||||
|
||||
## 程序结构(改造后)
|
||||
|
||||
```
|
||||
src/
|
||||
├── component/
|
||||
│ ├── SqliteStore.js # 新增:SQLite 存储层封装(node:sqlite)
|
||||
│ ├── DataStore.js # 改:读写/迁移切换至 SqliteStore(API 升级为持仓生命周期)
|
||||
│ └── MarketDataHub.js # 改:行情持久化走 SqliteStore.market_quotes_cache
|
||||
├── index.js # 改:实例化 SqliteStore 注入 DataStore
|
||||
scripts/
|
||||
└── migrate-json-to-sqlite.mjs # 新增:一次性迁移脚本(独立可执行)
|
||||
```
|
||||
|
||||
## 实现步骤(建议顺序)
|
||||
|
||||
1. **文档骨架**:PLAN-007 + 迭代 06 子目录(本步);
|
||||
2. **约束变更**:技术约束-012 更新 + 数据存储设计.md 第 9 节落实(SQLite 表结构设计替代 JSON schema);
|
||||
3. **SqliteStore**:node:sqlite 封装(init/事务/upsert/query/备份);
|
||||
4. **DataStore 改造**:load/save/行情读写切换 SqliteStore,存储层 API 升级为持仓生命周期,保留迁移检测;
|
||||
5. **PositionManager 适配**:整策略重写 → 单票生命周期操作;
|
||||
6. **迁移脚本**:一次性迁移脚本(JSON → SQLite)+ 启动自动迁移(幂等);
|
||||
6. **MarketDataHub 适配**:行情持久化切至 market_quotes_cache 表(投影 last_price/last_close);
|
||||
7. **构建 + 测试**:pnpm run build + typecheck + 独立数据目录回归测试(技术约束-011);
|
||||
8. **验收**:对照迭代 06 验收标准逐条核验,记录迭代复盘。
|
||||
|
||||
## 验收要点
|
||||
|
||||
- 数据落 SQLite(one-divine-lot.db),store.json/store.market.json 迁移后废弃(迁移前备份);
|
||||
- 对外行为不变:份额分配 CRUD、行情缓存查询/写回、重启后首屏有价;
|
||||
- 幂等:重复启动不重复迁移;迁移失败不破坏原 JSON;
|
||||
- 旧 allocations.json 迁移链路仍有效(并入 SQLite 迁移);
|
||||
- 技术约束-011:回归测试用独立数据目录(ODL_TEST_DATA_DIR)。
|
||||
@@ -0,0 +1,38 @@
|
||||
# 计划:盘口内存快照(QuoteSync/QuoteHub + 同步指示灯)(阶段航点)
|
||||
|
||||
> 编号:PLAN-014 | 粒度:阶段航点 | 创建:2026-09-02 | 状态:**已实施(待验收)**
|
||||
> 派生自终极目标:目标-003(按策略监控市场)、目标-008(真实交易系统接入)
|
||||
> 依据需求:**R-015(已定稿,2026-09-02,老师逐项拍板)** —— 符合入范围门槛
|
||||
> 设计约束:技术约束-010 变更(行情服务端中转改纯内存)、技术约束-012 变更(market_quotes_cache 退役)、技术约束-016(分域沿用);本次新增 技术约束-018、产品约束-012
|
||||
|
||||
## 目标
|
||||
|
||||
盘口数据与持仓数据对称收敛:QuoteSync/QuoteHub 纯内存管理(5s REST 同步 + 涨停跌停合约信息),market_quotes_cache 退役(DROP),价格单一入口;会话头部新增持仓/盘口同步指示灯(绿黄灰、点击即同步)。
|
||||
|
||||
## 范围
|
||||
|
||||
**做**:
|
||||
1. QuoteSync(新):启动 prime + 5s REST 刷新 watch 集合 + 涨停跌停按交易日拉取缓存 + 热切换重置;WS 通路不迁移(删除);
|
||||
2. QuoteHub(新):内存快照 + 合约信息缓存 + watch 集合 + 读穿透(走 dataSource.getTicks)+ getQuote(s) 对外;对外方法签名与旧 hub 兼容(getByCodes/watch/stats/getQuote);
|
||||
3. 数据源适配:QmtBridgeRestDataSource 新增 getTicks(codes)(/data/tick 语义化批量);
|
||||
4. 存储清理:SqliteStore 删行情表读写 + DROP TABLE market_quotes_cache(幂等)+ migrateJson/isEmpty 联动;DataStore 删 loadMarket/getMarketQuote(s)/setMarketQuotes;
|
||||
5. API:market-snapshot 返回加涨停/跌停/昨收/updatedAt;新增 sync-status(吸收 market-stats)+ sync-now {domain};
|
||||
6. 前端:QmtConnectionChip 加 SyncIndicators(两圆点,10s 轮询,点击即同步);MarketDataProvider 清理 wsInfo 残留;
|
||||
7. 策略持仓表行情列(老师确认并入):COLUMN_META + 涨停价/跌停价/今开/最高(defaultVisible: false,纯价格);StrategyTab.renderDataCell 对应 case;normalizeStrategyColumns 缺省显隐跟随 defaultVisible;
|
||||
7. 回归脚本 test-quote-sync.mjs(纯内存 mock)。
|
||||
|
||||
**不做**:watch 收缩/降频;个股停牌标识;WS 相关需求推进;PriceCell 改动;最低/成交量/成交额列;距离涨停百分比展示。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- 新增:src/market/QuoteSync.js、src/market/QuoteHub.js、src/client/views/SyncIndicators.jsx、scripts/test-quote-sync.mjs
|
||||
- 删除:src/market/MarketFeed.js、src/market/MarketDataHub.js
|
||||
- 修改:src/data-source/QmtBridgeRestDataSource.js(getTicks)、src/storage/SqliteStore.js、src/storage/DataStore.js、src/api/market.js、src/api/qmt-connections.js(热切换联动)、src/index.js、src/client/views/QmtConnectionChip.jsx、src/client/market/MarketDataProvider.jsx
|
||||
|
||||
## 实现步骤
|
||||
|
||||
1. 文档链(本计划 + 迭代三件套 + 约束条目);2. 数据源 getTicks;3. QuoteHub + QuoteSync;4. 存储清理 + DROP;5. API 改造;6. 前端指示灯 + 清理;7. 回归脚本 + typecheck + build + 存量回归;8. 复盘。
|
||||
|
||||
## 验收要点
|
||||
|
||||
见 `docs/04-迭代记录/13-盘口内存快照/验收标准.md`。
|
||||
@@ -0,0 +1,49 @@
|
||||
# 计划:策略 tab 历史持仓展示(显示/隐藏已清仓持仓 + 清仓时间范围筛选)(阶段航点)
|
||||
|
||||
> 编号:PLAN-015 | 粒度:阶段航点 | 创建:2026-09-07 | 状态:**已实施(待验收)**
|
||||
> 派生自终极目标:目标-003(按策略监控市场)、目标-008(真实交易系统接入)
|
||||
> 依据需求:**R-016(已定稿,2026-09-07,Q1-Q7 老师拍板)** —— 符合入范围门槛
|
||||
> 设计约束:新增 技术约束-019(历史持仓查询走本地库,当前持仓快照语义不掺历史)
|
||||
|
||||
## 目标
|
||||
|
||||
两个策略 tab(做T / 网格超市)title 旁提供「历史持仓」开关 + 清仓时间范围筛选(默认近一周,另有近 1 个月 / 近 3 个月 / 近半年 / 近 1 年);历史持仓行可展开查看关联交易记录(复用 R-010),补上「清仓即失联」的操作追溯缺口。
|
||||
|
||||
## 范围
|
||||
|
||||
**做**:
|
||||
1. **存储层**(SqliteStore):新增 getHoldingsHistory(strategyId, { sinceMs }) —— closed_at 非空 + closed_at ≥ sinceMs 过滤 + 按 closed_at DESC 排序(读只读,零写路径变更,不改 closeHolding 语义);
|
||||
2. **门面层**(DataStore):getHoldingsHistory 透传(await _ensure 后委托);
|
||||
3. **API**:新增 strategy-holdings/history 端点(args: strategyId 必填 + range ∈ week|month|quarter|halfYear|year,服务端换算 sinceMs;缺失/非法参数报参数错误);strategy-positions 等现有端点零改动;
|
||||
4. **前端 StrategyTab**:
|
||||
- 头部按钮区(+ 添加持仓 / 列设置 旁)新增「历史持仓」开关按钮 + 范围下拉(仅开关开启时显示;默认 week);
|
||||
- 开启时懒加载历史数据(切换范围重新拉取;关闭即隐藏不销毁缓存);进 tab 默认关(会话级,不持久化);
|
||||
- 历史行渲染在当前持仓行之后:代码 / 名称(关联委托 name 兜底,无则 —)/ 份额(显示 DB 原值 0,Q2)/ 清仓时间 / 持有天数;实时行情列(现价/涨幅/涨停跌停/今开/最高)显示 —;
|
||||
- 历史行灰显 + 「已清仓」徽标(含清仓日期);标题计数(N 只)只数当前持仓;
|
||||
- 历史行行展开复用 R-010(trades/by-holding,按 holdingId 懒加载);
|
||||
- 自定义字段列历史行只读展示(values 随行返回,无编辑入口);
|
||||
5. **回归脚本** scripts/test-r016-history.mjs(独立临时数据目录,技术约束-011)。
|
||||
|
||||
**不做**:全部持仓 tab;盈亏统计;清仓方式标记;历史行实时行情;成本价/收益推导;closeHolding 语义与任何存储写入路径变更;范围偏好持久化;「全部历史」档位。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- 修改:src/storage/SqliteStore.js、src/storage/DataStore.js、src/api/strategies.js、src/client/views/StrategyTab.jsx
|
||||
- 新增:scripts/test-r016-history.mjs
|
||||
|
||||
## 实现步骤
|
||||
|
||||
1. 本文档链(PLAN + 迭代三件套 + 技术约束-019);2. SqliteStore.getHoldingsHistory + DataStore 门面;3. API 端点;4. StrategyTab 开关/范围/历史行渲染/展开复用;5. 回归脚本 + typecheck + build + 存量回归(test-position-sync / test-r013 / test-quote-sync);6. 复盘。
|
||||
|
||||
## 验收要点
|
||||
|
||||
见 `docs/04-迭代记录/14-策略tab历史持仓展示/验收标准.md`。
|
||||
|
||||
## 二轮补充(2026-09-07 Q8-Q9 老师拍板:今日已清仓默认层)
|
||||
|
||||
- **补充范围**:已清仓行分两层——L1「今日已清仓」(range='today',本地自然日 00:00 起)默认恒显示、不受「历史持仓」开关控制;L2「历史范围层」(今天之前的 week/month/quarter/halfYear/year)仍由开关控制(默认关)。前端按 holdingId 去重两层(开启范围不重复出两行);标题计数不含已清仓行维持;零写路径变更维持。
|
||||
- **不做(维持首轮)**:不做统计 / 不改 closeHolding / 范围偏好不持久化 / 全部持仓 tab 不加 / 不新增「全部历史」档。
|
||||
|
||||
## 验收要点(二轮)
|
||||
|
||||
见验收标准「二轮补充验收线 7-10」;自动化已复跑:test-r016 24/24 + 存量回归全绿。
|
||||
@@ -0,0 +1,23 @@
|
||||
# 计划:策略会话——会话以策略当前数据为依据做讨论(阶段航点)
|
||||
|
||||
> 来源:R-021(合并原 R-019+R-021,2026-09-08 已定稿)| 状态:执行中(设计阶段先行)| 所属迭代:17-策略会话
|
||||
> 粒度:阶段航点(覆盖设计 → 宿主调研 → 实现 → 验收全过程,随进展拆分小任务)
|
||||
|
||||
## 目标
|
||||
|
||||
让 AI 会话可以"以策略为 Workspace"(数据依据版):**对话开始前选「讨论策略」→ 服务端生成该策略当前数据摘要 → 预注入会话上下文 → 以该策略当前数据为依据进行复盘/分析讨论**;只读、会话中可换/刷新、产物不落盘。
|
||||
|
||||
## 范围与分阶段
|
||||
|
||||
1. **设计阶段(当前,老师指令:先做产品逻辑 + UI 交互逻辑)**:产出《产品逻辑设计》《UI交互设计》,D 系列问题老师拍板后定稿;
|
||||
2. **宿主注入通道调研**:确认把外部摘要注入会话上下文的可行通道/形态(决定"折叠数据依据条"还是"文本消息形态"呈现;影响客户端渲染方案与注入动作实现);
|
||||
3. **技术实现方案**(技术约束核对、服务端摘要服务 + 客户端选择 UI + 注入动作 + 回归);
|
||||
4. **实现 + 验收**。
|
||||
|
||||
## 验收指向
|
||||
|
||||
见 `docs/04-迭代记录/17-策略会话/验收标准.md`(实现阶段补充细化)。
|
||||
|
||||
## 边界
|
||||
|
||||
见 R-021 定稿边界(合并原 R-019+R-021):不用宿主工作区;不开放账本写;不做按需取数工具;复盘产物不落盘;本期不做通用视图(全部持仓/交易记录)作依据。
|
||||
@@ -0,0 +1,55 @@
|
||||
# 计划:策略持仓行展开关联交易记录(Holding → 交易汇总)(阶段航点)
|
||||
|
||||
> 编号:PLAN-009 | 粒度:阶段航点(大粒度) | 创建:2026-09-02 | 状态:**已完成(2026-09-02 迭代 08 老师确认)**
|
||||
> 派生自终极目标:目标-006(交易复盘)、目标-003(按策略监控市场)
|
||||
> 依据需求:**R-010(已定稿,2026-09-02,Q1-Q3 确认)** —— 符合入范围门槛
|
||||
> 设计约束:技术约束-012(数据存储设计)、技术约束-001/003/004(沿用)
|
||||
|
||||
## 目标
|
||||
|
||||
策略持仓 tab 每个持仓行(Holding)可展开,展开显示与该 holding 关联的交易记录(仅委托汇总,不分笔成交),实现「持仓 ↔ 交易」的双向追溯(R-009 反向:交易→持仓已实现,本迭代持仓→交易)。
|
||||
|
||||
## 范围
|
||||
|
||||
**做**:
|
||||
1. **strategy-positions 附加 holding_id**:PositionManager.getStrategyPositions 返回时,查 strategy_holdings 附加当前持仓的 holding_id(Q1);
|
||||
2. **trades/by-holding 端点**:按 holding_id 查 trade_orders,返回委托汇总列表(时间/方向/状态/委托量/成交量/均价/金额/费用)(Q2);
|
||||
3. **持仓行展开 UI**:StrategyTab 持仓行加展开(类似交易记录 tab 展开效果),展开后内嵌小表格显示该 holding 的委托汇总;懒加载(展开时才请求)(Q3)。
|
||||
|
||||
**不做**:
|
||||
- 分笔成交明细展示(老师明确只要汇总);
|
||||
- 交易记录 tab 改动;
|
||||
- 归属设置入口(R-009 已有)。
|
||||
|
||||
## 程序结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── component/
|
||||
│ ├── PositionManager.js # 改:getStrategyPositions 附加 holding_id
|
||||
│ ├── SqliteStore.js # 改:+getOrdersByHolding(holdingId)
|
||||
│ └── DataStore.js # 改:+getTradeOrdersByHolding
|
||||
├── api/
|
||||
│ └── trades.js # 改:+trades/by-holding 端点
|
||||
src/client/views/
|
||||
└── StrategyTab.jsx # 改:持仓行展开 + 内嵌委托汇总表(懒加载)
|
||||
```
|
||||
|
||||
## 实现步骤
|
||||
|
||||
1. **文档骨架**:PLAN-009 + 迭代 08(本步);
|
||||
2. **SqliteStore**:getOrdersByHolding(WHERE holding_id=? ORDER BY insert_ts DESC);
|
||||
3. **DataStore**:委托方法;
|
||||
4. **PositionManager**:getStrategyPositions 附加 holding_id;
|
||||
5. **api/trades.js**:trades/by-holding 端点;
|
||||
6. **StrategyTab**:持仓行展开 + 懒加载 + 内嵌汇总表;
|
||||
7. **构建测试**:pnpm run build + typecheck + 独立数据目录回归(技术约束-011);
|
||||
8. **验收 + 复盘**。
|
||||
|
||||
## 验收要点
|
||||
|
||||
- strategy-positions 每行含 holding_id(同策略同 code 当前持仓);
|
||||
- trades/by-holding 返回该 holding 的委托汇总(无成交的委托也显示);
|
||||
- StrategyTab 持仓行可展开,展开显示委托汇总表(不分笔成交);
|
||||
- 懒加载:不展开不请求;
|
||||
- 现有功能不回归。
|
||||
@@ -0,0 +1,40 @@
|
||||
# 计划:策略数据 JSON data store schema 标准化(阶段航点)
|
||||
|
||||
> 编号:PLAN-005 | 粒度:阶段航点(大粒度) | 创建:2026-08-31 | 状态:**已完成(2026-09-01 迭代 04 验收通过,R-006 已实现)**
|
||||
> 派生自终极目标:目标-001(DSH 插件形态)
|
||||
> 依据需求:**R-006(已定稿,2026-08-31)** —— 符合入范围门槛
|
||||
> 设计约束:技术约束-011(测试数据隔离)、技术约束-012(新增:JSON data store schema)
|
||||
|
||||
## 目标
|
||||
|
||||
将策略份额分配数据(现 allocations.json,code 为中心)标准化为 **JSON data store schema**:
|
||||
- store.schema.json:描述数据集(dataset)的标准字段(类 JSON Schema);
|
||||
- store.json:实际数据文件,策略为中心(strategies[].dataset = [{code, shares}]);
|
||||
- 策略定义仍留 DSH settings(设置页交互不变);
|
||||
- 旧 allocations.json 启动时自动迁移(保留备份)。
|
||||
|
||||
## 范围
|
||||
|
||||
**做**:
|
||||
1. 新增 DataStore 模块(src/component/DataStore.js):
|
||||
- 定义 store.schema.json 与 store.json 结构;
|
||||
- 读写 store.json(schema 校验 + 原子写);
|
||||
- 迁移:启动时检测旧 allocations.json → 转换为 store.json(code 为中心 → 策略为中心),保留 allocations.json.bak;
|
||||
- 数据集 CRUD:getDataset(strategyId) / setDataset(strategyId, items) / removeDataset(strategyId)。
|
||||
2. PositionManager / api 改造:
|
||||
- 份额读写改走 DataStore(替代 AllocationStorage);
|
||||
- strategies/remove 联动删 store dataset(同现状联清份额模式);
|
||||
- 现有端点(add-shares/remove-shares/strategy-positions/summary 等)行为不变。
|
||||
3. 删除/归档 AllocationStorage(迁移完成后)。
|
||||
|
||||
**不做**:
|
||||
- 策略定义迁移(保留 settings);
|
||||
- 不引入数据库(数据量少,JSON 足够)。
|
||||
|
||||
## 实现步骤
|
||||
|
||||
1. DataStore 模块(schema + 读写 + 迁移 + dataset CRUD);
|
||||
2. PositionManager 改接 DataStore;
|
||||
3. api 编排(strategies/remove 联动删 dataset);
|
||||
4. 构建 + 测试隔离模式验证迁移;
|
||||
5. 验收 + 记录迭代。
|
||||
@@ -0,0 +1,58 @@
|
||||
# 计划:策略自定义字段配置(定义随策略,值落库)(阶段航点)
|
||||
|
||||
> 编号:PLAN-012 | 粒度:阶段航点 | 创建:2026-09-02 | 状态:**已完成(迭代 11 验收通过,2026-09-02)**
|
||||
> 派生自终极目标:目标-002(策略定义能力)、目标-007(人机合一)
|
||||
> 依据需求:**R-013(已定稿,2026-09-02,老师确认 Q1-Q4 + D6)** —— 符合入范围门槛
|
||||
> 设计约束:技术约束-012(策略定义仍存 DSH settings / SQLite 存储)沿用;本次新增 产品约束-010、技术约束-015、UI约束-005
|
||||
|
||||
## 目标
|
||||
|
||||
策略可**自定义、可扩展**:每个策略在设置页「策略分组」子 tab 配置自己的自定义字段定义(configSchema,含类型/枚举/默认值,随策略定义存 settings 不落库);该策略下的每个持仓(strategy_holdings 行)按定义存取一份键值对值(新增 values JSON 列落库);策略持仓 tab 持仓行展开区按定义渲染并编辑值。旧策略无定义的行为与现状完全一致。
|
||||
|
||||
## 范围
|
||||
|
||||
**做**:
|
||||
1. settings.strategies 扩展 configSchema(schemastery:array of object,type union text|number|boolean|enum + enum 选项 + def 默认值),读取归一化(旧项缺省 []);
|
||||
2. strategy_holdings 新增 values TEXT(JSON) 列(幂等 ALTER,沿用 _ensureTradeAttributionColumns 模式,只读容忍);新增 readValues(holdingId) / writeValues(holdingId, values);现有 openHolding/addShares/reduceShares/closeHolding 保持 values 不随份额操作变动;
|
||||
3. API:strategy-positions 每行附 values;新增 holdings/values-update {holdingId, values} 写回(服务端按 configSchema 校验:数字有限数、枚举在选项内、布尔为布尔;允许额外键=可扩展;空值/缺省可写入);
|
||||
4. 设置页「策略分组」子 tab:策略行可展开 → 展开区字段列表(label/key/type/enum/def)+ 添加/编辑/删除字段(字段名/类型/选项/默认值表单)+ 保存走 strategies/update(整表);
|
||||
5. 策略持仓 tab:持仓行展开区(R-010 基础上)增加「自定义字段」区块:按该策略 configSchema 渲染输入控件(文本=输入框、数字=数字输入、布尔=开关、枚举=下拉),值来自该行 values,编辑即保存调 values-update,保存成功 Toast 反馈;
|
||||
6. 回归脚本(独立数据目录,技术约束-011):定义字段 → 批量写/改持仓行 values → 校验存储与读取、校验过界值拒绝。
|
||||
|
||||
**不做**:
|
||||
- 字段定义落库(定义随策略定义走,D1);
|
||||
- 策略级(整策略一份)值(值=持仓级,Q1);
|
||||
- T-006 数据池 / 表格列动态配置(独立需求,起草中);
|
||||
- T-003 策略配置文件骨架(被本需求吸收,不另做);
|
||||
- 删除策略时字段定义联动处理之外的数据迁移(兼容:旧策略无定义、旧行 NULL,不迁移,Q4)。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
```
|
||||
src/
|
||||
├── settings.js # strategySchema 扩展 configSchema + 归一化
|
||||
├── storage/SqliteStore.js # _ensureHoldingValuesColumn + readValues/writeValues
|
||||
├── api/strategies.js # strategy-positions 附加 values + holdings/values-update 端点
|
||||
├── client/views/SettingsSection.jsx # 「策略分组」子 tab 策略行展开配置字段
|
||||
└── client/views/StrategyTab.jsx # 持仓行展开区「自定义字段」编辑
|
||||
scripts/
|
||||
└── test-r013-custom-fields.mjs # 回归脚本(独立数据目录)
|
||||
```
|
||||
|
||||
## 实现步骤
|
||||
|
||||
1. **文档骨架**:PLAN-012 + 迭代 11(迭代目标 / 技术实现方案 / 验收标准)+ 约束条目(产品约束-010、技术约束-015、UI约束-005)(本步);
|
||||
2. **数据层**:settings.js configSchema 扩展 + SqliteStore 补列/读写(幂等迁移验证);
|
||||
3. **API 层**:strategy-positions 附加 values + holdings/values-update(含 configSchema 校验);
|
||||
4. **设置页 UI**:「策略分组」展开配置字段;
|
||||
5. **策略持仓 UI**:展开区自定义字段编辑(文本/数字/布尔/枚举);
|
||||
6. **回归脚本 + 验证 + 验收复盘**。
|
||||
|
||||
## 验收要点
|
||||
|
||||
- 设置页「策略分组」:策略行可展开,添加/编辑/删除字段(四种类型 + 枚举选项 + 默认值)后保存,重启后定义仍在;
|
||||
- 策略持仓 tab:持仓行展开可见「自定义字段」区块,四种类型控件按定义渲染,编辑值保存后刷新仍在(落库);
|
||||
- 同策略多行各有各的值;不同策略字段集互不影响;
|
||||
- 旧策略(无 configSchema)持仓行不出现字段区,现有操作(加仓/减仓/清仓/展开交易记录)不受影响;
|
||||
- 服务端校验生效:数字填非数字、枚举填选项外拒绝并提示;
|
||||
- typecheck + build 通过;回归脚本全绿。
|
||||
@@ -0,0 +1,92 @@
|
||||
# 计划:策略计算字段 + 策略 tab 静态化重构(阶段航点)
|
||||
|
||||
> 编号:PLAN-019 | 粒度:阶段航点 | 创建:2026-09-10 | 状态:**进行中(迭代 22 交互设计阶段)**
|
||||
> 派生自终极目标:目标-002(策略定义能力)、目标-003(按策略监控市场)
|
||||
> 依据需求:**R-027(已定稿,2026-09-10,重构·第一阶段)+ R-026(已定稿,2026-09-10,计算字段·第二阶段)** —— 均符合入范围门槛
|
||||
> 设计约束:沿用 技术约束-012(策略定义存储)、技术约束-015(configSchema)、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静态化(两阶段)。
|
||||
+12
-2
@@ -9,8 +9,18 @@
|
||||
|
||||
| 编号 | 约束说明 | 添加日期 | 生效状态 | 失效日期 | 最后一次变更描述 |
|
||||
|---|---|---|---|---|---|
|
||||
| (待添加) | | | | | |
|
||||
| UI约束-001 | 会话头部 QMT 连接切换 chip:紧凑形态 `QMT: <激活配置名> ▾`,与 PTC 模式标签并排;下拉菜单列出全部配置(当前激活项勾选标记),点选即激活;操作结果用顶部轻提示反馈(成功绿/失败红,约 3s 消失,沿用现有 Toast 体系) | 2026-08-29 | 生效 | - | R-004 定稿(2026-08-29):Q9/Q10 确认的头部控件形态 |
|
||||
| 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 | 策略自定义字段配置 UI(R-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 | 生效 | - | 讨论确认:移动端优先,避免滚动打断仪式感 |
|
||||
-->
|
||||
-->
|
||||
+13
-2
@@ -11,8 +11,19 @@
|
||||
|---|---|---|---|---|---|
|
||||
| 产品约束-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-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 定稿);**变更 1(2026-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 | 生效 | - | 讨论确认:为保证公平性,抽取必须不可预测 |
|
||||
|
||||
+20
-1
@@ -11,11 +11,30 @@
|
||||
|---|---|---|---|---|---|
|
||||
| 技术约束-002 | 分仓管理采用「全量持仓 + 标签」模型:持仓数据为账户真实持仓(来自数据源适配器),标签为逻辑分组元数据(独立于持仓存储,可增删改),聚合视图按标签汇总仓位 | 2026-08-27 | 生效 | - | 讨论确认(R-002):全量持仓为基础,标签体系做逻辑分仓 |
|
||||
| 技术约束-001 | 行情/交易数据源必须通过统一接口抽象(定义统一的数据模型与操作接口),实现层为具体数据源适配器;当前限定实现 QMT Bridge 适配器,未来可新增其他行情/交易接口适配器,业务层不感知具体数据源 | 2026-08-27 | 生效 | 2026-08-27 | 变更(2026-08-27 R-002 讨论):原为「QMT Bridge MCP 适配器」,修正为**直接调用 QMT Bridge RESTful 接口**(http://192.168.3.43:8610),MCP 仅作为 Agent 侧封装层,插件侧直连 REST |
|
||||
| 技术约束-003 | QMT Bridge 数据源通过其 RESTful HTTP 接口接入(基础地址 http://192.168.3.43:8610,OpenAPI v3 规范),不经过 MCP 层;MCP 是给 Agent 用的封装,插件内部直连 REST | 2026-08-27 | 生效 | - | 讨论确认(R-002 第 7 轮):老师指出 MCP 是给智能体用的,插件应直连同一服务端口的 RESTful 接口 |
|
||||
| 技术约束-003 | QMT Bridge 数据源通过其 RESTful HTTP 接口接入(基础地址 http://192.168.3.43:8610,OpenAPI v3 规范),不经过 MCP 层;MCP 是给 Agent 用的封装,插件内部直连 REST | 2026-08-27 | 生效 | - | 讨论确认(R-002 第 7 轮):老师指出 MCP 是给智能体用的,插件应直连同一服务端口的 RESTful 接口;变更标注(2026-08-29 R-004 定稿):基础地址由固定单一地址改为多配置动态管理(由激活配置决定,见技术约束-008),直连 REST 原则不变 |
|
||||
| 技术约束-004 | 插件服务端 API 用 webServer 自开路由(如 /odl/api/*),**不直接使用 connection.rpc.intercept('/api')** —— DSH 的 /api 通道只能一个 interceptor(api-gateway 已占用),重复 intercept 会抛错导致插件 apply 失败 | 2026-08-28 | 生效 | - | 迭代 01 复盘沉淀:RPC 冲突导致服务端插件 apply 失败、RPC 404 |
|
||||
| 技术约束-005 | 客户端插件 bundle 必须是 CJS + window.__ModuleLoader__.load({id, factory}) 包装(tsdown 构建 + wrap 脚本),裸 ESM 无法被 DSH 客户端模块系统加载 | 2026-08-28 | 生效 | - | 迭代 01 复盘沉淀:客户端 bundle 未包装导致 loaded without registering via __ModuleLoader__.load |
|
||||
| 技术约束-006 | 客户端 slots.register 的 component 必须是第二参数(register({...}, Component));settings schema 必须用 schemastery z.object() 函数式定义 | 2026-08-28 | 生效 | - | 迭代 01 复盘沉淀:component 位置错误致 React #130;普通对象 schema 报 schema is not a function |
|
||||
| 技术约束-007 | 插件安装用 dsh plugin add(自动 reconcile bundles),不直接用 pnpm add;bundle patch 顶层必须是 insert 操作 | 2026-08-28 | 生效 | - | 迭代 01 复盘沉淀:pnpm add 不会更新 dsh.profile.bundles |
|
||||
| 技术约束-008 | QMT 连接配置存储复用 one-divine-lot settings namespace(新增 qmtConnections 字段:list[{id,name,baseUrl,order}] + activeId + defaultId),与策略配置同机制持久化;激活切换 = 更新数据源实例的 baseUrl(数据源按请求读取地址,已核实),立即生效无需重启 DSH;插件启动时激活默认配置(列表为空时回退 cordis 注入的 qmtBaseUrl 兜底,不做自动迁移);测试连接由服务端代理请求 {baseUrl}/health(避免浏览器跨域) | 2026-08-29 | 生效 | - | R-004 定稿(2026-08-29):Q1/Q2/Q4/Q5/Q8 确认(启动自动激活默认、立即切换、不迁移、超时不配置化、复用 settings) |
|
||||
| 技术约束-012 | 插件数据存储遵循 **docs/03-设计约束/数据存储设计.md**:存储引擎为 **SQLite(node:sqlite)**——strategy_holdings + ~~market_quotes_cache~~(**R-015 退役 DROP,2026-09-02**;行情改内存快照)+ **trade_orders + trade_fills(交易记录两表,R-009)**;存储层单票生命周期操作;交易记录两表零冗余 strategy_id/holding_id(外键链推导策略归属);策略定义仍存 DSH settings;旧 JSON(store.json / store.market.json / allocations.json)经一次性迁移脚本 + 启动自动迁移(幂等、迁移前自动备份)后废弃;仅存储引擎替换,对外行为不变 | 2026-09-01 | 生效 | - | 变更(2026-09-01 R-008 定稿 + 迭代 06 实施):JSON data store → SQLite;变更(2026-09-01 R-009 定稿 + 迭代 07 实施):+trade_orders/trade_fills 交易记录两表;**变更 3(2026-09-02 R-015/迭代 13)**:market_quotes_cache 退役(DROP),行情移出 SQLite |
|
||||
| 技术约束-011 | 测试/回归脚本**禁止在真实数据上执行写操作**:份额写操作(add/remove/move/clear)必须使用独立数据目录(AllocationStorage 支持 ODL_TEST_DATA_DIR 环境变量或 dataDir 参数指向临时目录),只读端点(positions/summary/strategies/market-snapshot)可直连生产 API | 2026-08-31 | 生效 | - | 2026-08-31 数据误删事故沉淀:回归测试误删大连热电/万顺新材份额分配,老师定「测试用独立数据目录」 |
|
||||
| 技术约束-010 | 行情实时数据由**服务端中转 + 内存缓存**提供(R-005 演进,2026-08-31 老师改;R-015 二改,2026-09-02):DSH 服务端做「REST 轮询 + 行情内存缓存」,前端统一轮询 /odl/api/market-snapshot(不做前端直连,无跨域);~~行情持久化到 store.market.json~~(**R-015 失效:改纯内存,QuoteSync 启动 prime + 读穿透保证秒级有价**);~~QMT Bridge WS 推送~~(**R-015 移除 WS 通路**,见技术约束-018) | 2026-08-31 | 生效 | - | 变更(2026-08-31):老师由「前端直连 WS」改为「服务端中转 + 缓存」;2026-09-01 加行情持久化与启动 prime;**变更 2(2026-09-02 R-015/迭代 13)**:持久化与 WS 均失效——行情纯内存(QuoteSync/QuoteHub),落库与 WS 通路删除 |
|
||||
| 技术约束-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 归一化;委托交易日 = insertDate(tradeDate 兜底) | 2026-09-01 | 生效 | - | 新增(2026-09-01 R-009 定稿 + 迭代 07 实施):两表 + 定时同步 + 本地历史查询;变更 1(2026-09-01):+code 归一化 + tradeDate 兜底;变更 2(2026-09-01 老师二次定稿):归属改**手动设置**(trade_orders 冗余 strategy_id+holding_id,UPSERT 不覆盖归属列),弃算法推导 |
|
||||
| 技术约束-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 TEXT(JSON 键值对,key 对齐 configSchema.key,允许额外键=可扩展,NULL=未配置);补列用幂等 ALTER(沿用 _ensureTradeAttributionColumns 模式,只读连接容忍);API:strategy-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)**:定义真相源迁 strategyFields(strategies 只读兼容)、类型白名单加计算类型、写路径改单策略端点 |
|
||||
| 技术约束-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/ 仅放 UI(views/ 组件 + market/ provider);文件命名 = 类名(PascalCase)+ .js/.jsx;新增服务端模块必须先落对应域目录,无合适域时先讨论补域,不得回退平铺 | 2026-09-02 | 生效 | - | 新增(2026-09-02 结构审查 + 优化落地):component/ 平铺还原为语义分域,删除死代码 AllocationStorage、DataStore.setDataset/removeDataset |
|
||||
| 技术约束-017 | 持仓内存快照(R-014,2026-09-02;**变更 1:2026-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 实施);**变更 1(2026-09-08 R-018/迭代 16 拍板)**:幽灵自动清仓退役(不再 closeHolding 本地账本),PositionSync 只同步对账单快照;漏关联由只读软提示承担 |
|
||||
| 技术约束-018 | 盘口内存快照(R-015,2026-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**(幂等),价格单一入口 = QuoteHub,DataStore 行情方法(loadMarket/getMarketQuote(s)/setMarketQuotes)删除;同步失败保留内存旧值;watchCodes 维持只进不出无上限;涨停/跌停/昨收不落库 | 2026-09-02 | 生效 | - | 新增(2026-09-02 R-015 定稿 + 迭代 13 实施):老师五拍板(替换/WS 移除/纯内存 DROP/watch 现状/指示灯);warmup bug 复现(loadMarket return this 残迹)为不落库关键证据 |
|
||||
|
||||
| 技术约束-019 | 历史持仓查询(R-016,2026-09-07):策略 tab 历史持仓展示走**本地库只读查询**(SqliteStore.getHoldingsHistory:strategy_id + closed_at IS NOT NULL + closed_at ≥ sinceMs,closed_at DESC),经独立端点 strategy-holdings/history 暴露;**当前持仓路径(strategy-positions / PositionSync 快照语义)不掺历史数据**(两份结果前端合并渲染);closeHolding 置 shares=0 语义维持不变(Q2 老师拍板:历史行份额显示 0,重点在追溯该持仓的历史操作而非清仓时份额);范围换算服务端做(week=7d/month=30d/quarter=90d/halfYear=182d/year=365d 自然日近似)——**变更 1(2026-09-07 二轮补充 Q8-Q9 老师拍板)**:range 新增 'today' = **本地自然日 00:00 起**(特判零点,不落回溯毫秒档),供前端「今日已清仓默认层」(恒显示、不受历史持仓开关控制);历史范围层语义收窄为**今天之前**;其余不变 | 2026-09-07 | 生效 | - | 变更 1(2026-09-07 R-016 二轮补充 Q8-Q9 老师拍板):+range=today 自然日边界,历史开关只控今天之前;首轮新增(Q1-Q7):历史行=追溯操作锚点,不动存储写路径 |
|
||||
| 技术约束-020 | QMT 连接健康自适应探测(R-022,2026-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-023,2026-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-026,2026-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-eval(CVE-2025-12735 变量对象注入 → RCE)、mathjs(标识符不含 CJK)、Jexl(ASCII 标识符)、CEL、解析器工具包后**维持自写引擎**,论证见 迭代 23 技术方案 §4.5 |
|
||||
| 技术约束-023 | 计算字段现算口径(R-026,2026-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-026,2026-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 既有能力,新框架需论证 |
|
||||
|
||||
@@ -0,0 +1,453 @@
|
||||
# 数据存储设计(JSON Data Store Schema)
|
||||
|
||||
> 策略数据集 + 行情缓存的 JSON data store 设计(R-006 及其扩展,2026-08-31 / 2026-09-01 定)
|
||||
> 本文件是数据存储领域的**设计约束文档**,后续数据存储相关迭代以此为依据。
|
||||
|
||||
## 1. 设计目标
|
||||
|
||||
1. **数据快速展现**:DataStore 的核心目标是「数据快速展现」——份额分配、行情价格持久化,重启/刷新后首屏即有数据,不依赖每次实盘查询。
|
||||
2. **标准化描述**:为每个策略定义标准化的字段描述(schema),在描述之上挂数据集。
|
||||
3. **统一 JSON 操作**:数据量很少(无需数据库/Circe),用 JSON 文件 + 类 JSON Schema 统一读写与校验。
|
||||
|
||||
## 2. 总体架构
|
||||
|
||||
```
|
||||
~/.dsh/one-divine-lot/
|
||||
├── store.schema.json # 数据集 schema(类 JSON Schema,随代码发布,自动生成)
|
||||
├── store.json # 份额分配数据(策略为中心,原子写)
|
||||
└── store.market.json # 行情快照缓存(code → 最新行情,原子写)
|
||||
```
|
||||
|
||||
**分层**:
|
||||
- **策略定义**(id/name/visible/order):存 DSH settings(~/.dsh/settings.yaml),设置页交互不变
|
||||
- **份额分配**(策略 → 股票 → 股数):存 store.json(策略为中心)
|
||||
- **行情快照**(code → lastPrice 等):存 store.market.json(持久化缓存)
|
||||
|
||||
## 3. 数据文件结构与 Schema
|
||||
|
||||
### 3.1 store.schema.json(数据集 schema)
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"kind": "one-divine-lot-data-store",
|
||||
"strategies": {
|
||||
"description": "策略数据集:每个策略一个 dataset(份额分配)",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"strategyId": { "type": "string", "required": true, "description": "策略 id(与 settings 中的策略一致)" },
|
||||
"dataset": {
|
||||
"type": "array",
|
||||
"description": "该策略的持仓数据集(份额分配)",
|
||||
"items": {
|
||||
"code": { "type": "string", "required": true, "description": "证券代码(含后缀,如 600719.SH)" },
|
||||
"shares": { "type": "number", "required": true, "description": "份额(股数,>0)" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 store.json(份额分配,策略为中心)
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"strategies": [
|
||||
{
|
||||
"strategyId": "grid-supermarket",
|
||||
"dataset": [
|
||||
{ "code": "600719.SH", "shares": 800 },
|
||||
{ "code": "300057.SZ", "shares": 1000 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"strategyId": "manual-t",
|
||||
"dataset": [
|
||||
{ "code": "600719.SH", "shares": 1000 }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**数据视角**:按策略查持仓(策略为中心),比旧 code 为中心(allocations.json)更贴合业务。
|
||||
|
||||
### 3.3 store.market.json(行情快照缓存)
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"savedAt": 1788193491108,
|
||||
"quotes": {
|
||||
"600719.SH": {
|
||||
"time": 1788159604000,
|
||||
"timetag": "20260831 15:00:04",
|
||||
"lastPrice": 7.16,
|
||||
"open": 7.19,
|
||||
"high": 7.24,
|
||||
"low": 7.02,
|
||||
"lastClose": 7.22,
|
||||
"volume": 63523,
|
||||
"amount": 45341300
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**用途**:宿主重启后首屏快速展示上次价格(不依赖实盘订阅);实盘推送/轮询做增量更新并定期写回。
|
||||
|
||||
## 4. 数据流
|
||||
|
||||
```
|
||||
启动时:
|
||||
DataStore.load() → 读 store.json(份额分配)
|
||||
DataStore.loadMarket() → 读 store.market.json(行情缓存,首屏有价)
|
||||
MarketFeed._primePositions() → 主动拉持仓盘口 → 写缓存(服务端启动即预热最新价)
|
||||
|
||||
实盘中:
|
||||
MarketFeed WS 推送 / REST 定时刷新 → MarketDataHub.ingest() → 内存缓存
|
||||
MarketDataHub 防抖 3s → DataStore.setMarketQuotes() → store.market.json
|
||||
|
||||
查询时(market-snapshot):
|
||||
MarketDataHub.getByCodes() → 内存 → 磁盘 → REST 补拉(三级命中)
|
||||
```
|
||||
|
||||
## 5. 关键设计决策
|
||||
|
||||
| 决策 | 结论 | 理由 |
|
||||
|---|---|---|
|
||||
| 存储介质 | JSON 文件(不引入数据库) | 数据量很少,JSON + schema 足够 |
|
||||
| 策略定义位置 | DSH settings(不迁 store) | 设置页交互不变,查询简单 |
|
||||
| 份额分配结构 | 策略为中心(strategies[].dataset) | 贴合业务视角,schema 统一描述 |
|
||||
| dataset 格式 | [{code, shares}] 数组 | 可扩展字段(未来加成本价/备注) |
|
||||
| 行情缓存 | 持久化(store.market.json) | DataStore 为数据快速展现存在,重启不丢价 |
|
||||
| 写入 | 原子写(临时文件 + rename) | 防损坏,多次写安全 |
|
||||
| 测试隔离 | ODL_TEST_DATA_DIR 环境变量或 dataDir 参数 | 防误删真实数据(技术约束-011) |
|
||||
|
||||
## 6. 迁移
|
||||
|
||||
- 旧 allocations.json(code 为中心)→ 新 store.json(策略为中心):**启动时自动迁移**,旧文件备份为 allocations.json.bak
|
||||
- 迁移逻辑在 DataStore._migrateFromLegacy():code 为中心 → 策略为中心聚合
|
||||
|
||||
## 7. 对应实现
|
||||
|
||||
| 模块 | 职责 | 文件 |
|
||||
|---|---|---|
|
||||
| DataStore | 份额分配 + 行情持久化读写、迁移、schema | src/storage/DataStore.js |
|
||||
| PositionManager | 份额业务逻辑(依赖 DataStore) | src/position/PositionManager.js |
|
||||
| MarketDataHub | 行情缓存(内存 + 磁盘持久化 + 查询) | src/market/MarketDataHub.js |
|
||||
| MarketFeed | 行情获取(WS + REST 定时刷新 + 启动 prime) | src/market/MarketFeed.js |
|
||||
|
||||
## 8. 约束条目(引用)
|
||||
|
||||
- 技术约束-011:测试/回归脚本禁止在真实数据上执行写操作(独立数据目录)
|
||||
- 本文件为新领域设计约束,后续数据存储变更以本文件为最终依据
|
||||
|
||||
## 9. SQLite 存储设计(R-008 落实,2026-09-01 迭代 06 实施)
|
||||
|
||||
> **变更理由**(R-008 讨论确认):① 需要复杂查询/筛选(按 code/时间/策略,JSON 内存过滤不便);② 为未来功能铺路(交易记录/复盘/消息等统一存储)。
|
||||
> **变更范围**:仅存储引擎替换(JSON → SQLite),对外行为不变(数据快速展现目标不变)。本节替代第 3 节的 JSON schema 作为存储设计;第 3 节 JSON 结构保留为上一版本基线(迁移源)。
|
||||
> **2026-09-01 迭代 06 演进**:存储层从「数据集整体读写」升级为「持仓生命周期」(自增 holding_id + created_at/closed_at),为交易记录关联铺路。
|
||||
|
||||
### 9.1 存储介质
|
||||
|
||||
```
|
||||
~/.dsh/one-divine-lot/
|
||||
├── one-divine-lot.db # SQLite 数据库(strategy_holdings + market_quotes_cache 两表)
|
||||
├── store.json.bak # 迁移前备份(原 store.json)
|
||||
├── store.market.json.bak # 迁移前备份(原 store.market.json)
|
||||
└── allocations.json.bak # 迁移前备份(原 allocations.json,若存在)
|
||||
```
|
||||
|
||||
### 9.2 表结构
|
||||
|
||||
> 2026-09-01 讨论修正:策略持仓为**一对多的「多」侧单表**(strategy_holdings)+ **持仓生命周期**(自增 holding_id / created_at / closed_at);不建 strategies(JSON 列压扁)+ allocation(冗余)双表;「一」侧=settings 中的策略定义(D5,不迁 SQLite)。
|
||||
|
||||
```sql
|
||||
-- 策略持仓生命周期(一笔 = 一次「建仓→清仓」的完整持仓;历史保留,供交易记录关联)
|
||||
CREATE TABLE IF NOT EXISTS strategy_holdings (
|
||||
holding_id INTEGER PRIMARY KEY AUTOINCREMENT, -- 自增持仓编号(交易记录关联锚点;实测删除不复用)
|
||||
strategy_id TEXT NOT NULL, -- 对应 settings 策略的 id(如 grid-supermarket)
|
||||
code TEXT NOT NULL, -- 证券代码(含后缀)
|
||||
shares REAL NOT NULL, -- 当前份额(股数,>0)
|
||||
created_at INTEGER NOT NULL, -- 建仓时间(毫秒时间戳)
|
||||
closed_at INTEGER, -- 清仓时间(NULL=当前持仓;非 NULL=已清仓历史)
|
||||
PRIMARY KEY (holding_id)
|
||||
);
|
||||
-- 业务约束:同一策略同一股票只允许一笔「当前持仓」(closed_at IS NULL 唯一)
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_active_holding
|
||||
ON strategy_holdings (strategy_id, code) WHERE closed_at IS NULL;
|
||||
|
||||
-- 行情快照缓存(code 维度一行一码;只存 UI 消费的现价 + 昨收,无 JSON 列;cache 语义=重启首屏有价)
|
||||
CREATE TABLE IF NOT EXISTS market_quotes_cache (
|
||||
code TEXT PRIMARY KEY,
|
||||
last_price REAL NOT NULL, -- 最新价(UI 现价)
|
||||
last_close REAL NOT NULL, -- 昨收(UI 涨跌着色对比基准)
|
||||
updated_at INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
```
|
||||
|
||||
**数据示例**(strategy_holdings):
|
||||
|
||||
| holding_id | strategy_id | code | shares | created_at | closed_at |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | grid-supermarket | 601117.SH | 600 | 迁移时间戳 | NULL |
|
||||
| 2 | grid-supermarket | 300057.SZ | 1000 | 迁移时间戳 | NULL |
|
||||
| 8 | manual-t | 600719.SH | 1000 | 迁移时间戳 | NULL |
|
||||
| 9 | manual-t | 600719.SH | 0 | 迁移时间戳 | 1788xxx(清仓后) |
|
||||
|
||||
**语义对齐**(store.schema.json → SQLite):
|
||||
- holding_id 自增(SQLite AUTOINCREMENT 实测删除不复用),稳定唯一,作为未来交易记录(trades)的关联外键;
|
||||
- strategy_holdings.strategy_id = settings 策略的 id(英文 slug,稳定唯一标识;改名只改 name 不影响数据);
|
||||
- created_at=建仓时间;closed_at=清仓时间(NULL=当前持仓);部分唯一索引保证同策略同股票仅一笔当前持仓;
|
||||
- market_quotes_cache 对齐原 store.market.json 的 quotes 对象,但只抽取 lastPrice/lastClose 两个具体列入库(无 JSON 列);盘口五档等实时字段仅存内存缓存,不落盘(重启首屏只需「上次价格」)。
|
||||
|
||||
### 9.3 分层
|
||||
|
||||
- **策略定义**(id/name/visible/order):仍存 DSH settings(~/.dsh/settings.yaml),设置页交互不变(D5);
|
||||
- **持仓份额**(策略 → 股票 → 股数 + 生命周期):存 strategy_holdings 表(一对多「多」侧,holding_id 唯一);
|
||||
- **行情快照**(code → lastPrice/lastClose):存 market_quotes_cache 表(极简两列)。
|
||||
|
||||
### 9.4 数据流(不变)
|
||||
|
||||
```
|
||||
启动时:
|
||||
DataStore.load() → 读 SQLite strategy_holdings(迁移检测:旧 JSON 存在且库空 → 自动迁移)
|
||||
DataStore.loadMarket() → 读 SQLite market_quotes_cache(行情缓存,首屏有价)
|
||||
MarketFeed._primePositions() → 主动拉持仓盘口 → 写缓存(服务端启动即预热最新价)
|
||||
|
||||
实盘中:
|
||||
MarketFeed WS 推送 / REST 定时刷新 → MarketDataHub.ingest() → 内存缓存
|
||||
MarketDataHub 防抖 3s → SqliteStore.setMarketQuotes() → market_quotes_cache 表(只投影 last_price/last_close)
|
||||
|
||||
查询时(market-snapshot):
|
||||
MarketDataHub.getByCodes() → 内存 → SQLite → REST 补拉(三级命中)
|
||||
```
|
||||
|
||||
### 9.5 迁移
|
||||
|
||||
- 触发:启动检测 —— 旧 JSON 文件(store.json / store.market.json / allocations.json)存在 且 SQLite 空 → 自动迁移;
|
||||
- 动作:store.json → strategy_holdings(dataset 平铺为行,created_at=迁移时间戳,closed_at=NULL);store.market.json → market_quotes_cache(抽取 lastPrice/lastClose 两列);allocations.json → strategy_holdings(code 为中心聚合转换);迁移前 JSON 备份为 *.json.bak;
|
||||
- 幂等:SQLite 有数据即跳过;迁移失败不破坏原 JSON;
|
||||
- 一次性脚本:scripts/migrate-json-to-sqlite.mjs(与启动自动迁移共用逻辑)。
|
||||
|
||||
### 9.6 关键设计决策(更新)
|
||||
|
||||
| 决策 | 结论 | 理由 |
|
||||
|---|---|---|
|
||||
| 存储介质 | **SQLite(node:sqlite)** | 复杂查询 + 未来多数据集统一存储(R-008 D1) |
|
||||
| 驱动 | node:sqlite(Node ≥22.5 内置) | 零依赖分发,experimental 风险由 SqliteStore 封装隔离(D2) |
|
||||
| 持仓表 | **strategy_holdings**(持仓生命周期:holding_id 自增 + created_at/closed_at) | 关系模型正确、无 JSON 压扁、支持按 code 反查;为交易记录关联铺路(2026-09-01 讨论演进) |
|
||||
| 行情表 | market_quotes_cache(code → last_price/last_close 两列) | 只存 UI 消费的现价+昨收;无 JSON 列;盘口深度仅内存(2026-09-01 讨论修正) |
|
||||
| 不建表 | 不建 trades(R-007 时再建) | 本期最小改动(D4) |
|
||||
| 策略定义位置 | DSH settings(不迁 SQLite) | 设置页交互不变(D5) |
|
||||
| 存储层语义 | 单票生命周期(openHolding/addShares/reduceShares/closeHolding),废弃整策略重写(setDataset/removeDataset) | 份额=持仓生命周期,历史保留可关联交易记录(2026-09-01 讨论演进) |
|
||||
| 迁移 | 一次性脚本 + 启动自动迁移(幂等) | 参照 R-006 迁移模式(D6) |
|
||||
| JSON 去留 | 迁移后废弃(迁移前自动备份) | 单一数据源(D7) |
|
||||
| 能力范围 | 仅存储引擎替换,对外行为不变 | 最小改动(D3) |
|
||||
| 写入 | SQLite 事务(同步 API) | 原子性由数据库保证 |
|
||||
| 测试隔离 | ODL_TEST_DATA_DIR 环境变量或 dataDir 参数 | 防误删真实数据(技术约束-011) |
|
||||
|
||||
### 9.7 对应实现(更新)
|
||||
|
||||
| 模块 | 职责 | 文件 |
|
||||
|---|---|---|
|
||||
| SqliteStore | SQLite 存储层封装(node:sqlite:init/迁移/持仓生命周期/行情读写/close) | src/storage/SqliteStore.js |
|
||||
| DataStore | 持仓 + 行情持久化(委托 SqliteStore,对外 API 升级为生命周期语义) | src/storage/DataStore.js |
|
||||
| PositionManager | 份额业务(适配单票生命周期:openHolding/addShares/reduceShares/closeHolding) | src/position/PositionManager.js |
|
||||
| MarketDataHub | 行情缓存(内存 + SQLite 持久化 + 查询) | src/market/MarketDataHub.js |
|
||||
| MarketFeed | 行情获取(WS + REST 定时刷新 + 启动 prime) | src/market/MarketFeed.js |
|
||||
| migrate 脚本 | 一次性迁移脚本 | scripts/migrate-json-to-sqlite.mjs |
|
||||
|
||||
## 10. 交易记录存储设计(R-009 落实,2026-09-01 迭代 07 实施)
|
||||
|
||||
> 依据:R-009(Q1-Q8 定稿,2026-09-01)。在 SQLite 新增交易记录两表(trade_orders + trade_fills),QMT 当日交易数据本地持久化(跨日积累成历史库),支持按策略过滤 / 复盘交易。
|
||||
|
||||
### 10.1 表结构
|
||||
|
||||
```sql
|
||||
-- 交易委托(委托主行;order_id 唯一,UPSERT 幂等;零冗余 strategy_id/holding_id)
|
||||
CREATE TABLE IF NOT EXISTS trade_orders (
|
||||
order_id TEXT PRIMARY KEY, -- m_strOrderSysID
|
||||
trade_date TEXT NOT NULL, -- 交易日 YYYYMMDD(m_strInsertDate)
|
||||
code TEXT NOT NULL,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
exchange TEXT NOT NULL DEFAULT '',
|
||||
direction TEXT NOT NULL DEFAULT '', -- buy / sell
|
||||
direction_code INTEGER,
|
||||
opt_name TEXT NOT NULL DEFAULT '',
|
||||
status INTEGER,
|
||||
order_volume REAL NOT NULL DEFAULT 0,
|
||||
traded_volume REAL NOT NULL DEFAULT 0,
|
||||
limit_price REAL NOT NULL DEFAULT 0,
|
||||
traded_price REAL NOT NULL DEFAULT 0,
|
||||
amount REAL NOT NULL DEFAULT 0,
|
||||
insert_date TEXT NOT NULL DEFAULT '',
|
||||
insert_time TEXT NOT NULL DEFAULT '',
|
||||
insert_ts INTEGER NOT NULL, -- 派生:insert_date+insert_time 合成毫秒时间戳
|
||||
cancel_info TEXT NOT NULL DEFAULT '',
|
||||
error_msg TEXT NOT NULL DEFAULT '',
|
||||
strategy_id TEXT, -- 用户手动设置的策略归属(冗余,Q1 确认)
|
||||
holding_id INTEGER, -- 用户手动设置的持仓归属(冗余,Q1 确认)
|
||||
fetched_at INTEGER NOT NULL -- 同步时间戳
|
||||
);
|
||||
|
||||
-- 交易成交(trade_id 唯一,order_id 外键关联委托;零冗余 strategy_id/holding_id)
|
||||
CREATE TABLE IF NOT EXISTS trade_fills (
|
||||
trade_id TEXT PRIMARY KEY, -- m_strTradeID
|
||||
order_id TEXT NOT NULL, -- → trade_orders.order_id
|
||||
trade_date TEXT NOT NULL,
|
||||
code TEXT NOT NULL,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
exchange TEXT NOT NULL DEFAULT '',
|
||||
direction TEXT NOT NULL DEFAULT '',
|
||||
direction_code INTEGER,
|
||||
opt_name TEXT NOT NULL DEFAULT '',
|
||||
price REAL NOT NULL DEFAULT 0,
|
||||
volume REAL NOT NULL DEFAULT 0,
|
||||
amount REAL NOT NULL DEFAULT 0,
|
||||
commission_rate_wan REAL NOT NULL DEFAULT 0,
|
||||
trade_time TEXT NOT NULL DEFAULT '',
|
||||
fetched_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_trade_orders_date ON trade_orders (trade_date);
|
||||
CREATE INDEX IF NOT EXISTS idx_trade_orders_ts ON trade_orders (insert_ts);
|
||||
CREATE INDEX IF NOT EXISTS idx_trade_fills_order ON trade_fills (order_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_trade_fills_date ON trade_fills (trade_date);
|
||||
```
|
||||
|
||||
### 10.2 关键设计决策(R-009 Q1-Q8 定稿)
|
||||
|
||||
| 决策 | 结论 | 理由 |
|
||||
|---|---|---|
|
||||
| 表范围 | trade_orders + trade_fills 两表 | 委托 1:N 成交,忠实 R-007 单表合并模型;保留无成交委托(待报/已撤/废单);避免 JSON 压扁 |
|
||||
| 冗余 | **trade_orders 冗余 strategy_id + holding_id**(用户手动设置) | 老师二次定稿(Q3 修正):手动设置归属,便于过滤/展示/复盘;trade_fills 仍零冗余(跟随委托) |
|
||||
| 委托时间派生列 | insert_ts(insert_date+insert_time 合成毫秒) | join 持仓生命周期窗口用(created_at ≤ t < closed_at) |
|
||||
| 策略/持仓归属 | **用户手动设置**(交易记录 tab 下拉:候选 = 该 code 当前持仓策略 + 未关联);持久化到 trade_orders.strategy_id + holding_id;UPSERT 不覆盖归属列;可随时改以最终为准 | 一票多策略无法算法区分(300057.SZ 分属 grid/manual),人为确认符合「人机合一」(2026-09-01 老师二次定稿) |
|
||||
| 同步 | 服务端 TradeSync:启动预热 + 60s 定时 UPSERT(幂等);前端今日轮询写穿 | 不依赖前端开 tab 也持续积累 |
|
||||
| 代码归一化 | QMT 委托/成交 code 无后缀(001330),持仓带后缀(001330.SZ)——数据源映射层统一 `normalizeInstrumentCode`(补交易所后缀);委托交易日 = insertDate(无独立 tradeDate 字段),tradeDate 兜底 insertDate | 保证 FK 链 join 匹配 + 历史按时间段过滤正确(迭代 07 真实数据发现) |
|
||||
| 同步范围 | 只同步当日(QMT 无历史接口) | 数据逐日积累 = 本地历史库 |
|
||||
| 历史查询 | trades/history 端点(时间段/code/策略/方向过滤) | 复盘 + 按策略过滤 |
|
||||
| 数据清理 | 不做自动清理(复盘需要历史) | 导出/清理后续迭代 |
|
||||
|
||||
### 10.3 数据流
|
||||
|
||||
```
|
||||
启动时:TradeSync 启动预热一次(拉今日 orders+trades → UPSERT 落库,不覆盖归属列)
|
||||
实盘中:TradeSync 60s 定时同步(UPSERT 幂等,状态覆盖更新,归属列保留)
|
||||
前端今日轮询命中 orders/trades 端点 → 写穿本地(机会式)
|
||||
用户: 交易记录 tab 手动设置每笔委托归属(strategy_id + holding_id 落库,可随时改)
|
||||
查询时:今日 → QMT 实时(+ 本地归属);历史范围 → trades/history 查本地 SQLite(策略过滤 = 用户设置的归属)
|
||||
```
|
||||
|
||||
### 10.4 对应实现(更新)
|
||||
|
||||
| 模块 | 职责 | 文件 |
|
||||
|---|---|---|
|
||||
| SqliteStore | +trade_orders/trade_fills 建表 + 交易 UPSERT/历史查询 + 策略归属推导 | src/storage/SqliteStore.js |
|
||||
| DataStore | +交易记录方法(upsertTradeOrders/upsertTradeFills/queryTradeHistory) | src/storage/DataStore.js |
|
||||
| TradeSync | 服务端定时同步(启动预热 + 60s + UPSERT 幂等) | src/trades/TradeSync.js |
|
||||
| api/trades.js | +trades/history 端点 | src/api/trades.js |
|
||||
|
||||
## 11. 持仓内存快照(2026-09-02 优化)
|
||||
|
||||
> **变更理由**(老师拍板讨论):原实盘持仓在每次请求时穿透 QMT(`PositionManager.getAllPositions` 实时拉 `/trade/positions`),带来 ① QMT 抖动 → 策略/全部持仓/未分配三个页面当场空白;② 每次进 tab 都打一次 QMT HTTP。改为服务端内存快照为准,10s 定时同步。
|
||||
|
||||
### 11.1 决策
|
||||
|
||||
| 决策 | 结论 | 理由 |
|
||||
|---|---|---|
|
||||
| 存储介质 | **纯内存**(PositionSync 进程内快照),**不落库** | 持仓快照随时可用一次调用重拿全,不满足落库的任一正当条件(不可再生/重启首屏依赖);落库反而引入「过期快照冒充实时的说谎风险」;不复用 strategy_holdings(账本 ≠ 对账单,holding_id 是交易归属锚点,不可掺易变快照) |
|
||||
| 同步 | PositionSync:启动预热 + 10s 定时全量拉取 → 校验 → 整体替换内存 | 与 TradeSync 同构;闸门校验与页面显示同源同鲜度 |
|
||||
| 失败策略 | 同步失败**保留上次快照**,不清空不报错 | 读方继续消费旧快照,QMT 抖动不再白屏(强于旧的穿透行为) |
|
||||
| 空快照 | 双重确认(/health 可用 + getAsset 账户身份可识别)才接受为「真清仓」,否则视为异常保留旧快照 | 防一次异常响应清空缓存 |
|
||||
| 读路径 | `getAllPositions()` 读内存快照;快照为空 → 读穿透当场拉一次并回填;QMT 也挂 → 抛错(前端 LoadState 重试) | 首启兜底;未注入 positionSync 时保持旧行为(兼容) |
|
||||
| ~~幽灵持仓自动清仓~~(**退役,2026-09-08 R-018 数据域分界**) | ~~QMT 快照连续 3 轮消失 → 本地全部策略当前持仓 closeHolding 转历史~~ → **不再写 strategy_holdings**:同步机制只作用于对账单域(老师拍板:幽灵清仓本就是一个同步机制,不可以让幽灵把爪子伸太长);账本转历史唯一途径 = 卖出单关联减至 0(R-018);对账单码消失 → 快照无此行 + 漏关联软提示(positions/orphan-hints,只读) | R-014 选定时防本地账本残留;R-018 改由交易关联驱动账本生命周期,幽灵自动归档与「账本只由关联交易改」冲突 → 退役(迭代 16 实施,见 §12) |
|
||||
| 前端 | 零改动 | 三个接口数据源自动切换 |
|
||||
|
||||
### 11.2 数据流(持仓部分,替代原「读取时组装」描述)
|
||||
|
||||
```
|
||||
启动:PositionSync 预热一次 → 内存快照
|
||||
实盘:10s 定时全量同步(失败保留旧快照;空快照双重确认;幽灵清仓判定)
|
||||
读取:getAllPositions() → 内存快照 →(空)读穿透回填
|
||||
```
|
||||
|
||||
### 11.3 对应实现(追加)
|
||||
|
||||
| 模块 | 职责 | 文件 |
|
||||
|---|---|---|
|
||||
| PositionSync | 持仓内存快照同步(10s 定时 + 失败保留 + 空快照双重确认 + 幽灵清仓防抖) | src/position/PositionSync.js |
|
||||
| PositionManager | getAllPositions 改读快照 + 读穿透兜底(未注入 sync 时兼容旧穿透) | src/position/PositionManager.js |
|
||||
| 回归测试 | 纯内存 mock 34 项(同步/失败保留/空快照/幽灵防抖/账户守卫/读穿透/组装回归) | scripts/test-position-sync.mjs |
|
||||
|
||||
## 12. 归属分段存储设计(R-018 落实,2026-09-08 迭代 16 实施)
|
||||
|
||||
> **变更理由**(R-018 老师拍板):交易归属从「纯标签」(trade_orders 单组 strategy_id/holding_id,不改数量)升级为**账本写操作**——关联即自动调整 strategy_holdings 份额;支持一笔委托拆多段分配多策略;撤段做逆操作。归属真相源从 trade_orders 两列迁移到**分段表**。
|
||||
|
||||
### 12.1 strategy_holdings 状态扩展(作废第三态 + 清仓份额快照)
|
||||
|
||||
```sql
|
||||
-- 幂等 ALTER(沿用 _ensureXxxColumn 模式)
|
||||
ALTER TABLE strategy_holdings ADD COLUMN void_at INTEGER; -- 作废时间(NULL=有效;非 NULL=建仓被撤=从未成立;不进历史层)
|
||||
ALTER TABLE strategy_holdings ADD COLUMN closed_shares REAL; -- 清仓前份额快照(closeHolding 写入,供撤卖出段恢复活动用)
|
||||
-- 活动唯一索引收窄(R-018:活动 = closed_at IS NULL AND void_at IS NULL)
|
||||
DROP INDEX IF EXISTS idx_active_holding;
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_active_holding
|
||||
ON strategy_holdings (strategy_id, code) WHERE closed_at IS NULL AND void_at IS NULL;
|
||||
```
|
||||
|
||||
- 生命周期操作语义:closeHolding = shares 置 0 + closed_at + **closed_shares=清仓前份额**(shares 展示仍 0,R-016 Q2 语义不变);voidHolding(新增)= shares 置 0 + void_at(非清仓:建仓被撤 = 从未成立);
|
||||
- 作废行不进 R-016 历史「已清仓」层(getHoldingsHistory 过滤 void_at IS NULL),保留数据可追溯。
|
||||
|
||||
### 12.2 归属分段表 trade_order_attributions
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS trade_order_attributions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
order_id TEXT NOT NULL, -- → trade_orders.order_id
|
||||
strategy_id TEXT NOT NULL, -- 段归属策略
|
||||
holding_id INTEGER NOT NULL, -- 段锚点持仓(R-010 展开用)
|
||||
code TEXT NOT NULL, -- 冗余
|
||||
direction TEXT NOT NULL, -- buy/sell(冗余,撤段判向)
|
||||
volume REAL NOT NULL, -- 该段已成交量(>0;总额 ≤ order.traded_volume)
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_attr_order_strategy ON trade_order_attributions (order_id, strategy_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_attr_holding ON trade_order_attributions (holding_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_attr_strategy ON trade_order_attributions (strategy_id);
|
||||
```
|
||||
|
||||
- **真相源 = 段表**;trade_orders.strategy_id/holding_id 退役为冗余(保留列不删,防外部脚本 break;不再作为查询/过滤源);
|
||||
- 存量迁移:段表空且 trade_orders 有单组归属 → 迁为第一段(volume = 该单 traded_volume;幂等);
|
||||
- 查询适配:orders 附加归属、by-holding(R-010)、历史策略过滤(R-009)全部改经段表。
|
||||
|
||||
### 12.3 关键设计决策(R-018 迭代 16)
|
||||
|
||||
| 决策 | 结论 | 理由 |
|
||||
|---|---|---|
|
||||
| 归属真相源 | **分段表 trade_order_attributions**(order → 多段 strategyId/holdingId/volume) | 一笔委托可拆 N 段关联不同策略(R2/R4);单列两字段无法表达 |
|
||||
| 数据域分界 | PositionSync/幽灵清仓**只写对账单快照**,不写 strategy_holdings | R-018 老师拍板:同步机制不伸爪子到账本;账本只由交易关联 + 手动份额操作驱动 |
|
||||
| 关联驱动量 | 段 volume = 该段**已成交量** | Q4:撤单/废单不动账 |
|
||||
| 撤段逆操作 | 撤 buy 段 → 减回 / 归零作废(void);撤 sell 段 → 加回 / 恢复活动仓(closed_shares) | R3/R6/R7:作废 ≠ 清仓(不产生假清仓历史);恢复用 closed_shares 快照 |
|
||||
| 撤段约束 | 同 holding 内**逆序撤销**(乱序返回 segment-order-conflict);跨 holding 段独立可任意撤 | 账本正确性 > 操作便利(宁可拒绝不写错账) |
|
||||
| 事务 | setSegments 全量替换在**单事务**内(撤旧段 + 加新段) | 改归属原子性(R3 边界) |
|
||||
|
||||
### 12.4 对应实现(追加)
|
||||
|
||||
| 模块 | 职责 | 文件 |
|
||||
|---|---|---|
|
||||
| AttributionService | 归属服务:apply/revoke/setSegments(动作表判定 + 逆操作 + 事务) | src/trades/AttributionService.js(新增) |
|
||||
| SqliteStore | 增列/索引重建/voidHolding/close 快照/段表 CRUD/runInTransaction/查询改段表/存量迁移 | src/storage/SqliteStore.js |
|
||||
| api/trades.js | attribution-targets / attribution-set / attribution-segments;orders 附加段 | src/api/trades.js |
|
||||
| PositionSync | 删除幽灵清仓逻辑(只同步对账单快照) | src/position/PositionSync.js |
|
||||
| 回归测试 | 分界/动作表/撤段/迁移/软提示 | scripts/test-r018-attribution.mjs(新增) |
|
||||
|
||||
## 13. 约束条目(引用)
|
||||
|
||||
- 技术约束-012:数据存储遵循本文件(SQLite 存储设计,含交易记录表 §10、归属分段存储 §12);
|
||||
- 技术约束-011:测试/回归脚本禁止在真实数据上执行写操作(独立数据目录);
|
||||
- 技术约束-017(变更 1,2026-09-08):幽灵自动清仓退役,PositionSync 只同步对账单快照;
|
||||
- 本文件为新领域设计约束,后续数据存储变更以本文件为最终依据。
|
||||
@@ -0,0 +1,55 @@
|
||||
# 迭代 03:技术实现方案
|
||||
|
||||
> 依据:PLAN-003、技术约束-003/008/009、产品约束-005/006、UI约束-001/002
|
||||
|
||||
## 数据模型(settings namespace: one-divine-lot 新增字段 qmtConnections)
|
||||
|
||||
```
|
||||
qmtConnections: {
|
||||
list: [{ id, name, baseUrl, order }], // order 决定列表展示顺序
|
||||
activeId: string, // 当前激活配置 id('' = 无,展示层回退 defaultId/第一条)
|
||||
defaultId: string, // 默认配置 id('' = 无;启动时自动激活的目标)
|
||||
}
|
||||
```
|
||||
|
||||
- id:复用策略 slug 生成逻辑(拼音映射 + 去重),fallback 前缀 qmt-conn;
|
||||
- baseUrl:归一化(trim + 去尾斜杠 + 校验 http(s):// 前缀);
|
||||
- 生效连接解析顺序(getActiveConnection):activeId → defaultId → order 第一条 → null。
|
||||
|
||||
## 关键机制
|
||||
|
||||
1. **启动激活(Q1)**:apply 时 resolveStartupConnection(settings, config)——list 空 → cordis config.qmtBaseUrl 兜底(Q4 不迁移);否则 defaultId → activeId → 第一条;结果直接作为数据源构造参数并打日志。
|
||||
2. **热切换(Q2)**:QmtBridgeRestDataSource 增加 setBaseUrl(url)——baseUrl 为按请求读取的实例字段,改字段即对后续请求生效,无需重建实例、无需重启。
|
||||
3. **删除边界(Q6)**:removeQmtConnection 内处理——被删为激活 → activeId 指向 defaultId(若有效)否则第一条;被删为默认 → defaultId 转移到第一条;删除后为空 → activeId/defaultId 清空;**列表仅剩一条时拒绝删除**(返回约定错误)。
|
||||
4. **测试连接(Q3)**:服务端代理 GET {baseUrl}/health(15s 超时),返回 { ok, latencyMs, status, error };支持传未保存的 baseUrl(表单内先测后存)。
|
||||
5. **头部挂载(Q9)**:client 插件向 slot conversation.session.header.actions 注册 id=odl-qmt-switch、order=-9(内置 PTC 标签 order=-10,并排其后);控件用插件自身 connection RPC 读写配置,不依赖 slot 注入。
|
||||
|
||||
## 服务端 API(POST /odl/api/<method>,webServer 自开路由 · 技术约束-004)
|
||||
|
||||
| 端点 | 入参 | 行为 |
|
||||
|---|---|---|
|
||||
| qmt-connections | - | 返回 { list, activeId, defaultId, active }(active 为生效连接对象) |
|
||||
| qmt-connections/add | { name, baseUrl } | 新增;返回新配置 |
|
||||
| qmt-connections/update | { id, name, baseUrl } | 编辑;若改的是激活配置且 baseUrl 变化 → 编排 setBaseUrl |
|
||||
| qmt-connections/remove | { id } | 删除(含边界处理);生效连接变化 → 编排 setBaseUrl;仅剩一条时报错 |
|
||||
| qmt-connections/activate | { id } | 激活(写 activeId)+ setBaseUrl |
|
||||
| qmt-connections/set-default | { id } | 设默认(写 defaultId) |
|
||||
| qmt-connections/test | { baseUrl } 或 { id } | 代理 /health,返回可达性与延迟 |
|
||||
|
||||
## 客户端
|
||||
|
||||
- **QmtConnectionChip.jsx(新增)**:chip `QMT: <激活配置名> ▾` + 自实现下拉菜单(绝对定位 + 外点关闭;激活项勾选);点选 → activate 端点 → toast 反馈 + 本地状态刷新。
|
||||
- **SettingsSection.jsx(改)**:第三个子 tab「QMT 连接配置」;配置列表(名称/地址/激活与默认徽标)+ 行内操作(激活 / 设为默认 / 编辑 / 测试 / 删除)+ 新增与编辑表单 + 删除确认弹窗(复用 ConfirmDialog)+ Toast 反馈。
|
||||
- **client/index.js(改)**:注册头部 slot;其余 tab 注册不动。
|
||||
|
||||
## 安装与构建
|
||||
|
||||
服务端直接改 src;客户端 bundle 走既有 tsdown + wrap 构建(技术约束-005/006);安装用 dsh plugin add(技术约束-007)。cordis.patch.yml 不改动(Q4)。
|
||||
|
||||
## 实施事实(2026-08-29)
|
||||
|
||||
- **改动文件**:src/settings.js(schema + 9 个 QMT 函数段)、src/data-source/qmt-bridge-rest.js(setBaseUrl)、src/index.js(启动解析 + dataSource 注入 api)、src/api.js(7 个 qmt-connections 端点 + testQmtConnection 代理 + activate/update/remove 热切换编排)、src/client/index.js(头部 slot 注册 odl-qmt-switch order=-9)、src/client/views/QmtConnectionChip.jsx(新增)、src/client/views/SettingsSection.jsx(第三个子 tab QmtConnectionsSettings,追加于文件尾)。cordis.patch.yml 未改动。
|
||||
- **构建**:pnpm run build 一次通过(服务端 ESM 14 文件 + 客户端 CJS bundle 69.60kB + wrap 65907 bytes);node --check 服务端全通过;bundle 内已核对 chip 与端点调用代码。
|
||||
- **安装方式更正**:profile(~/.dsh/profiles/web)中插件为 link 符号链接指向工作区,工作区 lib/ 即宿主加载产物,构建后无需再执行 dsh plugin add(技术约束-007 补充事实:link 安装形态下构建即生效)。
|
||||
- **数据层单测**:14 项断言全部通过(mock settings scope):新增归一化去尾斜杠、order 排序、新增不自动激活、空激活态回退第一条、非法地址拒绝、激活/默认写入、生效连接解析、编辑激活配置地址、删除标记返回、删激活/默认→转移第一条并激活、禁删最后一条、空列表回退 cordis、启动激活默认(覆盖上次激活)、启动解析回写 activeId。
|
||||
- **生效条件**:线上宿主进程(GUI http://127.0.0.1:3080)启动时已加载旧 lib(qmt-connections 端点 404 证实),需重启 DSH Web 或重载插件后新代码生效。
|
||||
@@ -0,0 +1,20 @@
|
||||
# 迭代 03 复盘(2026-08-29)
|
||||
|
||||
## 结果
|
||||
|
||||
R-004 全量实现并验收通过(7/7),需求归档至 docs/05-需求池/已完成/R-004.md。
|
||||
|
||||
## 过程事实
|
||||
|
||||
1. 编码按 PLAN-003 七步执行,构建一次通过;数据层 14 项单测全部通过;
|
||||
2. 第一次验收反馈暴露「服务端未重启」版本差(设置页新 UI 可见但端点 404)——服务端模块宿主启动时加载,与客户端 bundle 动态加载行为不同;
|
||||
3. 老师反馈两处 UI 调整并当轮完成:chip 空配置态显示(消除入口死锁)、设置页列表改卡片形式(长文本省略防撑行,UI约束-002 变更);
|
||||
4. 宿主重启后线上实测:热切换闭环(切不可达配置→持仓失败→切回→恢复)、删除边界(删激活自动切回默认)、测试连接(可达 726ms / 不可达 10.1s)全部通过;
|
||||
5. 过程操作失误一次(猜错临时配置 id 导致 404),用真实 id 重跑通过——API 返回的 id 应先查再用;
|
||||
6. 拼音表外中文名 slug 兜底通用名(qmt-conn),不阻塞验收,留作后续增强。
|
||||
|
||||
## 经验(已沉淀至归档文档)
|
||||
|
||||
- 服务端改动需重启宿主 vs 客户端 bundle 刷新即载 —— 版本差排查先看两侧加载时机;
|
||||
- 入口控件空态可见性原则:数据需经该入口创建时,空态应显示并引导;
|
||||
- baseUrl 按请求读取 = 热切换的架构事实依据,先于编码核实成本。
|
||||
@@ -0,0 +1,28 @@
|
||||
# 迭代 03:QMT 连接配置(多配置管理 + 会话头部快捷切换)
|
||||
|
||||
> 创建:2026-08-29 | 依据需求:R-004(已定稿 2026-08-29)| 依据计划:PLAN-003(计划-QMT连接配置.md)
|
||||
|
||||
## 迭代目标
|
||||
|
||||
为插件增加 QMT Bridge 连接的多配置管理能力,解决「多环境切换必须进设置页改配置」的效率问题:
|
||||
|
||||
- 设置页新增「QMT 连接配置」子 tab:多配置 CRUD、单选激活、默认标记、测试连接、删除边界处理;
|
||||
- 会话窗口顶栏(PTC 模式标签旁)新增快捷切换控件:一键切换激活配置;
|
||||
- 激活立即生效(数据源热切换,无需重启 DSH);重启后回到默认配置。
|
||||
|
||||
## 目标描述
|
||||
|
||||
插件当前只能通过 cordis.patch.yml 注入单一 qmtBaseUrl,切换环境(本机 QMT / 局域网其他机器)需改宿主配置并重启。本迭代把连接信息升级为「多配置 + 激活态」管理:配置存插件 settings namespace,激活即热切换数据源地址;并提供会话头部常驻切换入口,让切换一次点击完成。
|
||||
|
||||
## 目标的讨论过程
|
||||
|
||||
R-004 三轮讨论(2026-08-29,详见 docs/05-需求池/R-004.md 讨论记录):
|
||||
1. **第一轮**:AI 代码审查(数据源按请求读取地址、/health 可复用、策略配置先例)+ 老师逐条拍板 Q1-Q8(含新增的存储位置问题);
|
||||
2. **第二轮**:老师提出会话头部快捷切换需求;AI 调研 DSH 宿主确认 PTC 标签所在 slot(conversation.session.header.actions)可并排挂载;老师拍板 Q9(入口=会话头部)/Q10(菜单仅切换);
|
||||
3. **第三轮**:老师确认定稿(三要素满足)。
|
||||
|
||||
## 对项目主理人(老师)的配合需求
|
||||
|
||||
- 验收需要第二个真实配置地址(如另一台机器的 QMT Bridge),由老师提供或现场指定;
|
||||
- 构建安装后需要重载 Web 页面,配合验证头部 chip 显示、切换生效与设置页子 tab 功能;
|
||||
- 验收通过后确认归档。
|
||||
@@ -0,0 +1,59 @@
|
||||
# 迭代 03:验收标准
|
||||
|
||||
> 依据:PLAN-003 验收标准;产品约束-005/006、UI约束-001/002
|
||||
|
||||
## 验收标准线(全部满足才判定达成)
|
||||
|
||||
1. 设置页出现「QMT 连接配置」子 tab(与通用设置/策略分组并列),配置 CRUD 完整可用(名称 + HTTP 地址);
|
||||
2. 激活某配置后数据源**立即**切到该地址(无需重启 DSH)——切换后用「测试连接」/持仓加载验证目标端点随激活配置变化;
|
||||
3. 默认配置行为正确:启动自动激活默认;重启后回到默认(即使运行中切过其他配置);列表为空时按 cordis qmtBaseUrl 兜底可用;
|
||||
4. 「测试连接」返回可达性 + 延迟;激活不强制先测试;
|
||||
5. 删除边界符合产品约束-005:删激活→自动切默认;删默认→默认转移列表第一条并激活;删最后一条被禁止(有提示);
|
||||
6. 会话头部 PTC 标签旁出现 `QMT: <激活配置名>` chip;点开菜单列出全部配置(激活项勾选);点选即激活并 toast 反馈;头部仅切换(无管理入口);
|
||||
7. 现有功能(分仓/策略/设置现有子 tab)不回归。
|
||||
|
||||
## 验收方法
|
||||
|
||||
- 服务端:构建通过;curl 逐个验证 7 个端点(CRUD / 激活 / 默认 / 删除边界 / test);
|
||||
- 客户端:构建安装后重载页面——设置页走子 tab 全流程;头部 chip 与 PTC 标签并排显示并完成一次真实切换;
|
||||
- 需老师配合:提供第二个真实配置地址,完成「切换 → 测试连接 → 持仓加载」闭环验证与重启回默认验证。
|
||||
|
||||
## 验收目标
|
||||
|
||||
判定本迭代是否达成 R-004 定稿范围(产品约束-005/006 全量落地);通过后 R-004 移入已完成并归档。
|
||||
|
||||
## 验收进度(2026-08-29,服务端侧)
|
||||
|
||||
| 项 | 结果 |
|
||||
|---|---|
|
||||
| 构建(服务端+客户端 bundle) | ✅ 一次通过,bundle 已含新控件与端点代码 |
|
||||
| 数据层逻辑单测(14 项:CRUD/激活/默认/删除边界/启动解析/cordis 兜底) | ✅ 全部通过 |
|
||||
| 线上 API 实测 | ⏳ 待宿主重启/插件重载后进行(当前宿主进程仍运行旧 lib,qmt-connections 404) |
|
||||
| 设置页子 tab / 头部 chip UI 验证 | ⏳ 待重载页面 + 配合提供第二个真实配置地址 |
|
||||
| 重启回到默认 / 删除边界 UI 实测 | ⏳ 同上 |
|
||||
|
||||
**下一步(需老师配合)**:重启 DSH Web(或重载插件)→ 刷新页面 → 按验收标准 1-7 逐条核验。
|
||||
|
||||
### 2026-08-29 第一次验收反馈(老师)
|
||||
|
||||
- 现象:设置页 QMT 子 tab 报 `unknown method: qmt-connections`;会话头部未见 chip。
|
||||
- 定位:① 页面刷新只重载了客户端 bundle(新设置子 tab 已出现),服务端插件代码为宿主进程启动时加载,宿主未重启仍在运行旧 lib —— 端点 404 属预期过渡状态,需重启 DSH Web;② chip 原实现「无配置时隐藏」与 Q4 不迁移组合产生空态不可见问题,已修正为**空配置态也显示 chip**(菜单提示去设置添加,仅服务端未就绪时隐藏),重新构建通过(66127 bytes)。
|
||||
- 待办:老师重启 DSH Web 后按验收标准 1-7 核验。
|
||||
|
||||
## 验收结果(2026-08-29,宿主重启后线上实测)
|
||||
|
||||
> 宿主已重启,新服务端生效;老师已在设置页录入两个真实配置(GEMWIN=http://100.110.38.78:8610 / QMT_CYY=http://192.168.3.43:8610),激活与默认均为 GEMWIN。
|
||||
|
||||
| 验收标准 | 结果 | 证据 |
|
||||
|---|---|---|
|
||||
| 1. 设置页「QMT 连接配置」子 tab + 配置 CRUD | ✅ | 老师经 UI 实录 2 条配置(GEMWIN/QMT_CYY),列表/激活/默认状态正确返回 |
|
||||
| 2. 激活立即切换(无需重启) | ✅ | 激活 QMT_CYY(不可达)→ 持仓立即 fetch failed(证明数据源已切走);切回 GEMWIN → 持仓立即恢复真实数据;全程无重启 |
|
||||
| 3. 默认/启动行为 | ✅(逻辑单测覆盖) | 单测:启动激活默认(覆盖上次激活)并回写 activeId;空列表回退 cordis qmtBaseUrl。线上最终激活=GEMWIN=默认,状态一致;下次重启可复核启动日志 |
|
||||
| 4. 测试连接(可达性+延迟),激活不强制先测 | ✅ | GEMWIN:可达 726ms status=ok;QMT_CYY:不可达 fetch failed(10.1s);激活不可达配置无预检直接成功(符合 Q3) |
|
||||
| 5. 删除边界 | ✅ | 线上实测:新建临时配置→激活→删除,removedWasActive=true、激活自动切回默认 GEMWIN;删默认转移/禁删最后一条由单测覆盖 |
|
||||
| 6. 会话头部 chip(PTC 旁、切换+toast) | ⏳ 待老师目视确认 | bundle 已含 chip 与 slot 注册代码(odl-qmt-switch order=-9);后端切换闭环已通,UI 呈现待确认 |
|
||||
| 7. 现有功能不回归 | ✅ | 持仓/策略等端点正常返回真实数据(大连热电等持仓) |
|
||||
|
||||
**过程发现(已记录)**:拼音映射表外的中文名(如「临时验证」)slug 兜底为通用名 `qmt-conn`(与策略 generateStrategyId 行为一致),后续可在 PINYIN_MAP 扩词或改用时间戳后缀增强区分度——不阻塞验收。
|
||||
|
||||
**遗留待确认**:验收标准 6(头部 chip UI 呈现),由老师刷新页面目视确认。
|
||||
@@ -0,0 +1,60 @@
|
||||
# 技术实现方案:04-WS盘中价格实时更新
|
||||
|
||||
> 依据:PLAN-004 | 需求:R-005 | 设计约束:技术约束-001/003/008/010、产品约束-007
|
||||
|
||||
## 技术选型
|
||||
|
||||
- **WebSocket 客户端**:浏览器原生 WebSocket(前端直连,无需 ws 库);
|
||||
- **连接地址**:`ws://<激活配置host>:8610/ws`,host 来自激活 QMT 连接配置(R-004 多配置机制,复用热切换);
|
||||
- **订阅协议**:`POST /data/subscribe` body `{ type: 'whole', codes: ['SH','SZ'] }` → `{ ok, sub_id }`;
|
||||
- **推送消息**:`{ type: 'whole', data: { '<code>': TickSnapshot } }`,TickSnapshot.lastPrice = 现价;
|
||||
- **快照补初值**:连接后先 `GET /data/tick?codes=<关注codes>` 拉一次快照(WS 只推增量,无 prime);
|
||||
- **退订**:`POST /data/unsubscribe` body `{ sub_id }`(断开时清理)。
|
||||
|
||||
## 架构设计
|
||||
|
||||
### 前端行情数据流(事件驱动)
|
||||
|
||||
```
|
||||
QMT Bridge WS (ws://<host>:8610/ws)
|
||||
│ 推送 { type:'whole', data: { code: { lastPrice, ... } } }
|
||||
▼
|
||||
MarketDataProvider (React Context + useReducer)
|
||||
│ 维护 prices: Map<code, { lastPrice, lastClose, stime }>
|
||||
│ 事件:onPrice(codes) → 通知订阅方
|
||||
▼
|
||||
3 个监控表格(AllPositionsTab / StrategyTab)
|
||||
│ 行渲染时读 prices.get(code) 叠加现价
|
||||
▼
|
||||
PriceCell(格式化 + 红涨绿跌 + 变化高亮)
|
||||
```
|
||||
|
||||
### 关注列表(自动汇总)
|
||||
|
||||
- 3 个表格各自渲染时,把出现的 code 上报给 MarketDataProvider(`registerCodes(codes)`);
|
||||
- Provider 合并为关注集合,作为快照拉取与推送过滤的依据;
|
||||
- 简化:全量推送本来就带所有 code,前端只从 prices Map 取自己行需要的 code(无需显式过滤,天然按需)。
|
||||
|
||||
### 生命周期
|
||||
|
||||
- 挂载:获取激活配置 host → 拉快照 → 建 WS → 订阅 whole → 收推送更新 Map;
|
||||
- 配置切换:监听激活配置变化(host 变化)→ 断开旧 WS → 退订 → 重建连接;
|
||||
- 断开:指数退避静默重连 + 状态提示(连接中/已连接/重连中/断开)。
|
||||
|
||||
## 涉及设计约束
|
||||
|
||||
| 约束 | 内容 |
|
||||
|---|---|
|
||||
| 技术约束-001 | 统一数据源抽象:行情实时数据仍走 QMT Bridge 适配器口径(现价字段 lastPrice 语义一致) |
|
||||
| 技术约束-003 | 直连 QMT Bridge(前端直连其原生 WS,不经过 MCP) |
|
||||
| 技术约束-008 | 复用激活 QMT 连接配置的 host(多配置热切换联动 WS) |
|
||||
| 技术约束-010(新增) | 前端直连 QMT Bridge WebSocket 订阅行情;服务端不中转 WS |
|
||||
| 产品约束-007(新增) | 3 个监控表格展示盘中现价列:红涨绿跌着色 + 变化高亮;仅现价一项 |
|
||||
|
||||
## 实现步骤
|
||||
|
||||
1. 服务端 API:新增 `qmt-connections/active-host`(返回 { host, baseUrl, wsUrl, name });
|
||||
2. MarketDataProvider.jsx:WS 客户端(连接/订阅/价格 Map/快照/重连/配置切换)+ Context;
|
||||
3. PriceCell.jsx:现价单元格(格式化 + 涨跌色 + 高亮动画);
|
||||
4. 3 表格接入:AllPositionsTab / StrategyTab 加「现价」列;
|
||||
5. 构建安装 + 验收。
|
||||
@@ -0,0 +1,42 @@
|
||||
# 迭代 04 复盘:WS 盘中价格实时更新(2026-09-01)
|
||||
|
||||
> 复盘日期:2026-09-01 | 迭代状态:**已完成(验收通过)**
|
||||
> 关联需求:R-005(WS 盘中价格实时更新,已定稿)、R-006(策略数据 JSON data store schema,已定稿)
|
||||
> 关联计划:PLAN-004(WS 盘中价格实时更新)、PLAN-005(策略数据 JSON 标准化)
|
||||
|
||||
## 结果
|
||||
|
||||
迭代 04 达成:3 个监控表格(全部持仓 / 手动做T / 网格超市)实现盘中现价实时显示(服务端缓存 + 前端轮询),数据层标准化为 JSON data store(份额 + 行情持久化),并完成多项架构治理(api 拆分、component 目录、健康检查、膨胀修复)。
|
||||
|
||||
## 过程事实
|
||||
|
||||
1. **R-005 WS 实时价格**:经历多次架构演进——
|
||||
- 初版前端直连 WS(老师拍板)→ 实测发现 QMT Bridge REST 无 CORS 头、前端直连订阅被跨域拦截;
|
||||
- 改服务端代理订阅/快照 → 前端轮询;再演进为「服务端中转 + 行情缓存」(老师定);
|
||||
- 行情持久化(store.market.json)解决首屏无价;MarketFeed 启动 prime 主动拉持仓盘口;
|
||||
2. **R-006 JSON data store**:策略数据集标准化(store.schema.json / store.json / store.market.json),旧 allocations.json 自动迁移(保留 .bak);
|
||||
3. **架构治理**:
|
||||
- src/api.js(377 行)按领域拆分 → src/api/ 目录(positions / strategies / qmt-connections / market / common);
|
||||
- 创建 src/component/ 目录,market-cache 拆分为 MarketDataHub(缓存)+ MarketFeed(获取),业务模块陆续迁入;
|
||||
- 移除冗余端点(subscribe-ws/unsubscribe-ws/tick-snapshot)、前端去重(表格上报 code 替代 Provider 自拉);
|
||||
4. **QMT 健康检查**:服务端定时探测(5 分钟)+ 缓存,前端 QMT chip 右侧圆点(绿/红),点击触发即时探测;
|
||||
5. **数据误删事故**(严重教训):回归测试脚本误在真实数据执行写操作,导致大连热电/万顺新材份额丢失——已恢复 + 建立测试隔离机制(ODL_TEST_DATA_DIR)并沉淀技术约束-011;
|
||||
6. **行情缓存膨胀 bug**(38MB):WS 全市场推送无过滤 + setMarketQuotes 合并残留——三层修复(watchCodes 过滤 / warmup 不认可全市场 / 整体替换),最终 store.market.json 13 条 11KB;
|
||||
7. **WS 推送稳定性**:实测 QMT Bridge WS 推送是会话级/有状态(连接后 wsPushCount=0,纯 REST 轮询供数)——归属 QMT Bridge 工作空间;
|
||||
8. **全市场缓存**:实测 whole 推送 26,751 条中标准 A股仅 2,775 只(其余为期权/衍生品代码段)——方案搁置,保持 watchCodes。
|
||||
|
||||
## 经验(已沉淀)
|
||||
|
||||
- **服务端重启 vs 客户端刷新**:服务端模块宿主启动时加载(改动需重启),客户端 bundle 刷新即载——版本差排查先看两侧加载时机(迭代 03 经验复用);
|
||||
- **前端对行情状态应无感**:行情刷新/获取是服务端职责,前端只拿结果——移除状态条,避免误报;
|
||||
- **测试隔离**:回归/测试脚本禁止在真实数据上执行写操作(技术约束-011,数据误删事故沉淀);
|
||||
- **缓存膨胀三要素**:全量数据源(WS 全市场)必须过滤(watchCodes)、加载不认可历史全量(warmup)、写盘必须整体替换(setMarketQuotes);
|
||||
- **WS 推送不可靠**:QMT Bridge WS 为会话级行为(连接后可能 0 推送),实时数据需 REST 轮询兜底;
|
||||
- **代码段过滤**:QMT whole 推送含大量非 A股品种(89%),全市场类操作需按标准股票代码过滤。
|
||||
|
||||
## 遗留/后续
|
||||
|
||||
1. **WS 推送稳定性**:归属 QMT Bridge 工作空间(老师另开筛选解决);
|
||||
2. **全市场缓存**:搁置(需先解决 WS 稳定 + 只缓存标准 A股);
|
||||
3. **临时诊断端点 market-stats**:可保留观测或后续移除;
|
||||
4. **数据存储设计文档**:docs/03-设计约束/数据存储设计.md(已沉淀,后续存储变更以此为据)。
|
||||
@@ -0,0 +1,29 @@
|
||||
# 迭代目标:04-WS盘中价格实时更新
|
||||
|
||||
## 目标
|
||||
|
||||
在 3 个监控表格(全部持仓 / 手动做T / 网格超市)中实现**盘中价格实时更新**:前端直连 QMT Bridge WebSocket 订阅全市场 tick,事件驱动更新关注股票的现价(lastPrice)。
|
||||
|
||||
## 目标描述
|
||||
|
||||
- 范围:R-005(已定稿 2026-08-31);不新增其他功能域;
|
||||
- 要解决的问题:交易时段表格价格**不会自动更新**(R-003 复盘遗留 O4 问题),需实时反映盘中价格;
|
||||
- 引用需求:**R-005(已定稿,2026-08-31)**;
|
||||
- 实现方式:前端直连 QMT Bridge WS(老师确认拓扑),订阅 whole 全市场,前端按关注列表过滤,现价叠加层更新。
|
||||
|
||||
## 目标讨论过程
|
||||
|
||||
- 2026-08-28 迭代 01 复盘提出 T-005(WS 数据监控)方向,放草稿;
|
||||
- 2026-08-31 老师提出本轮需求:前端直连 ws 全量订阅 + 事件驱动更新 3 个表格的盘中价格(仅现价一项);
|
||||
- 2026-08-31 讨论确认 4 项决策:
|
||||
1. 连接拓扑:**前端直连 QMT Bridge WS**(非插件服务端中转);
|
||||
2. 订阅范围:**订阅全市场行情流**(type=whole,codes=SH/SZ),前端按关注列表过滤;
|
||||
3. 关注列表来源:**3 表格自动汇总**;
|
||||
4. 现价展示:**现价列 + 红涨绿跌 + 变化高亮**,刷新机制为**价格叠加层**(不整表重拉);
|
||||
- 2026-08-31 协议实测(GEMWIN 100.110.38.78:8610):订阅/退订/推送/快照格式全部确认。
|
||||
|
||||
## 对老师的配合需求
|
||||
|
||||
- 验收需要 GEMWIN(Tailscale 100.110.38.78:8610)可达(已确认可达);
|
||||
- 构建安装后重载页面配合验证;
|
||||
- 验收通过后确认归档。
|
||||
@@ -0,0 +1,482 @@
|
||||
# 迭代说明:04-WS盘中价格实时更新
|
||||
|
||||
> 本文件为本次迭代的**修改记录**(追加式,只记事实):每完成一处修改,追加一条记录(改了什么 / 为什么 / 涉及文件)。
|
||||
> 创建:2026-08-31
|
||||
|
||||
## 修改记录
|
||||
|
||||
### 2026-08-31 · 建立迭代 04 文档骨架
|
||||
|
||||
- **做了什么**:需求 T-005 草稿转正为 R-005(docs/05-需求池/R-005.md);创建计划 PLAN-004(docs/02-计划/计划-WS盘中价格实时更新.md);创建迭代 04 子目录(docs/04-迭代记录/04-WS盘中价格实时更新/)下的迭代目标 / 技术实现方案 / 验收标准 三件套 + 本说明。
|
||||
- **为什么**:本轮迭代按 divine-lot-dev 流程先落文档(需求定稿 → 计划 → 迭代范围),后续修改全部可回溯。
|
||||
- **涉及文件**:docs/05-需求池/R-005.md、docs/02-计划/计划-WS盘中价格实时更新.md、docs/04-迭代记录/04-WS盘中价格实时更新/(4 个 md)。
|
||||
|
||||
### 2026-08-31 · 服务端:新增 active-host / unsubscribe-ws 端点
|
||||
|
||||
- **做了什么**:src/api.js 新增两个端点:
|
||||
- `qmt-connections/active-host`:返回激活 QMT 连接配置的 `{ name, baseUrl, host, wsUrl, activeId }`(host 含端口,wsUrl = ws://host/ws),供前端据此直连 QMT Bridge WebSocket;连接列表为空时回退数据源当前 baseUrl(cordis 兜底);
|
||||
- `qmt-connections/unsubscribe-ws`:服务端代理 `POST {baseUrl}/data/unsubscribe`(浏览器侧 WS 直连可行,REST 退订统一走服务端避免跨域)。
|
||||
- **为什么**:前端需要激活配置的 host 才能建 WS(技术约束-010);退订走服务端代理更干净。
|
||||
- **涉及文件**:src/api.js。
|
||||
|
||||
### 2026-08-31 · 客户端:MarketDataProvider(WS 行情客户端)
|
||||
|
||||
- **做了什么**:新增 src/client/market/MarketDataProvider.jsx:
|
||||
- 直连 `ws://<激活配置host>:8610/ws`(浏览器原生 WebSocket);
|
||||
- 连接前 REST 订阅 `POST /data/subscribe { type:'whole', codes:['SH','SZ'] }` 拿 sub_id;
|
||||
- 维护 code → { lastPrice, lastClose, stime, open, high, low } 价格 Map(useReducer);
|
||||
- 事件驱动:WS 推送 `{ type:'whole', data: { code: snap } }` → dispatch update → 所有订阅表格重渲染现价;
|
||||
- 连接后拉一次 `GET /data/tick?codes=SH,SZ` 快照补初值(WS 只推增量,无 prime);
|
||||
- 断线指数退避重连(1s→2s→4s…上限 15s)+ 连接状态(connecting/live/reconnecting/disconnected);
|
||||
- 每 5s 轮询 active-host,激活配置 host 变化时自动断开旧 WS 并重建(复用 R-004 多配置热切换)。
|
||||
- **为什么**:实现「前端直连 + 全量订阅 + 事件驱动」的核心(R-005 定稿)。
|
||||
- **涉及文件**:src/client/market/MarketDataProvider.jsx(新增)。
|
||||
|
||||
### 2026-08-31 · 客户端:PriceCell 现价单元格 + 行情状态条
|
||||
|
||||
- **做了什么**:新增 src/client/views/PriceCell.jsx:
|
||||
- `PriceCell`:显示 lastPrice(≥100 用 2 位小数,否则 3 位),红涨绿跌(对比 lastClose,A股习惯:涨红 #d32f2f / 跌绿 #2e7d32 / 平灰);价格变化时 1.2s 金色高亮闪动(CSS keyframes);
|
||||
- `MarketStatusBar`:表格顶部行情连接状态提示(连接中/实时/重连中/未连接,圆点颜色区分)。
|
||||
- **为什么**:产品约束-007(现价列 + 涨跌色 + 变化高亮)。
|
||||
- **涉及文件**:src/client/views/PriceCell.jsx(新增)。
|
||||
|
||||
### 2026-08-31 · 客户端:3 个监控表格接入现价列
|
||||
|
||||
- **做了什么**:
|
||||
- src/client/index.js:withConnection 内包一层 MarketDataProvider,3 个表格共享同一行情实例;
|
||||
- src/client/views/AllPositionsTab.jsx:表头/表体新增「现价」列(名称后),标题下加 MarketStatusBar;
|
||||
- src/client/views/StrategyTab.jsx:表头/表体新增「现价」列(名称后),顶部加 MarketStatusBar(手动做T / 网格超市共用)。
|
||||
- **为什么**:3 表格展示关注股票盘中现价(R-005 目标)。
|
||||
- **涉及文件**:src/client/index.js、src/client/views/AllPositionsTab.jsx、src/client/views/StrategyTab.jsx。
|
||||
|
||||
### 2026-08-31 · 构建验证
|
||||
|
||||
- **做了什么**:`pnpm typecheck`(0 错误)+ `pnpm build` 通过(客户端 bundle 82.54kB / 17.96kB gzip,服务端 14 文件,wrap 完成)。
|
||||
- **端到端链路验证**:Node 模拟前端流程(订阅 whole → WS 连接 → 26 批推送 → 关注 code 过滤 → 提取现价)成功:603028.SH → lastPrice=7.18(lastClose=7.1,涨,UI 红色)。
|
||||
- **为什么**:构建与链路验证通过,可进入宿主安装验收。
|
||||
- **涉及文件**:lib/(构建产物)。
|
||||
|
||||
|
||||
### 2026-08-31 · 问题分析记录:订阅走服务端代理,WS 推送问题归属另一工作空间
|
||||
|
||||
**现象**(老师观察):全部持仓页「行情实时 IP」短暂出现后变「行情重连中」;持仓列表现价始终无数据。
|
||||
|
||||
**分析过程(基于实验证据,非猜测)**:
|
||||
1. **CORS 实证**:QMT Bridge REST(/health、/data/subscribe、/data/tick)响应**不含 Access-Control-Allow-Origin 头**,OPTIONS 预检返回 404 → 浏览器页面(origin=127.0.0.1:3080)直接 fetch QMT Bridge REST 会被**跨域拦截**。这与项目历史一致:REST 一直走 DSH 服务端代理(qmt-connections/test 注释「浏览器跨域规避」)。
|
||||
2. **架构结论**:**订阅是 RESTful 接口(POST /data/subscribe),必须走 DSH 服务端代理转发**,服务端无跨域问题;前端直连 REST 订阅是错误的架构。
|
||||
3. **WS 行为实验**(Node 直连 GEMWIN):
|
||||
- 不订阅直接连 WS → 收到 25 条/5秒 推送(首个连接);
|
||||
- REST 订阅成功(sub_id 有效)→ 连 WS → 0 条;
|
||||
- 全新进程不订阅直连 → 稳定收到 21 条/5秒。
|
||||
- 结论:**QMT Bridge 的 WS 推送语义与 OpenAPI 文档描述不符**(订阅反而可能干扰推送),该问题**归属 QMT Bridge 工作空间**(老师负责,另行解决)。
|
||||
4. **本工作空间策略**:订阅与快照(REST)一律走 DSH 服务端代理;数据显示先以 **tick 快照轮询**(GET /data/tick,稳定可用)为兜底数据源 + WS 推送为增强通道,确保持仓列表实盘价格**先显示出来**。
|
||||
|
||||
**涉及文件**:本记录(docs/04-迭代记录/04-WS盘中价格实时更新/迭代说明.md)。
|
||||
|
||||
|
||||
### 2026-08-31 · 服务端:新增 subscribe-ws / tick-snapshot 代理端点
|
||||
|
||||
- **做了什么**:src/api.js 新增两个端点(均走 DSH 服务端代理,无跨域):
|
||||
- `qmt-connections/subscribe-ws`:代理 `POST {激活baseUrl}/data/subscribe`(body { type, codes }),返回 { ok, sub_id };
|
||||
- `qmt-connections/tick-snapshot`:代理 `GET {激活baseUrl}/data/tick?codes=...`,返回 { ok, data: { code: snapshot } }。
|
||||
- **为什么**:订阅/快照是 RESTful 接口,浏览器直连 QMT Bridge 会跨域(无 CORS 头、OPTIONS 404),统一走服务端代理(与 qmt-connections/test 同模式,项目既有架构约定)。
|
||||
- **涉及文件**:src/api.js。
|
||||
|
||||
### 2026-08-31 · 客户端:MarketDataProvider 重构(服务端代理 + 快照轮询兜底)
|
||||
|
||||
- **做了什么**:重写 src/client/market/MarketDataProvider.jsx:
|
||||
- 订阅 / 快照 / 退订全部改走服务端代理端点(/odl/api/qmt-connections/subscribe-ws、tick-snapshot、unsubscribe-ws),**移除前端直连 QMT Bridge REST**;
|
||||
- **双通道价格源**:① tick 快照轮询(3s 间隔,走服务端代理,稳定兜底,确保现价一定显示);② WS 推送(增强通道,收到即实时覆盖);
|
||||
- WS 连接保持前端直连(WebSocket 不受 CORS 限制),断线重连、激活配置切换联动保留。
|
||||
- **为什么**:实证确认 QMT Bridge REST 无 CORS 头(浏览器直连被拦截);「先实现订阅后的数据显示」——快照轮询保证现价先显示出来,WS 作为增强。
|
||||
- **涉及文件**:src/client/market/MarketDataProvider.jsx。
|
||||
|
||||
### 2026-08-31 · 模拟验证
|
||||
|
||||
- **做了什么**:Node 模拟新版前端流程(tick 快照轮询走服务端代理)→ 一次返回全部关注 code(600719.SH/601117.SH/603028.SH/001330.SZ/002065.SZ)的 lastPrice + lastClose,现价提取完整(如 600719.SH → 7.17,昨收 7.22 → 跌,UI 绿色)。
|
||||
- **为什么**:验证服务端代理 + 快照轮询链路可用,前端渲染 PriceCell 即可显示红涨绿跌。
|
||||
- **涉及文件**:无(验证记录)。
|
||||
|
||||
|
||||
### 2026-08-31 · 端到端验收(宿主链路)
|
||||
|
||||
- **做了什么**:服务端新端点经宿主验证生效(subscribe-ws 订阅成功拿 sub_id;tick-snapshot 返回真实持仓现价);前端 bundle 确认加载新代码(rev=ae40cadd056a,与本地 lib 一致)。
|
||||
- **端到端链路验证**(Node 模拟前端调宿主 /odl/api):
|
||||
- 激活配置 GEMWIN → 订阅 ok(sub_id)→ 13 条持仓 → tick 快照 5/5 命中现价 → 红涨绿跌判定正确(600719.SH 跌→绿、603028.SH 涨→红)→ 退订 ok。
|
||||
- **验证中发现**:/odl/api/positions 端点返回**数组**(value 直接是数组),非 summary 的 {positions:[...]} 结构(测试脚本字段误用,已修正;非产品问题)。
|
||||
- **待老师确认**:浏览器刷新后 3 个表格现价列显示与红涨绿跌效果。
|
||||
- **涉及文件**:无(验证记录)。
|
||||
|
||||
|
||||
### 2026-08-31 · 修复:快照用真实持仓代码(根因定位)
|
||||
|
||||
- **现象**:刷新后订阅成功(sub_id 日志出现),但快照日志与现价均无。
|
||||
- **定位(实证,非猜测)**:给前端加调试日志(订阅/快照/prices 三处)→ 只见订阅成功,无快照日志 → 审查 fetchSnapshot 传参为 'SH,SZ'(交易所代码)→ 宿主实测:tick-snapshot 传 'SH,SZ' 返回 **data 为空** {},传真实股票代码(600719.SH)才返回数据。
|
||||
- **根因**:QMT Bridge 的 /data/tick 需要**真实股票代码**,前端快照却传了交易所代码 'SH,SZ',导致 data 为空、现价无数据。
|
||||
- **修复**:MarketDataProvider 新增 fetchWatchCodes()(从 /odl/api/positions 获取持仓 code 集合),快照改用真实持仓代码。
|
||||
- **验证**:Node 模拟修复后流程 → 13 只持仓全部取到现价(002347.SZ → 6.25 昨收 6.14 红涨;600719.SH → 7.17 昨收 7.22 绿跌)。
|
||||
- **涉及文件**:src/client/market/MarketDataProvider.jsx。
|
||||
|
||||
|
||||
### 2026-08-31 · 架构升级:服务端中转 + 行情缓存(老师定)
|
||||
|
||||
- **老师决策**:DSH 服务端做「一次 WS 订阅 + QMT 桥整合」,维护一份实时行情缓存(内存 Map);页面统一 5s 轮询读 DSH 接口(按 code 查询),不做前端直连。
|
||||
- **实现**:
|
||||
- 新增 src/market-cache.js:MarketCache 类(服务端连 ws://<激活host>:8610/ws 收全市场推送 → 写缓存 Map;断线指数退避重连;setBaseUrl 热切换;getByCodes 按 code 查询);
|
||||
- src/index.js:创建 MarketCache 并启动,插件释放时 stop;激活热切换同步 setBaseUrl;
|
||||
- src/api.js:新增 `market-snapshot` 端点(按 codes 查询缓存);激活/更新/删除连接时同步 marketCache;
|
||||
- src/client/market/MarketDataProvider.jsx:**移除前端直连 WS**,改为 5s 轮询 /odl/api/market-snapshot(先取持仓 code,再按 code 查询)。
|
||||
- **WS 推送稳定性实验**(Node 直连 GEMWIN,2026-08-31):
|
||||
- 不订阅直连 10s → 0 条;REST 订阅后连 10s → 0 条;退订后再连 10s → 8 条;
|
||||
- 结论:**QMT Bridge WS 推送是会话级/有状态行为**,仅首个连接或特定订阅窗口会推送——该问题归属 QMT Bridge 工作空间(老师负责,另行解决)。
|
||||
- **缓存兜底**:MarketCache 以 WS 推送更新缓存为主,**辅以 REST tick 快照补数据**(REST 稳定可用,13 只持仓全部可取),确保缓存有值、前端轮询能拿到现价。
|
||||
- **涉及文件**:src/market-cache.js(新增)、src/index.js、src/api.js、src/client/market/MarketDataProvider.jsx。
|
||||
|
||||
|
||||
### 2026-08-31 · 缓存兜底:market-snapshot 按需补 + REST 定时刷新
|
||||
|
||||
- **做了什么**:
|
||||
- src/market-cache.js:MarketCache 加 REST tick 快照兜底(每 5s 对缓存已有 code 用 /data/tick 刷新);
|
||||
- src/api.js:market-snapshot 端点加「miss 按需补」——查询的 code 不在缓存时,现场用 REST /data/tick 拉取补入缓存并返回(**不依赖 WS 推送**,REST 稳定可用)。
|
||||
- **为什么**:2026-08-31 实验证实 QMT Bridge WS 推送是会话级/有状态行为(仅首个连接或特定订阅窗口推送),不能作为唯一数据源;REST tick 稳定可取(13 只持仓全取到),作为兜底确保前端轮询必有数据。
|
||||
- **验证**:Node 模拟 market-snapshot(缓存空→按需补→5/5 命中→缓存 5 条→再次查询全命中)。
|
||||
- **涉及文件**:src/market-cache.js、src/api.js。
|
||||
|
||||
|
||||
### 2026-08-31 · 结构调整:src/api.js 按领域拆分
|
||||
|
||||
- **做了什么**(老师要求,审视结构后清理):
|
||||
- 原 src/api.js(377 行,聚合 5 类端点)拆分为:
|
||||
- src/api/index.js:路由入口(合并各领域方法表 + 405/404/500 公共处理 + 领域分发);
|
||||
- src/api/common.js:公共工具(writeJson / readJsonBody / resolveActiveBaseUrl);
|
||||
- src/api/positions.js:持仓域(positions);
|
||||
- src/api/strategies.js:策略/份额/tabs 域(14 端点);
|
||||
- src/api/qmt-connections.js:QMT 连接域(11 端点,含热切换编排 setBaseUrl + marketCache 同步);
|
||||
- src/api/market.js:行情域(market-snapshot,含 miss 按需补)。
|
||||
- 更新 src/index.js import(./api.js → ./api/index.js)与 tsdown.config.ts 入口。
|
||||
- **验证**:typecheck 0 错误;构建通过(lib/api/index.js + api chunk 正常);27 个端点方法表核对完整(positions 1 + strategies 14 + qmt 11 + market 1);构建产物模块加载正常(registerApi 正确导出)。
|
||||
- **涉及文件**:src/api/*(6 个新文件)、src/index.js、tsdown.config.ts;删除 src/api.js。
|
||||
|
||||
|
||||
### 2026-08-31 · 拆分验收(宿主重启后全端点回归)
|
||||
|
||||
- **做了什么**:宿主重启后对拆分后的 API 做全端点回归测试。
|
||||
- **结果**:
|
||||
- 27 端点全部可用(15 项首轮通过;3 项「失败」为测试参数错误——shares=0 触发业务校验、无未分配份额触发 no-unallocated,属正确业务行为);
|
||||
- 合法参数重测 add-shares / remove-shares / move-all-shares 全部 OK(600719.SH 100 股往返,清理干净);
|
||||
- 核心端点:market-snapshot 13/13、active-host GEMWIN、QMT CRUD、subscribe-ws/unsubscribe-ws、tick-snapshot(lastPrice=7.15)全部通过。
|
||||
- **结论**:api.js 按领域拆分无功能回归,验收通过。
|
||||
- **涉及文件**:无(验证记录)。
|
||||
|
||||
|
||||
### 2026-08-31 · 结构调整:创建 src/component/ 目录 + market-cache 拆分为 Hub/Feed
|
||||
|
||||
- **老师决策**:在 src/ 下统一创建 component/ 目录收纳业务模块(便于统一位置管理);market-cache 拆成多个模块各一个文件。
|
||||
- **实现**:
|
||||
- 新建 src/component/ 目录:
|
||||
- MarketDataHub.js:行情缓存服务(缓存 Map + getByCodes 查询 + **miss 自动补拉收敛在内部** + ingest 写入入口);
|
||||
- MarketFeed.js:行情数据获取(WS 订阅连接 + REST 定时刷新兜底 + 断线重连 + setBaseUrl 热切换),写入 Hub;
|
||||
- src/index.js:装配改为 marketHub + marketFeed(替代 marketCache);
|
||||
- src/api/market.js:**移除 miss 补拉代码**,只调 hub.getByCodes(HTTP 编排纯净);
|
||||
- src/api/qmt-connections.js:热切换 marketCache?.setBaseUrl → marketFeed?.setBaseUrl;
|
||||
- src/api/index.js:runtime 命名 marketCache → marketHub/marketFeed;
|
||||
- 删除 src/market-cache.js。
|
||||
- **职责划分**(解决「职责混乱」):
|
||||
- MarketDataHub = 缓存 + 查询(单一职责,查询保证有值);
|
||||
- MarketFeed = 取数(WS + REST 刷新),不对外查询;
|
||||
- api/market.js = 纯 HTTP 编排,不碰缓存内部。
|
||||
- **验证**:typecheck 0 错误;构建通过(chunk 打包正常,无动态导入残留);Node 模拟 hub.getByCodes 三场景(空缓存 miss 补拉 / 命中缓存 / 重复全命中)全部通过。
|
||||
- **涉及文件**:src/component/MarketDataHub.js(新增)、src/component/MarketFeed.js(新增)、src/index.js、src/api/market.js、src/api/qmt-connections.js、src/api/index.js;删除 src/market-cache.js。
|
||||
|
||||
|
||||
### 2026-08-31 · 拆分验收(宿主重启后全端点回归)
|
||||
|
||||
- **结果**:宿主重启后全端点回归全部通过:
|
||||
- market-snapshot 13/13(miss 补拉正常,600719.SH → 7.18);二次查询命中缓存 13 条;
|
||||
- summary 13 只;qmt-connections / active-host(GEMWIN)/ activate 热切换 / subscribe-ws+unsubscribe-ws / tick-snapshot(7.18)/ strategies(2 个)全部 OK。
|
||||
- **结论**:MarketDataHub + MarketFeed 拆分无功能回归,component/ 目录结构落地,验收通过。
|
||||
- **涉及文件**:无(验证记录)。
|
||||
|
||||
|
||||
### 2026-08-31 · 优化:移除冗余端点 + 前端去重 + 模块迁入 component/
|
||||
|
||||
**#1 移除冗余端点**(死代码清理):
|
||||
- 移除 src/api/qmt-connections.js 的 subscribe-ws / unsubscribe-ws / tick-snapshot 三端点(架构演进后无人调用:前端无引用、MarketFeed 直接 new WebSocket 不依赖它们)。
|
||||
- 涉及文件:src/api/qmt-connections.js。
|
||||
|
||||
**#2 前端去重**(消除重复 positions 调用):
|
||||
- MarketDataProvider 不再自拉 positions 获取关注 code,改为暴露 registerCodes(codes),由表格组件渲染后上报(AllPositionsTab 上报 summary 持仓 code、StrategyTab 上报策略持仓 code);Provider 只轮询 market-snapshot。
|
||||
- 收益:减少一次网络往返;Provider 与表格数据天然同步(同一份 code 来源)。
|
||||
- 涉及文件:src/client/market/MarketDataProvider.jsx、src/client/views/AllPositionsTab.jsx、src/client/views/StrategyTab.jsx。
|
||||
|
||||
**#3 业务模块迁入 src/component/**(结构治理延续):
|
||||
- 迁移:storage.js → component/AllocationStorage.js;position/manager.js → component/PositionManager.js;data-source/qmt-bridge-rest.js → component/QmtBridgeRestDataSource.js;data-source/types.js → component/data-source-types.js。
|
||||
- 删除空的 src/position/、src/data-source/ 目录;更新 index.js import、tsdown 入口(src/component/*.js)、JSDoc 类型引用。
|
||||
- 收益:lib/component/ 每个模块独立编译文件,结构统一(component = 业务模块,api = 路由,client = 前端)。
|
||||
- 涉及文件:迁移 4 文件 + src/index.js + tsdown.config.ts + 2 处 JSDoc。
|
||||
|
||||
**验证**:typecheck 0 错误;构建通过(lib/component/ 6 个模块独立文件);构建产物模块加载全部正常。
|
||||
|
||||
|
||||
### 2026-08-31 · 优化验收(宿主重启后)
|
||||
|
||||
- **全端点回归通过**:positions 13 条、market-snapshot 13/13、summary 13 只、strategy-positions 12 只、qmt-connections/active-host GEMWIN、已移除端点 tick-snapshot 返回 404(确认移除生效)、test 可达 82ms、二次 market-snapshot 命中缓存。
|
||||
- **备注**:回归中出现一次 positions/summary fetch failed,排查为 QMT Bridge 瞬时网络抖动(Tailscale relay 切换,ping 通 + health ok 后恢复),非代码问题。
|
||||
- **结论**:#1 移除冗余端点 + #2 前端去重 + #3 模块迁入 component/ 三项优化全部完成,无功能回归,验收通过。
|
||||
- **涉及文件**:无(验证记录)。
|
||||
|
||||
|
||||
### 2026-08-31 · 数据误删事故分析与防再发机制
|
||||
|
||||
**事故**:大连热电(600719.SH)与万顺新材(300057.SZ)的份额分配数据丢失。
|
||||
|
||||
**根因(非业务代码 bug,是测试脚本误操作)**:
|
||||
- 我在回归测试脚本(regression.mjs / retest-shares.mjs)中在**真实生产数据**上执行了写操作:
|
||||
- regression.mjs 的 remove-all-shares(清空某策略全部份额);
|
||||
- retest-shares.mjs 对 600719.SH 的 move-all-shares + remove-shares 清理。
|
||||
- 这些操作本应只读或使用临时数据,误在真实 allocations.json 上执行,导致 600719 的 grid/manual-t 份额、300057 的 manual-t 份额被清空。
|
||||
|
||||
**恢复**:按老师提供的原值恢复(600719 = grid 800 + manual-t 1000;300057 = grid 1000 + manual-t 1000),已确认 13 只全部正确。
|
||||
|
||||
**防再发机制(老师定:测试用独立数据目录)**:
|
||||
- AllocationStorage 新增测试模式:环境变量 ODL_TEST_DATA_DIR 或构造参数 dataDir 指向独立目录时,存储隔离,不影响真实 ~/.dsh/one-divine-lot/allocations.json;
|
||||
- 已验证:临时目录实例执行 set/removeStrategyShares 写操作,真实数据不变;
|
||||
- 测试规范:回归/测试脚本必须用独立数据目录,**禁止在真实数据上执行写操作**(只读端点可直连,写操作一律走测试实例)。
|
||||
|
||||
**涉及文件**:src/component/AllocationStorage.js(测试模式支持)。
|
||||
|
||||
|
||||
### 2026-08-31 · R-006:策略数据 JSON data store schema 标准化
|
||||
|
||||
**需求定稿**(讨论确认):
|
||||
- 范围:策略数据集(份额分配)标准化;策略定义仍留 DSH settings(设置页交互不变);
|
||||
- 形态:store.schema.json(类 JSON Schema 描述 dataset)+ store.json(数据,策略为中心);
|
||||
- dataset 格式:[{code, shares}] 数组(可扩展);
|
||||
- 旧 allocations.json 启动时自动迁移(保留 .bak 备份);
|
||||
- 删除策略时 API 层联动删 store dataset(同现状联清份额模式)。
|
||||
|
||||
**实现**:
|
||||
- 新增 src/component/DataStore.js:STORE_SCHEMA 定义 + 读写 store.json(原子写)+ 迁移 + dataset CRUD(getDataset/setDataset/removeDataset/getAllDatasets);
|
||||
- 重写 src/component/PositionManager.js:存储改接 DataStore(策略为中心),对外方法签名不变(addToStrategy/removeFromStrategy/getSummary 等);
|
||||
- src/index.js:DataStore 替代 AllocationStorage 装配。
|
||||
|
||||
**验证**(测试隔离目录):
|
||||
- 迁移:allocations.json(code 为中心)→ store.json(策略为中心 strategies[].dataset=[{code,shares}]),旧文件备份 .bak ✅;
|
||||
- schema 生成 ✅;dataset CRUD ✅;原子写多次 save 完整 ✅。
|
||||
|
||||
**涉及文件**:src/component/DataStore.js(新增)、src/component/PositionManager.js(重写)、src/index.js。
|
||||
|
||||
|
||||
### 2026-08-31 · R-006 验收(宿主重启后)
|
||||
|
||||
- **迁移验证**:宿主重启自动迁移真实 allocations.json → store.json(策略为中心),13 只持仓份额完整转入(grid 13 只 + manual-t 2 只),旧文件备份 allocations.json.bak ✅
|
||||
- **数据完整性**:大连热电 grid 800 + manual-t 1000(分配 1800);万顺新材 grid 1000 + manual-t 1000(分配 2000)✅
|
||||
- **端点回归**:summary 13 只、strategy-positions(grid) 13 只、strategies 列表正常;add-shares 超限正确拒绝(业务校验);strategies/remove 联动清 dataset 正常;market-snapshot 行情正常 ✅
|
||||
- **真实数据未污染**:测试用临时策略(add/remove 往返 + 删除联动),600719 份额不变 ✅
|
||||
- **结论**:R-006 JSON data store schema 标准化完成,数据迁移零丢失,无功能回归,验收通过。
|
||||
- **涉及文件**:无(验证记录)。
|
||||
|
||||
|
||||
### 2026-08-31 · 修复:行情状态误显示「无激活配置」
|
||||
|
||||
- **现象**:点击持仓 tab 能加载持仓数据,但状态条显示「行情未连接(无激活配置)」。
|
||||
- **根因**:MarketDataProvider 主循环在 watchCodes 为空(表格尚未上报 code)时设置 DISCONNECTED 状态;DISCONNECTED 文案「无激活配置」是前端直连时代遗留——R-006 后行情走服务端中转,前端对激活配置应**无感**。
|
||||
- **修复**:
|
||||
- Provider 主循环:watchCodes 为空时保持 IDLE(等待表格上报),不再设 DISCONNECTED;用 statusRef 同步最新状态避免闭包过期;
|
||||
- PriceCell 状态文案:disconnected 改为「行情未连接」(去掉「无激活配置」);
|
||||
- IDLE 时不显示状态条。
|
||||
- **涉及文件**:src/client/market/MarketDataProvider.jsx、src/client/views/PriceCell.jsx。
|
||||
|
||||
|
||||
### 2026-08-31 · 行情持久化(store.market.json,解决首屏无价格)
|
||||
|
||||
- **问题**:行情缓存仅内存(MarketDataHub.cache Map),宿主重启后为空;持仓 tab 首次刷新时价格 miss,若 REST 补拉慢/失败则显示「—」。
|
||||
- **根因**:行情未持久化——重启后无上次价格,需每次实盘订阅/拉取。
|
||||
- **方案(老师确认:DataStore 就是为数据快速展现存在)**:
|
||||
- DataStore 新增行情持久化:store.market.json(code → snapshot,原子写);
|
||||
- MarketDataHub 接入:启动 warmup 读盘 → 内存缓存首屏即有价;ingest 增量更新后防抖写盘(3s);查询命中顺序:内存 → 磁盘 → REST 补拉;
|
||||
- index.js:dataStore 注入 Hub + 启动预热。
|
||||
- **验证**(测试隔离目录):实盘 ingest → 写盘 2 条;模拟重启 warmup 加载 2 条;首次查询 2/2 命中(600719 lastPrice=7.16)——重启后首屏即有价格 ✅。
|
||||
- **涉及文件**:src/component/DataStore.js、src/component/MarketDataHub.js、src/index.js。
|
||||
|
||||
|
||||
### 2026-08-31 · 修复:启动报错 storage 未初始化(TDZ)
|
||||
|
||||
- **现象**:宿主重启后插件加载失败,报 `Cannot access 'storage' before initialization`。
|
||||
- **根因**:index.js 装配时序错误——marketHub 创建时引用 storage(dataStore: storage),但 storage 在其后才创建(const 暂时性死区 TDZ)。
|
||||
- **修复**:将 storage 创建移到 marketHub 之前(55 行),顺序改为 storage → marketHub → marketFeed。
|
||||
- **验证**:装配顺序正确,构建通过。
|
||||
- **涉及文件**:src/index.js。
|
||||
|
||||
|
||||
### 2026-09-01 · 行情持久化验收(宿主重启后)
|
||||
|
||||
- **启动修复**:storage 时序问题(TDZ)修复后宿主正常启动。
|
||||
- **持久化验证**:
|
||||
- store.market.json 已生成(13 条行情快照,600719 大连热电 lastPrice=7.16 等完整字段);
|
||||
- market-snapshot 查询 13/13 命中(内存→磁盘→REST 三级);
|
||||
- 即使 QMT 断网,磁盘缓存也能提供上次价格(首屏有价,不依赖实盘订阅)。
|
||||
- **结论**:行情持久化闭环完成,验收通过。
|
||||
- **涉及文件**:无(验证记录)。
|
||||
|
||||
|
||||
### 2026-09-01 · 修复:持仓 tab 价格要等很久才出现(时序)
|
||||
|
||||
- **现象**:打开全部持仓,价格要等很久(约 5s)才刷新出来。
|
||||
- **根因**:MarketDataProvider 主循环是 5s 定时器;页面加载时 poll() 立即跑一次但 watchCodes 为空(表格未上报)跳过;表格加载后 registerCodes 上报 code,但**不触发立即拉取**,只能等下一次定时器(最多 5s)才有价格。
|
||||
- **修复**:registerCodes 上报新 code 后**立即触发一次 pollMarket**(通过 pollMarketRef 引用),首屏价格快速显示;定时器继续 5s 增量更新。
|
||||
- **验证**:构建通过。服务端 market-snapshot 本就 13/13 有价(已实测),修复后前端上报即拉取,不再等定时器。
|
||||
- **涉及文件**:src/client/market/MarketDataProvider.jsx。
|
||||
|
||||
|
||||
### 2026-09-01 · MarketFeed 启动主动拉取持仓盘口(prime)
|
||||
|
||||
- **问题**(老师指出):前端打开页面只有持仓数据、价格要等行情刷新才出现;但行情刷新是服务端职责,前端应直接拿到最新最终价格。
|
||||
- **根因查证**:插件启动时 MarketFeed 只做了 ①warmup 读磁盘(可能旧/空)②连 WS(被动等推送,QMT 推送会话级不可靠)③定时刷新(**空缓存时跳过**)——**没有任何「启动主动拉一次最新盘口」的逻辑**。
|
||||
- **修复**:MarketFeed.start() 新增 _primePositions():启动即从 dataSource 拿持仓 code → REST /data/tick → 写入缓存;setBaseUrl 热切换时也主动拉一次(新地址盘口)。
|
||||
- **验证**:Node 模拟启动 → prime 拉取 13 条盘口,缓存 13 条,查询 600719 → 7.16 ✅。
|
||||
- **效果**:服务端启动即有最新盘口价,前端任何时候打开页面都能从缓存拿到价格(不依赖前端轮询触发)。
|
||||
- **涉及文件**:src/component/MarketFeed.js。
|
||||
|
||||
|
||||
### 2026-09-01 · 需求:移除行情状态条 + 订阅状态灯 + QMT 健康检查
|
||||
|
||||
**需求 1:移除「行情实时」状态条**
|
||||
- 前端表格彻底移除 MarketStatusBar(PriceCell 删除该组件;AllPositionsTab / StrategyTab 移除引用与 status/wsInfo 解构)。
|
||||
- 原则:行情刷新是服务端职责,前端只拿结果,对行情状态彻底无感。
|
||||
- 涉及文件:src/client/views/PriceCell.jsx、AllPositionsTab.jsx、StrategyTab.jsx。
|
||||
|
||||
**需求 3:QMT 连接健康检查**
|
||||
- 新增 market-status 端点(src/api/market.js):探测激活 QMT Bridge /health 可达性(5s 超时)+ 返回订阅状态 + 缓存大小。
|
||||
- MarketFeed 新增 getStatus()(WS 连接态、最近推送时间、推送计数)。
|
||||
|
||||
**需求 2:订阅状态灯(QMT 切换 chip)**
|
||||
- QmtConnectionChip 文字前加圆点状态灯:
|
||||
- 绿:订阅存在 + 数据正常接收 + QMT 健康;
|
||||
- 黄:订阅存在但数据停滞(最近 30s 无推送);
|
||||
- 红:QMT 连接不健康;
|
||||
- 灰:无订阅。
|
||||
- 每 5s 轮询 market-status;下拉菜单显示订阅明细(推送次数)+ QMT 健康(延迟)。
|
||||
- 涉及文件:src/client/views/QmtConnectionChip.jsx。
|
||||
|
||||
**验证**:typecheck 0 错误;构建通过;本地模拟 market-status(QMT 健康 162ms、订阅 active+dataHealthy、状态灯绿)逻辑正确。
|
||||
|
||||
|
||||
### 2026-09-01 · 修正:market-status 数据健康判断(WS 不可靠时结合缓存)
|
||||
|
||||
- **问题**:宿主重启后 market-status 返回 wsConnected=true 但 tickCount=0、lastTickAt=0 → dataHealthy=false → 状态灯黄(停滞)。但缓存实际有 13 条(REST prime/轮询兜底)。
|
||||
- **根因**:数据健康只以「WS 收到推送」为标准,而 QMT WS 推送是会话级/有状态(连接后可能 0 推送)。
|
||||
- **修正**:dataHealthy = WS 推送新鲜(30s 内)**或** 缓存有数据(cacheSize > 0)——REST 兜底保证缓存更新时数据仍是好的。
|
||||
- **涉及文件**:src/api/market.js。
|
||||
|
||||
|
||||
### 2026-09-01 · 状态灯不稳定分析(黄/灰波动)
|
||||
|
||||
- **实测**(连续 5 次 market-status):qmt.healthy=true、wsConnected=true(WS 稳定)、tickCount=0、cache=13、dataHealthy=false → 灯黄。
|
||||
- **根因**:
|
||||
1. 服务端 dataHealthy 修正(wsFresh || cacheSize)**未生效**——宿主加载的是上一轮构建前的旧代码(构建在重启后完成);
|
||||
2. 前端首次加载 marketStatus=null 显示灰,轮询成功后变黄;「一会儿又变灰」可能与前端某次请求失败/时序有关(已加调试日志待确认)。
|
||||
- **修复**:
|
||||
- 服务端:dataHealthy = WS 推送新鲜 || 缓存有数据(WS 不可靠时缓存兜底);
|
||||
- 前端:loadStatus 失败保留上次状态(不误降灰);加调试日志定位。
|
||||
- **待办**:重启宿主让服务端修正生效,观察状态灯应为绿(订阅+缓存正常)。
|
||||
|
||||
|
||||
### 2026-09-01 · 数据存储设计文档(设计约束)
|
||||
|
||||
- **做了什么**:整理 DataStore 设计为独立设计约束文档 docs/03-设计约束/数据存储设计.md:
|
||||
- 设计目标(数据快速展现、标准化描述、统一 JSON 操作);
|
||||
- 总体架构(store.schema.json / store.json / store.market.json 三层);
|
||||
- 数据文件结构与 Schema(策略为中心份额分配 + 行情快照缓存);
|
||||
- 数据流(启动读盘 → 实盘更新 → 防抖写盘 → 三级查询命中);
|
||||
- 关键设计决策表(JSON 存储、策略定义留 settings、dataset 数组、行情持久化、原子写、测试隔离);
|
||||
- 迁移(allocations.json 自动迁移 + 备份)。
|
||||
- **同步更新**:技术方案约束.md —— 技术约束-010 更新为「服务端中转 + 缓存」(原前端直连已废弃);新增技术约束-012(数据存储设计引用)。
|
||||
- **涉及文件**:docs/03-设计约束/数据存储设计.md(新增)、docs/03-设计约束/技术方案约束.md。
|
||||
|
||||
|
||||
### 2026-09-01 · 重做:移除全盘订阅状态标记,独立 QMT health 检查
|
||||
|
||||
**移除**(全盘订阅状态暂缓,老师重开筛选做):
|
||||
- market-status 端点的 subscriptions 部分(订阅状态);
|
||||
- MarketFeed.getStatus()(订阅状态);
|
||||
- QmtConnectionChip 的订阅状态灯 + market-status 轮询。
|
||||
|
||||
**保留并重做**(QMT health 检查,需求 3):
|
||||
- 服务端:market-status → 精简为 qmt-health 端点(仅 QMT /health 可达性 + 延迟);
|
||||
- 前端:QmtConnectionChip 下拉框**右侧**新增独立 QmtHealthIndicator 控件(小圆点:绿灯=健康 / 红灯=异常;悬停显示延迟/错误;每 5s 轮询 qmt-health)。
|
||||
|
||||
**涉及文件**:src/api/market.js、src/component/MarketFeed.js、src/client/views/QmtConnectionChip.jsx。
|
||||
|
||||
|
||||
### 2026-09-01 · QMT 健康检查改为服务端定时 + 缓存机制
|
||||
|
||||
**机制(老师定)**:
|
||||
- 服务端 QmtHealthMonitor 每 **5 分钟**探测一次激活 QMT /health,结果缓存内存;
|
||||
- 前端打开页面读缓存(**秒回**,不等实时探测);
|
||||
- 前端点击圆点触发即时探测(refresh=true)刷新缓存;
|
||||
- 激活配置切换时立即补探(热切换联动)。
|
||||
|
||||
**实现**:
|
||||
- 新增 src/component/QmtHealthMonitor.js:定时探测(5min)+ 缓存 + getStatus(读缓存)+ probe(即时探测,防并发);
|
||||
- src/api/market.js:qmt-health 默认读缓存;refresh=true 触发即时探测;
|
||||
- src/index.js:创建并启动 QmtHealthMonitor,注入 registerApi,释放时 stop;
|
||||
- src/api/qmt-connections.js:激活/更新/删除连接后 qmtHealthMonitor.probe() 联动;
|
||||
- 前端 QmtHealthIndicator:打开读缓存、30s 低频同步、点击触发即时探测(悬停显示检测时间/延迟/错误)。
|
||||
|
||||
**验证**:typecheck 0 错误;构建通过;QmtHealthMonitor 模块加载正常。
|
||||
|
||||
|
||||
### 2026-09-01 · QMT 健康检查验收(宿主重启后)
|
||||
|
||||
- **验证**:
|
||||
- 读缓存(默认):healthy=true、fromCache=true,耗时 23ms(秒回);
|
||||
- 即时探测(refresh=true):healthy=true、latencyMs=745ms;
|
||||
- 再读缓存:fromCache=true(命中探测结果),耗时 3ms。
|
||||
- **结论**:服务端定时探测(5 分钟)+ 缓存机制生效;前端读缓存秒回,点击触发即时探测;验收通过。
|
||||
- **涉及文件**:无(验证记录)。
|
||||
|
||||
|
||||
### 2026-09-01 · 修复:store.market.json 膨胀(38MB,全市场行情入库)
|
||||
|
||||
- **问题**:store.market.json 膨胀到 38MB(50910 只股票行情)。
|
||||
- **根因**:MarketFeed WS 推送是全市场(whole),MarketDataHub.ingest 不过滤,把全市场 5 万+ code 写进缓存并持久化。
|
||||
- **修复**:MarketDataHub 增加 watchCodes 关注集合(前端查询 code + 持仓 code 自动加入);ingest 只保留关注集合内 code;写盘也只写关注集合(防抖)。MarketFeed prime 把持仓 code 加入 watch。
|
||||
- **清理**:删除膨胀的 store.market.json(修复后自动重建,只含持仓行情)。
|
||||
- **验证**:Node 模拟 WS 推 100 只 → 缓存只 3 只持仓 → store.market.json 只 3 条 ✅。
|
||||
- **涉及文件**:src/component/MarketDataHub.js、src/component/MarketFeed.js。
|
||||
|
||||
|
||||
### 2026-09-01 · 修复 v2:warmup 认可全市场导致 watchCodes 过滤失效
|
||||
|
||||
- **问题**:重启后 store.market.json 又膨胀到 38MB(50910 键)——watchCodes 过滤未生效。
|
||||
- **根因**:MarketDataHub.warmup 从磁盘加载旧数据时,把**所有 code 都加入 watchCodes**(this.watch(code))——磁盘历史认可了全市场,后续 WS 推送全部保留。
|
||||
- **修复**:warmup 只加载数据到内存 cache,**不加入 watchCodes**(watchCodes 只由「持仓 + 前端查询」驱动)。
|
||||
- **清理**:再次删除膨胀文件(重建后只含持仓 13 条)。
|
||||
- **涉及文件**:src/component/MarketDataHub.js。
|
||||
|
||||
|
||||
### 2026-09-01 · 修复 v3:setMarketQuotes 整体替换(清除旧键残留)
|
||||
|
||||
- **问题**:v2 后 store.market.json 仍含旧全量数据(100 条旧 + 新)。
|
||||
- **根因**:DataStore.setMarketQuotes 是**增量合并**(this.market[code]=snap),旧键永不清除——磁盘旧数据(如 50910 条)残留,每次合并越积越多。
|
||||
- **修复**:setMarketQuotes 改为**整体替换**(this.market = next),传入完整集合,清除不在其中的旧键。
|
||||
- **验证**:磁盘预置 100 旧 → warmup → 持仓查询 → WS 推送 → 写盘只 2 条(600719/300057),旧 100 条清除 ✅。
|
||||
- **清理**:删除真实膨胀文件(重启重建只含持仓)。
|
||||
- **涉及文件**:src/component/DataStore.js。
|
||||
|
||||
|
||||
### 2026-09-01 · 膨胀修复验收(宿主重启后)
|
||||
|
||||
- **结果**:store.market.json 从 38MB(50910 键)恢复为 **11KB(13 键 = 13 只持仓)**,无多余键。
|
||||
- **结论**:watchCodes 过滤 + warmup 不认可全市场 + setMarketQuotes 整体替换 三层修复全部生效,膨胀 bug 彻底解决,验收通过。
|
||||
- **涉及文件**:无(验证记录)。
|
||||
|
||||
|
||||
### 2026-09-01 · 决策:全市场缓存方案搁置(保持 watchCodes)
|
||||
|
||||
- **讨论**:老师考虑「全市场缓存」(前端任意 A股实盘查询秒回),vs 当前 watchCodes(持仓 + 前端查询自动累积)。
|
||||
- **实测发现**:QMT whole 推送 26,751 条中,**标准 A股仅 2,775 只(10.4%)**,其余 23,976 条为期权/衍生品/其他品种代码段(23x/10x/19x/16x 等)——A股真实约 5,400 只,推送全市场含大量非股票。
|
||||
- **结论(老师定)**:全市场缓存方案**搁置**,保持当前 watchCodes 方案(持仓 + 前端查询过的 code 自动累积,内存稳定 ~13 条);
|
||||
- **记录**:若未来启用全市场缓存,需只缓存标准 A股代码(正则过滤 60/688/000/001/002/003/300/301),并先解决 WS 推送稳定性(归属 QMT Bridge 工作空间)。
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# 验收标准:04-WS盘中价格实时更新
|
||||
|
||||
## 验收标准线
|
||||
|
||||
1. 3 个监控表格(全部持仓 / 手动做T / 网格超市)出现「现价」列,显示关注股票的 lastPrice(无数据时显示 —);
|
||||
2. 盘中价格变化**自动更新**(无需手动刷新)—— 观察价格列随时间变化;
|
||||
3. 红涨绿跌着色正确(现价 > 昨收 红色,< 昨收 绿色);价格变化时有轻微高亮提示;
|
||||
4. WS 连接与激活 QMT 配置联动:切换激活配置(host 变化)→ WS 自动切换到新 host;断线自动重连;
|
||||
5. 现有功能不回归:份额分配 / 策略管理 / 连接配置管理照常工作。
|
||||
|
||||
## 验收方法
|
||||
|
||||
- 打开 3 个表格,确认现价列出现且显示真实价格(对照 QMT 端行情);
|
||||
- 观察一段时间(约 30s),确认价格随推送自动变化;
|
||||
- 临时断网/杀 Bridge 进程 → 确认重连提示与恢复;
|
||||
- 切换激活 QMT 配置(GEMWIN ↔ 其他)→ 确认 WS 随之切换;
|
||||
- 操作份额添加/移出,确认表格功能正常。
|
||||
|
||||
## 验收目标
|
||||
|
||||
- 现价实时更新闭环完整:WS 推送 → 价格 Map → 表格行更新;
|
||||
- 关注列表自动覆盖 3 个表格出现的所有股票;
|
||||
- 交互(着色/高亮/重连提示)符合预期。
|
||||
@@ -0,0 +1,85 @@
|
||||
# 技术实现方案:05-交易记录接入(订单与成交 + 日期导航)
|
||||
|
||||
> 依据:PLAN-006 | 需求:R-007 | 设计约束:技术约束-001/003/004/010、产品约束-007 方向语义
|
||||
|
||||
## 技术选型
|
||||
|
||||
- **数据源**:复用 QmtBridgeRestDataSource(REST 直连,技术约束-003),新增 3 方法;
|
||||
- **服务端 API**:webServer 自开路由(技术约束-004),新增 api/trades.js 领域;
|
||||
- **前端数据流**:服务端中转 + 前端轮询(技术约束-010 思路,复用 market-snapshot 模式);
|
||||
- **UI**:React 组件 + 现有表格风格(LoadState 三态 / 内联样式 / Toast);
|
||||
- **时间段查询**(v3,2026-09-01):RangeSelector 组件(今日/本周/上周快捷 + 手动起止日期),替代单日导航。
|
||||
|
||||
## 架构设计
|
||||
|
||||
### 服务端
|
||||
|
||||
```
|
||||
QmtBridgeRestDataSource.getOrders({code,status}) → GET /trade/orders → 语义化映射 order 列表
|
||||
QmtBridgeRestDataSource.getTrades() → GET /trade/trades → 语义化映射 trade 列表
|
||||
QmtBridgeRestDataSource.getTradingDates({s,e}) → GET /data/calendar/trading_dates → dates[]
|
||||
|
||||
api/trades.js(新增领域):
|
||||
orders → dataSource.getOrders(args)
|
||||
trades → dataSource.getTrades()
|
||||
trading-dates → dataSource.getTradingDates(args)
|
||||
(registerApi 合并 TRADE_METHODS + HANDLERS)
|
||||
```
|
||||
|
||||
**语义化映射(核心)**:
|
||||
|
||||
```
|
||||
order: { orderId, code, name, exchange, direction(48买/49卖), status, orderVolume,
|
||||
tradedVolume, totalVolume, limitPrice, tradedPrice, amount, insertTime }
|
||||
trade: { tradeId, orderId, code, name, exchange, direction, price, volume, amount,
|
||||
commission, tradeTime }
|
||||
```
|
||||
|
||||
**方向判定**:m_nOffsetFlag(48=买入/49=卖出)为主 + m_strOptName 交叉验证(R-007 Q3 闭环)。
|
||||
|
||||
### 客户端(单表合并)
|
||||
|
||||
```
|
||||
TradeRecordsTab(标题 + 时间段同一行 + 单表)
|
||||
├─ RangeSelector:trading-dates → 快捷范围(今日/本周/上周)+ 手动起止日期
|
||||
│ 今日=最近交易日单日(完整互斥时间段);本周=所在自然周周一~周五(不考虑周六日);本月=所在自然月1号~月末
|
||||
├─ 合并逻辑:fetch orders+trades(带 start/end)→ 按 orderId 分组 → 聚合
|
||||
│ 聚合:成交量=Σ、成交均价=加权、成交额=Σ、手续费=Σ
|
||||
├─ OrderRow:委托主行(聚合成交),点击展开/折叠
|
||||
└─ TradeDetailRows:展开的每笔成交明细
|
||||
```
|
||||
|
||||
**合并规则**:
|
||||
- 主行 = 一笔委托(orderId 唯一);
|
||||
- 成交聚合:m_strOrderSysID 匹配的 trades 聚合进主行(加权均价 + 求和);
|
||||
- 无成交(待报/已撤/废单):成交量/均价/金额/手续费显示 —;
|
||||
- 展开:逐笔渲染成交(时间/价/量/额/手续费/成交号)。
|
||||
|
||||
**排序**:委托主行默认按时间降序(最新在前);表头点击排序(时间/代码/方向/状态/委托量/成交量/金额),再次点击切换升/降序。
|
||||
|
||||
**轮询**:仅「今日」范围(最近交易日单日)3-5s 轮询;切换时间段取消轮询、按 range 一次性查询(历史接口就绪后接入 start/end 实际过滤)。
|
||||
|
||||
**时间段查询(v3 定稿)**:
|
||||
- 服务端 orders/trades 端点接收 start/end(透传,QMT Bridge 历史接口就绪后生效);
|
||||
- 前端 RangeSelector:三个快捷按钮 + 两个 date input;
|
||||
- 历史范围(非今日)本期占位「历史数据接口开发中」。
|
||||
|
||||
## 涉及设计约束
|
||||
|
||||
| 约束 | 内容 |
|
||||
|---|---|
|
||||
| 技术约束-001 | 统一数据源抽象:交易数据走 QmtBridgeRestDataSource 适配器 |
|
||||
| 技术约束-003 | 直连 QMT Bridge REST(不经过 MCP) |
|
||||
| 技术约束-004 | webServer 自开路由 /odl/api/*(不占 /api interceptor) |
|
||||
| 技术约束-010 | 服务端中转 + 前端轮询(复用市场模式) |
|
||||
| 产品约束-007 | 方向着色沿用红/绿习惯语义(交易:红买绿卖) |
|
||||
|
||||
## 实现步骤
|
||||
|
||||
1. 文档骨架:PLAN-006 + 迭代 05(本步);
|
||||
2. 数据源:getOrders/getTrades/getTradingDates + 语义化映射;
|
||||
3. 服务端 API:api/trades.js 三端点 + 领域分发;
|
||||
4. 客户端合并逻辑:fetch 两接口 → 分组 → 聚合;
|
||||
5. UI:DateNav + OrderRow + TradeDetailRows + TradeRecordsTab + 排序;
|
||||
6. 构建安装:pnpm run build + 刷新/重启;
|
||||
7. 验收 + 复盘。
|
||||
@@ -0,0 +1,39 @@
|
||||
# 迭代 05 复盘:交易记录接入(订单与成交 + 日期导航)(2026-09-01)
|
||||
|
||||
> 复盘日期:2026-09-01 | 迭代状态:**已完成(老师确认归档,需求 R-007 已移入已完成/)**
|
||||
> 关联需求:R-007(交易记录接入,范围已定稿)
|
||||
> 关联计划:PLAN-006(计划-交易记录接入.md)
|
||||
|
||||
## 结果
|
||||
|
||||
迭代 05 达成:交易记录 tab 接入当日委托(/trade/orders)+ 当日成交(/trade/trades),单表合并展示(委托主行 + 按 m_strOrderSysID 展开成交明细),日期导航(交易日历驱动,历史接口占位),排序默认降序可手动。
|
||||
|
||||
## 过程事实
|
||||
|
||||
1. **需求定稿(R-007)**:老师指令接入订单与成交 → 技术研究(API 字段全表 / 方向判定澄清 / 数据时效研究 / sample data)→ 范围定稿(当日订单+成交 + 日期导航)→ 展示设计定稿(单表合并 + 排序降序);
|
||||
2. **方向判定闭环**:m_nDirection=48 恒为操作标记(官方 innerApi 字典),真实方向在 m_nOffsetFlag(48买/49卖)+ m_strOptName 交叉验证;
|
||||
3. **数据时效确认**:/trade/orders 与 /trade/trades 仅当日数据(get_trade_detail_data 读客户端缓存),历史查询需老师完善 QMT Bridge 接口;
|
||||
4. **服务端实现**:QmtBridgeRestDataSource 新增 getOrders/getTrades/getTradingDates(语义化映射)+ api/trades.js 领域(orders/trades/trading-dates 端点);
|
||||
5. **客户端实现**:TradeRecordsTab(单表合并 + 排序 + 轮询 4s)+ DateNav(交易日历导航,当日默认,历史占位);
|
||||
6. **验证**:typecheck 通过、构建成功、数据源三方法端到端实测通过(博纳影业卖出 200 股 @6.00,手续费 0.852)、合并逻辑多笔成交场景验证通过(加权均价 6.03);
|
||||
7. **未做**:历史数据查询实现(等老师 QMT Bridge 接口)、本地持久化(Q5 本期不做)、按策略过滤(Q10 后续)。
|
||||
|
||||
## 经验(待沉淀)
|
||||
|
||||
- **方向判定**:大 QMT m_nDirection 恒 48 是操作标记,m_nOffsetFlag 才是方向(48买/49卖)——实现时勿踩坑;
|
||||
- **手续费字段**:m_dCommission 与 m_dComssion(拼写变体)同值,取值优先 m_dCommission;
|
||||
- **前端轮询**:静默刷新不闪 loading(setOrders 直接更新,不置 loading=true);日期切换时清理轮询定时器;
|
||||
- **服务端/客户端加载时机**:服务端改动需宿主重启,客户端 bundle 刷新即载(沿用迭代 03/04 经验)。
|
||||
|
||||
## 遗留/后续
|
||||
|
||||
1. **历史成交接口**:老师完善 QMT Bridge 后,DateNav 历史日期从占位切换为真实查询(trading-dates 已就绪,orders/trades 加日期参数即可);
|
||||
2. **本地持久化**:跨日复盘需按日落盘(Q5),待历史接口就绪后评估;
|
||||
3. **按策略过滤**:Q10 后续迭代(strategy_name 与插件策略体系打通)。
|
||||
|
||||
## 验收状态
|
||||
|
||||
- 服务端/数据源端到端验证:✅(真实数据实测通过)
|
||||
- 前端 UI:✅(老师验收通过:单表合并/时间段查询/排序/费用计算/展开明细)
|
||||
- 完整验收:✅(2026-09-01 老师确认迭代完结,需求 R-007 归档)
|
||||
- 本次迭代 3 次 git 提交:bb3f8bd(迭代03-05)/ 8caeb76(佣金合并计费)/ 08c4ad3(箭头样式)
|
||||
@@ -0,0 +1,27 @@
|
||||
# 迭代目标:05-交易记录接入(订单与成交 + 日期导航)
|
||||
|
||||
## 目标
|
||||
|
||||
在「交易记录」tab 接入 QMT Bridge **当日委托(/trade/orders)与当日成交(/trade/trades)**:**单表合并**展示(委托主行 + 按 m_strOrderSysID 展开成交明细)+ **基于交易日历的日期导航**(当日默认选中,历史接口占位)。
|
||||
|
||||
## 目标描述
|
||||
|
||||
- 范围:R-007(范围已定稿 2026-09-01);不新增其他功能域;
|
||||
- 要解决的问题:交易记录 tab 当前为占位(PlaceholderTab),无当日订单/成交数据展示,无法支撑交易复盘(目标-006);
|
||||
- 引用需求:**R-007(范围定稿,2026-09-01)**;
|
||||
- 实现方式:服务端 REST 直连 QMT Bridge(技术约束-003)+ 前端轮询(技术约束-010 思路)+ 单表合并 UI;
|
||||
- 日期导航:复用 /data/calendar/trading_dates,为老师完善历史成交接口预留。
|
||||
|
||||
## 目标讨论过程
|
||||
|
||||
- 2026-09-01 老师提出需求:接入订单与成交数据,开始需求和技术研究(R-007 入池);
|
||||
- 2026-09-01 技术研究:API 面实测(orders/trades 字段全表)、方向判定澄清(m_nOffsetFlag + m_strOptName)、数据时效研究(仅当日)、sample data 沉淀;
|
||||
- 2026-09-01 范围定稿:当日订单+成交接入 + 日期导航(老师将完善历史接口);
|
||||
- 2026-09-01 展示设计定稿:**单表合并**(委托主行 + 展开成交明细)+ **排序默认降序可手动调整**;
|
||||
- 关键决策:Q1-Q11 全部有结论(两块合一、3-5s 轮询、m_nOffsetFlag 判方向、本期不持久化、红买绿卖、P1 等)。
|
||||
|
||||
## 对老师的配合需求
|
||||
|
||||
- QMT Bridge(激活配置)可达,验证当日订单/成交返回真实数据;
|
||||
- 构建安装后重载页面配合验证;
|
||||
- 历史成交接口完善后接入日期导航历史查询(本期仅占位)。
|
||||
@@ -0,0 +1,28 @@
|
||||
# 验收标准:05-交易记录接入(订单与成交 + 日期导航)
|
||||
|
||||
## 验收标准线
|
||||
|
||||
1. 「交易记录」tab 显示**当日委托**(单表合并视图):每行一笔委托,展示时间/代码/名称/方向/状态/委托量/成交量/委托价/成交均价/成交额/手续费;
|
||||
2. **合并正确**:委托行聚合成交(按 m_strOrderSysID);有成交的委托行可**展开**查看每笔成交明细(成交时间/价/量/额/手续费/成交号);聚合金额与逐笔合计一致;
|
||||
3. **未成交委托**(待报/已报/已撤/废单):成交量/均价/金额/手续费显示 —;状态列中文映射正确(未报/待报/已报/部成/已成/已撤/废单等);
|
||||
4. **方向**:红买绿卖(红=买、绿=卖);方向判定正确(对照 m_strOptName);
|
||||
5. **时间段查询**:标题 + 时间段控件同一行;快捷按钮 今日/本周/本月 可快速切换(基于交易日历最近交易日计算);手动起止日期可用;非今日范围展示「历史数据接口开发中」占位;
|
||||
6. **排序**:委托主行默认降序(最新在前);点击表头可手动排序(升/降切换);
|
||||
7. **刷新**:当日数据 3-5s 自动刷新(无需手动);切换日期取消轮询;
|
||||
8. **不回归**:持仓/策略/行情/连接配置等现有功能正常。
|
||||
|
||||
## 验收方法
|
||||
|
||||
- 打开交易记录 tab,对照 QMT 端确认当日委托/成交数据真实显示(实测博纳影业 001330.SZ 限价卖出 200 股);
|
||||
- 点击有成交的委托行,确认展开显示每笔成交明细;
|
||||
- 检查未成交委托(若有)成交列显示 —;
|
||||
- 点击 今日/本周/上周 快捷按钮,确认时间段切换正确(今日=最近交易日单日、本周=周一~周五(交易日,不考虑周六日)、本月=完整自然月(1号~月末);三范围互斥,点哪个激活哪个);手动改起止日期确认生效;历史范围显示占位提示;
|
||||
- 点击表头排序,确认升/降序切换与默认降序;
|
||||
- 观察 3-5s 轮询刷新(数据变化自动更新);
|
||||
- 回归:全部持仓 / 策略 / 行情 / QMT 连接配置正常。
|
||||
|
||||
## 验收目标
|
||||
|
||||
- 交易记录闭环完整:REST 查询 → 语义化映射 → 单表合并 → 展开明细 → 日期导航;
|
||||
- 支撑交易复盘(目标-006):当日委托/成交全貌可见;
|
||||
- 交互(合并/展开/排序/导航/着色)符合定稿设计。
|
||||
@@ -0,0 +1,247 @@
|
||||
# UI 交互调用分析:06-数据存储SQLite
|
||||
|
||||
> 分析日期:2026-09-01 | 依据:技术实现方案.md(接口定义)
|
||||
> 目的:基于 SQLite 存储层接口(strategy_holdings / market_quotes_cache + 持仓生命周期方法集),梳理**前端各操作 → 服务端 → 存储层**的完整调用链,确认对外 API 不变的前提下,存储层升级对调用流程的影响。
|
||||
|
||||
## 调用链路总览(升级后不变的部分)
|
||||
|
||||
```
|
||||
前端组件(React)
|
||||
│ fetch POST /odl/api/<method> body={ args }
|
||||
▼
|
||||
connection.jsx useRpc() ── 标准化剥离 one-divine-lot/ 前缀
|
||||
▼
|
||||
服务端 src/api/*.js 领域分发(positions / strategies / qmt-connections / market / trades)
|
||||
▼
|
||||
PositionManager / MarketDataHub / Settings(业务逻辑)
|
||||
▼
|
||||
DataStore(改造)── 委托 ──▶ SqliteStore(node:sqlite,新增)
|
||||
│ ├─ strategy_holdings 表(持仓生命周期)
|
||||
│ └─ market_quotes_cache 表(行情快照)
|
||||
```
|
||||
|
||||
**关键结论**:前端组件与 /odl/api/* 端点**零改动**;改动集中在服务端业务层(PositionManager 适配单票生命周期)+ 存储层(DataStore→SqliteStore)。以下逐操作分析。
|
||||
|
||||
---
|
||||
|
||||
## 操作 1:策略持仓 tab 加载(读)
|
||||
|
||||
**触发**:进入策略 tab → `StrategyTab.load()`
|
||||
|
||||
**前端调用**(`StrategyTab.jsx`):
|
||||
```js
|
||||
Promise.all([
|
||||
call('one-divine-lot/strategy-positions', { args: { strategyId } }), // 策略持仓
|
||||
call('one-divine-lot/unallocated', {}), // 未分配(添加下拉候选)
|
||||
]);
|
||||
```
|
||||
|
||||
**服务端**(`api/strategies.js`)→ `PositionManager.getStrategyPositions(strategyId)`:
|
||||
- 升级前:`getAllPositions()`(QMT REST)+ `storage.getDataset(strategyId)`(整策略读 store.json)
|
||||
- 升级后:`getAllPositions()` + `SqliteStore.getCurrentHoldings(strategyId)`(`SELECT ... WHERE strategy_id=? AND closed_at IS NULL`)
|
||||
|
||||
**存储层**:
|
||||
```sql
|
||||
SELECT code, shares, created_at FROM strategy_holdings
|
||||
WHERE strategy_id = ? AND closed_at IS NULL
|
||||
```
|
||||
|
||||
**返回**:`[{ code, name, volume, shares, ... }]`(对外结构不变,上层 API 无感知)
|
||||
|
||||
---
|
||||
|
||||
## 操作 2:添加持仓(建仓/加仓)
|
||||
|
||||
**触发**:策略 tab 添加表单 → 选标的 + 填份额 → 确认添加 → `handleAdd()`
|
||||
|
||||
**前端调用**(`StrategyTab.jsx`):
|
||||
```js
|
||||
call('one-divine-lot/add-shares', { args: { code, strategyId, shares: Number(addShares) } });
|
||||
```
|
||||
|
||||
**服务端** → `PositionManager.addToStrategy(code, strategyId, shares)`:
|
||||
- 升级前:读 code 份额 map → 校验超限 → `_setShares(code, next)`(**整策略 dataset 重写**)
|
||||
- 升级后:校验逻辑不变(total / 已分配 + 新增 ≤ 总持仓),写入改为**单票生命周期**:
|
||||
- 该策略该 code **无当前持仓** → `SqliteStore.openHolding(strategyId, code, shares)`(INSERT,created_at=now,closed_at=NULL)
|
||||
- **已有当前持仓** → `SqliteStore.addShares(strategyId, code, shares)`(UPDATE shares += ? WHERE closed_at IS NULL)
|
||||
|
||||
**存储层**:
|
||||
```sql
|
||||
-- openHolding(建仓)
|
||||
INSERT INTO strategy_holdings (strategy_id, code, shares, created_at, closed_at)
|
||||
VALUES (?, ?, ?, ?, NULL);
|
||||
|
||||
-- addShares(加仓)
|
||||
UPDATE strategy_holdings SET shares = shares + ?
|
||||
WHERE strategy_id = ? AND code = ? AND closed_at IS NULL;
|
||||
```
|
||||
|
||||
**返回**:`getShares(code)`(`{ strategyId: shares }`,结构不变)→ 前端 `toast.success` + `load()` 刷新
|
||||
|
||||
---
|
||||
|
||||
## 操作 3:移出份额(减仓/清仓)
|
||||
|
||||
**触发**:策略 tab 行内「移出」→ 填数量 → 确认 → `handleRemove()`
|
||||
|
||||
**前端调用**(`StrategyTab.jsx`):
|
||||
```js
|
||||
call('one-divine-lot/remove-shares', { args: { code, strategyId, shares: Number(removeTarget.shares) } });
|
||||
```
|
||||
|
||||
**服务端** → `PositionManager.removeFromStrategy(code, strategyId, shares)`:
|
||||
- 升级前:读份额 → 校验不超移 → `_setShares`(整策略重写;减到 0 则删除该 code 条目)
|
||||
- 升级后:校验不变,写入分两种:
|
||||
- 减后 **> 0** → `SqliteStore.reduceShares(strategyId, code, shares)`(UPDATE shares -= ? WHERE closed_at IS NULL)
|
||||
- 减到 **= 0** → `SqliteStore.closeHolding(strategyId, code)`(UPDATE closed_at=now, shares=0;**转历史,不物理删除**)
|
||||
|
||||
**存储层**:
|
||||
```sql
|
||||
-- reduceShares(减仓,仍持仓)
|
||||
UPDATE strategy_holdings SET shares = shares - ?
|
||||
WHERE strategy_id = ? AND code = ? AND closed_at IS NULL;
|
||||
|
||||
-- closeHolding(清仓,转历史)
|
||||
UPDATE strategy_holdings SET shares = 0, closed_at = ?
|
||||
WHERE strategy_id = ? AND code = ? AND closed_at IS NULL;
|
||||
```
|
||||
|
||||
**行为变化**:清仓数据**保留为历史**(closed_at 非 NULL),不再物理删除 —— 供未来交易记录/复盘关联(R-007)。
|
||||
|
||||
---
|
||||
|
||||
## 操作 4:全部移入(单票级)
|
||||
|
||||
**触发**:策略 tab 行内「全部移入」→ `handleMoveAllIn()`
|
||||
|
||||
**前端调用**(`StrategyTab.jsx`):
|
||||
```js
|
||||
call('one-divine-lot/move-all-shares', { args: { code, strategyId } });
|
||||
```
|
||||
|
||||
**服务端** → `PositionManager.moveAllUnallocatedToStrategy(code, strategyId)`:
|
||||
- 升级前:计算未分配份额 → `_setShares`(整策略重写,`current[strategyId] + unallocated`)
|
||||
- 升级后:等价于「把未分配份额并入该策略当前持仓」→ 走生命周期:
|
||||
- 无当前持仓 → `openHolding(strategyId, code, unallocated)`
|
||||
- 有当前持仓 → `addShares(strategyId, code, unallocated)`
|
||||
|
||||
**存储层**:同操作 2 的 openHolding / addShares(shares = 原份额 + 未分配份额)
|
||||
|
||||
**返回**:`getShares(code)` → `toast.success` + `load()`
|
||||
|
||||
---
|
||||
|
||||
## 操作 5:一键清零(策略级)
|
||||
|
||||
**触发**:策略 tab 顶部「一键清零」→ 弹窗确认 → `handleClearAll()`
|
||||
|
||||
**前端调用**(`StrategyTab.jsx`):
|
||||
```js
|
||||
call('one-divine-lot/remove-all-shares', { args: { strategyId } });
|
||||
```
|
||||
|
||||
**服务端** → `PositionManager.clearStrategyShares(strategyId)`:
|
||||
- 升级前:`storage.getDataset(strategyId)`(读整策略)→ `storage.removeDataset(strategyId)`(**物理删除整策略 dataset**)
|
||||
- 升级后:**批量 closeHolding** —— 该策略所有当前持仓转历史(不物理删除):
|
||||
- 读 `getCurrentHoldings(strategyId)` 得到受影响 code 列表
|
||||
- 逐笔 `closeHolding(strategyId, code)`
|
||||
- 返回受影响 code 列表(对外结构不变)
|
||||
|
||||
**存储层**:
|
||||
```sql
|
||||
UPDATE strategy_holdings SET shares = 0, closed_at = ?
|
||||
WHERE strategy_id = ? AND closed_at IS NULL;
|
||||
```
|
||||
|
||||
**返回**:`string[]`(受影响标的 code 列表)→ 前端提示「已清空策略份额(N 只标的回到未分配)」+ `load()`
|
||||
|
||||
---
|
||||
|
||||
## 操作 6:删除策略(设置页联动清份额)
|
||||
|
||||
**触发**:设置页「策略分组」→ 删除策略 → 弹窗确认 → `handleDelete()`
|
||||
|
||||
**前端调用**(`SettingsSection.jsx`):
|
||||
```js
|
||||
call('one-divine-lot/strategies/remove', { args: { strategyId: id } });
|
||||
```
|
||||
|
||||
**服务端**(`api/strategies.js`):
|
||||
```js
|
||||
await removeStrategy(settings, args.strategyId); // settings 删除策略定义
|
||||
await manager.clearStrategyShares(args.strategyId); // 联动清份额
|
||||
return getStrategies(settings);
|
||||
```
|
||||
|
||||
**升级后**:`clearStrategyShares` 同样走**批量 closeHolding** —— 该策略持仓全部转历史(closed_at=now),**历史保留**(供交易记录关联),settings 中策略定义删除(策略 id 从配置消失,但 SQLite 中历史行仍带 strategy_id,可追溯)。
|
||||
|
||||
**存储层**:同操作 5 的批量 closeHolding。
|
||||
|
||||
---
|
||||
|
||||
## 操作 7:行情现价展示(读缓存)
|
||||
|
||||
**触发**:任意持仓表格渲染现价列 → `useMarket().getPrice(code)`
|
||||
|
||||
**前端调用**(`MarketDataProvider` 轮询):
|
||||
```js
|
||||
// 表格上报 code → Provider 调行情接口(去重,不再自拉 positions)
|
||||
registerCodes(codes);
|
||||
// 周期轮询
|
||||
call('one-divine-lot/market-snapshot', { args: { codes } });
|
||||
```
|
||||
|
||||
**服务端**(`api/market.js`)→ `MarketDataHub.getByCodes()`:
|
||||
- 内存缓存 → 磁盘缓存 → REST 补拉(三级命中)
|
||||
- 升级后:磁盘缓存由 `store.market.json` 改为 `SqliteStore.getMarketQuotes(codes)`(读 market_quotes_cache 表)
|
||||
|
||||
**存储层**:
|
||||
```sql
|
||||
SELECT code, last_price, last_close FROM market_quotes_cache WHERE code IN (...);
|
||||
```
|
||||
|
||||
**返回**:`{ code: { lastPrice, lastClose } }` → PriceCell 渲染现价 + 红涨绿跌着色
|
||||
|
||||
---
|
||||
|
||||
## 操作 8:行情写回缓存(服务端后台)
|
||||
|
||||
**触发**:MarketDataHub.ingest() 防抖 3s 写回(非用户直接操作)
|
||||
|
||||
**前端**:无直接调用(服务端后台行为)
|
||||
|
||||
**服务端**:`MarketDataHub` → 升级后 `SqliteStore.setMarketQuotes(quotes)`:
|
||||
```sql
|
||||
INSERT INTO market_quotes_cache (code, last_price, last_close, updated_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT(code) DO UPDATE SET
|
||||
last_price = excluded.last_price,
|
||||
last_close = excluded.last_close,
|
||||
updated_at = excluded.updated_at;
|
||||
```
|
||||
|
||||
**说明**:只落盘 lastPrice/lastClose 两列(UI 消费的现价 + 涨跌基准);盘口五档等仅内存缓存,不落盘(方案明确)。
|
||||
|
||||
---
|
||||
|
||||
## 存储层接口映射总表
|
||||
|
||||
| 前端操作 | /odl/api 端点 | PositionManager 方法 | SqliteStore 方法(升级) | 原 DataStore 方法(废弃) |
|
||||
|---|---|---|---|---|
|
||||
| 策略 tab 加载 | strategy-positions | getStrategyPositions | **getCurrentHoldings** | getDataset |
|
||||
| 添加(无持仓) | add-shares | addToStrategy | **openHolding** | setDataset(整策略重写) |
|
||||
| 添加(有持仓) | add-shares | addToStrategy | **addShares** | setDataset |
|
||||
| 移出(>0) | remove-shares | removeFromStrategy | **reduceShares** | setDataset |
|
||||
| 移出(=0) | remove-shares | removeFromStrategy | **closeHolding** | setDataset(删条目) |
|
||||
| 全部移入 | move-all-shares | moveAllUnallocatedToStrategy | openHolding / addShares | setDataset |
|
||||
| 一键清零 | remove-all-shares | clearStrategyShares | **批量 closeHolding** | removeDataset(物理删) |
|
||||
| 删除策略 | strategies/remove | removeStrategy + clearStrategyShares | 批量 closeHolding | removeDataset |
|
||||
| 行情读取 | market-snapshot | MarketDataHub.getByCodes | **getMarketQuotes** | store.market.json 读 |
|
||||
| 行情写回 | (后台) | MarketDataHub.ingest | **setMarketQuotes** | store.market.json 写 |
|
||||
|
||||
## 对前端的结论
|
||||
|
||||
1. **前端零改动**:所有操作仍走 /odl/api/* 端点,`useRpc` 封装、请求/响应结构不变;
|
||||
2. **行为增强(前端无感)**:清仓/一键清零/删除策略后数据**转历史保留**(closed_at),而非物理删除 —— 为 R-007 交易记录关联与复盘铺路;
|
||||
3. **行情落盘降维**:只落 lastPrice/lastClose 两列(响应无需关心,服务端投影);
|
||||
4. **唯一可见差异**:历史持仓数据存在(未来若做「持仓历史」UI,getHoldingHistory 可直接支撑)。
|
||||
@@ -0,0 +1,132 @@
|
||||
# 技术实现方案:06-数据存储SQLite(JSON → SQLite)
|
||||
|
||||
> 依据:PLAN-007 | 需求:R-008 | 设计约束:技术约束-012(变更)、数据存储设计.md(第 9 节变更落实)、技术约束-011(测试隔离)
|
||||
|
||||
## 技术选型
|
||||
|
||||
- **存储引擎**:node:sqlite(Node ≥22.5 内置,DatabaseSync 同步 API)——零依赖分发(技术约束-005 不受影响),experimental 风险由存储层封装隔离(D2);
|
||||
- **封装**:新增 SqliteStore 模块(init/事务/读写/close),对外暴露与 DataStore 兼容的行为,未来可切换;
|
||||
- **迁移**:一次性迁移脚本(scripts/migrate-json-to-sqlite.mjs)+ DataStore 启动检测自动迁移(旧 JSON 存在且 SQLite 空 → 自动迁移,幂等);
|
||||
- **表结构**:strategy_holdings + market_quotes_cache 两表(R-008 讨论修正:① 策略持仓为**持仓生命周期表**(自增 holding_id + created_at/closed_at),不用 JSON 列压扁;② 行情快照只存 last_price/last_close 两个具体列,表名 market_quotes_cache 明确缓存语义;不建 trades,R-007 时再建)。
|
||||
|
||||
## 表结构设计(落实数据存储设计.md 第 9 节)
|
||||
|
||||
```sql
|
||||
-- 策略持仓生命周期(一笔 = 一次「建仓→清仓」的完整持仓;历史保留,供交易记录关联)
|
||||
CREATE TABLE IF NOT EXISTS strategy_holdings (
|
||||
holding_id INTEGER PRIMARY KEY AUTOINCREMENT, -- 自增持仓编号(交易记录关联锚点;实测删除不复用)
|
||||
strategy_id TEXT NOT NULL, -- 对应 settings 中策略的 id(如 grid-supermarket,非 name)
|
||||
code TEXT NOT NULL, -- 证券代码(含后缀,如 600719.SH)
|
||||
shares REAL NOT NULL, -- 当前份额(股数,>0)
|
||||
created_at INTEGER NOT NULL, -- 建仓时间(毫秒时间戳)
|
||||
closed_at INTEGER, -- 清仓时间(NULL=当前持仓;非 NULL=已清仓历史)
|
||||
PRIMARY KEY (holding_id)
|
||||
);
|
||||
-- 业务约束:同一策略同一股票只允许一笔「当前持仓」(closed_at IS NULL 唯一)
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_active_holding
|
||||
ON strategy_holdings (strategy_id, code) WHERE closed_at IS NULL;
|
||||
|
||||
-- 行情快照缓存(code 维度一行一码;只存 UI 消费的现价 + 昨收,无 JSON 列;cache 语义=重启首屏有价)
|
||||
CREATE TABLE IF NOT EXISTS market_quotes_cache (
|
||||
code TEXT PRIMARY KEY, -- 证券代码(含后缀,如 600719.SH)
|
||||
last_price REAL NOT NULL, -- 最新价(UI 现价)
|
||||
last_close REAL NOT NULL, -- 昨收(UI 涨跌着色对比基准)
|
||||
updated_at INTEGER NOT NULL DEFAULT 0 -- 写入时间戳(毫秒)
|
||||
);
|
||||
```
|
||||
|
||||
**数据示例**(strategy_holdings,真实数据迁移后):
|
||||
|
||||
| holding_id | strategy_id | code | shares | created_at | closed_at |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | grid-supermarket | 601117.SH | 600 | 迁移时间戳 | NULL |
|
||||
| 2 | grid-supermarket | 002129.SZ | 200 | 迁移时间戳 | NULL |
|
||||
| 3 | grid-supermarket | 300057.SZ | 1000 | 迁移时间戳 | NULL |
|
||||
| ... | ... | ... | ... | ... | ... |
|
||||
| 8 | manual-t | 600719.SH | 1000 | 迁移时间戳 | NULL |
|
||||
| 9 | manual-t | 300426.SZ | 800 | 迁移时间戳 | NULL |
|
||||
| 10 | manual-t | 600719.SH | 0 | 迁移时间戳 | 1788xxx(清仓后) |
|
||||
|
||||
**schema 语义对齐**(store.schema.json → SQLite):
|
||||
- holding_id 自增(SQLite AUTOINCREMENT 实测删除不复用),稳定唯一,作为未来交易记录(trades)的关联外键;
|
||||
- strategy_holdings.strategy_id = settings 策略的 id(英文 slug,稳定唯一标识;改名只改 name 不影响数据);
|
||||
- created_at=建仓时间;closed_at=清仓时间(NULL=当前持仓);部分唯一索引保证同策略同股票仅一笔当前持仓;
|
||||
- market_quotes_cache 对齐 store.market.json 的 quotes 对象,但只抽取 lastPrice/lastClose 两个具体列入库(无 JSON 列);盘口五档等实时字段仅存内存缓存,不落盘。
|
||||
|
||||
## 架构设计
|
||||
|
||||
```
|
||||
SqliteStore(新增,node:sqlite 封装)
|
||||
├─ init() : 建库建表(CREATE TABLE IF NOT EXISTS)
|
||||
├─ migrateJson() : 旧 JSON → SQLite(迁移前备份,幂等)
|
||||
├─ 持仓生命周期 : openHolding/addShares/reduceShares/closeHolding
|
||||
├─ 持仓查询 : getCurrentHoldings/getHoldingHistory
|
||||
├─ 行情读写 : getMarketQuote/getMarketQuotes/setMarketQuotes
|
||||
└─ close() : 关闭连接(插件释放时)
|
||||
|
||||
DataStore(改造,对外 API 升级为持仓生命周期语义)
|
||||
└─ 内部委托 SqliteStore(PositionManager 按单票操作,不再整策略重写)
|
||||
|
||||
MarketDataHub(改造,持久化路径不变)
|
||||
└─ setMarketQuotes → SqliteStore.setMarketQuotes(防抖写回)
|
||||
```
|
||||
|
||||
**存储层方法集(替代原 getDataset/setDataset/removeDataset/getAllDatasets 整体读写)**:
|
||||
|
||||
| 方法 | 是什么 | 什么时候用 | SQL 要点 |
|
||||
|---|---|---|---|
|
||||
| openHolding(strategyId, code, shares) | 建仓:创建一笔新持仓 | 某策略首次分配某股票(原「从 0 添加」) | INSERT(holding_id 自增,created_at=now,closed_at=NULL) |
|
||||
| addShares(strategyId, code, shares) | 加仓:当前持仓份额累加 | 原 addToStrategy 的「该策略已有该票」分支 | UPDATE shares=shares+? WHERE closed_at IS NULL |
|
||||
| reduceShares(strategyId, code, shares) | 减仓:当前持仓份额减少 | 原 removeFromStrategy | UPDATE shares=shares-? WHERE closed_at IS NULL |
|
||||
| closeHolding(strategyId, code) | 清仓:份额归零,closed_at=now 转历史 | 原 removeFromStrategy 减到 0 / 份额清零 / 删除策略联动 | UPDATE closed_at=now WHERE closed_at IS NULL(shares 置 0) |
|
||||
| getCurrentHoldings(strategyId?) | 查当前持仓(含 code/shares/created_at) | 策略 tab 渲染 / 汇总 / 单票查询 | SELECT ... WHERE closed_at IS NULL(可加 strategy_id 过滤) |
|
||||
| getHoldingHistory(code?) | 查历史(含 closed_at,当前+历史) | 未来交易记录关联 / 复盘 | SELECT ...(可加 code 过滤) |
|
||||
|
||||
**不再保留**:removeDataset(物理删除整策略)、setDataset(整策略整体重写)——删除策略/一键清零改为 closeHolding 批量软关闭(历史保留,可关联交易记录)。
|
||||
|
||||
**PositionManager 适配**:
|
||||
- _setShares(内部整策略重写)改为按单票调 openHolding/addShares/reduceShares/closeHolding;
|
||||
- addToStrategy:无当前持仓 → openHolding;有 → addShares;
|
||||
- removeFromStrategy:减后>0 → reduceShares;减到0 → closeHolding;
|
||||
- clearStrategyShares / strategies/remove:批量 closeHolding(转历史,不物理删除);
|
||||
- getDataset/getAllDatasets 调用点改 getCurrentHoldings(对外返回 [{code, shares}] 结构不变,上层 API 无感知)。
|
||||
|
||||
## 迁移策略(D6)
|
||||
|
||||
1. **启动检测**:DataStore.load()/loadMarket() 时检测 —— 旧 JSON 文件存在 且 SQLite 空(strategy_holdings / market_quotes_cache 无数据)→ 触发自动迁移;
|
||||
2. **迁移动作**:
|
||||
- store.json → strategy_holdings 表(strategies[].dataset 平铺为行:strategy_id + code + shares,created_at=迁移时间戳,closed_at=NULL);
|
||||
- store.market.json → market_quotes_cache 表(抽取每条快照的 lastPrice/lastClose → last_price/last_close 两列);
|
||||
- 旧 allocations.json(若仍存在,R-006 遗留迁移源,code 为中心)→ strategy_holdings 表(聚合转换,created_at=迁移时间戳);
|
||||
- 迁移前将 JSON 文件备份为 *.json.bak(D7);
|
||||
3. **幂等**:SQLite 有数据即跳过迁移;迁移失败不破坏原 JSON(先读后写,写库失败不删原文件);
|
||||
4. **一次性脚本**:scripts/migrate-json-to-sqlite.mjs(独立执行,供手动/CI 使用,逻辑与启动自动迁移共用)。
|
||||
|
||||
## 涉及设计约束
|
||||
|
||||
| 约束 | 内容 |
|
||||
|---|---|
|
||||
| 技术约束-012 | 数据存储遵循数据存储设计.md:SQLite 升级后更新为遵循 SQLite 存储设计(本迭代执行变更) |
|
||||
| 技术约束-011 | 测试/回归脚本禁止在真实数据上执行写操作:ODL_TEST_DATA_DIR 独立数据目录 |
|
||||
| 技术约束-005 | 客户端 bundle 不受影响(node:sqlite 为 Node 内置,服务端使用) |
|
||||
|
||||
## 变更记录(实现期间)
|
||||
|
||||
- **2026-09-01 移除「一键清零」按钮**(老师决定):
|
||||
- 前端:策略 tab 顶部「一键清零」按钮 + 确认弹窗 + handleClearAll 移除(StrategyTab.jsx);
|
||||
- 后端:`remove-all-shares` 端点移除(api/strategies.js、api/index.js 注释);
|
||||
- 保留:`clearStrategyShares` 方法(`strategies/remove` 删除策略仍联动清份额);
|
||||
- 理由:该操作粒度尴尬(误触风险高、单票移出+删除策略已覆盖需求),实际使用价值低;
|
||||
- 影响:前端 UI 精简;API 面收缩(旧客户端若调用 remove-all-shares 将 404,本次同版本更新无兼容问题)。
|
||||
|
||||
## 实现步骤
|
||||
|
||||
1. 文档骨架:PLAN-007 + 迭代 06(本步);
|
||||
2. 约束变更:技术约束-012 更新 + 数据存储设计.md 第 9 节落实;
|
||||
3. SqliteStore:node:sqlite 封装(init/迁移/持仓生命周期/行情读写/close);
|
||||
4. DataStore 改造:持仓读写切换 SqliteStore(openHolding/addShares/reduceShares/closeHolding/getCurrentHoldings/getHoldingHistory),保留迁移检测;
|
||||
5. PositionManager 适配:整策略重写 → 单票生命周期操作;
|
||||
6. 迁移脚本:一次性迁移脚本 + 启动自动迁移(幂等);
|
||||
7. MarketDataHub 适配:行情持久化切至 market_quotes_cache 表(落盘只投影 last_price/last_close 两列);
|
||||
8. 构建测试:pnpm run build + typecheck + 独立数据目录回归测试(技术约束-011);
|
||||
9. 验收 + 复盘。
|
||||
@@ -0,0 +1,42 @@
|
||||
# 迭代复盘:06-数据存储SQLite(JSON → SQLite)
|
||||
|
||||
> 复盘日期:2026-09-01 | 迭代状态:**已完成(验收通过)**
|
||||
> 关联需求:R-008(数据存储管理 JSON → SQLite,已定稿)
|
||||
> 关联计划:PLAN-007(数据存储 SQLite 化)
|
||||
|
||||
## 结果
|
||||
|
||||
迭代 06 达成:数据存储从 JSON data store(store.json / store.market.json / store.schema.json)升级为 SQLite(store.db,node:sqlite),仅存储引擎替换、对外行为不变。真实数据迁移完成(15 条持仓 + 13 条行情),JSON 清理废弃,份额/行情/设置功能全部不回归。
|
||||
|
||||
## 过程事实
|
||||
|
||||
1. **技术选型验证**:node:sqlite(Node 22.23.1 内置,DatabaseSync,SQLite 3.51.3)实测可用;better-sqlite3 需原生编译 → 选 node:sqlite(零依赖分发,experimental 风险由 SqliteStore 封装隔离);
|
||||
2. **表结构**:strategy_holdings(持仓生命周期表:holding_id 自增 + strategy_id/code/shares/created_at/closed_at + 部分唯一索引)+ market_quotes_cache(code 主键 + last_price/last_close/updated_at 两列);不建 trades(R-007 再建);
|
||||
3. **SqliteStore 封装**:init/迁移/持仓生命周期(openHolding/addShares/reduceShares/closeHolding)/行情读写/close;
|
||||
4. **DataStore 改造**:门面委托 SqliteStore,保留兼容 API(getDataset/getAllDatasets/行情读写),启动检测自动迁移(幂等);
|
||||
5. **PositionManager 适配**:整策略 dataset 重写(_setShares)→ 单票生命周期(_applySharesForStrategy 绝对目标语义);
|
||||
6. **迁移脚本**:scripts/migrate-json-to-sqlite.mjs + pnpm migrate 命令(一次性 + 启动自动迁移共用逻辑);
|
||||
7. **真实迁移**:15 持仓 + 13 行情迁入 SQLite,与迁移前逐条一致;JSON 备份 .bak 后清理删除(仅留 store.db);
|
||||
8. **移除「一键清零」按钮**(老师决定):前端按钮/弹窗 + 后端 remove-all-shares 端点移除;clearStrategyShares 保留(删除策略联动)。
|
||||
|
||||
## 经验教训(复盘沉淀)
|
||||
|
||||
### 1. 语义迁移必须逐方法核对(重要)
|
||||
- **教训**:改造 PositionManager 时,把 addToStrategy 的「新增量」误当「绝对目标份额」传给生命周期方法,导致加仓 100 变 100 —— 真实 API 验证时发现(600719.SH 手动做T 少了 100 股),已修复并回滚数据;
|
||||
- **沉淀**:方法语义变更(增量 vs 绝对量)必须显式命名(_applySharesForStrategy 接收 target 绝对目标),调用方逐处核对;改造后必须用真实 API 走一遍 CRUD 回归,不能只靠隔离单测。
|
||||
|
||||
### 2. node:sqlite 的锁行为
|
||||
- 插件进程持有 SQLite 连接时,外部脚本再开连接写库会报 `attempt to write a readonly database`;
|
||||
- **沉淀**:真实数据修正必须走插件自己的 API(或停机后操作),外部脚本只能读;测试写操作用 ODL_TEST_DATA_DIR 隔离(技术约束-011)。
|
||||
|
||||
### 3. 关闭连接
|
||||
- 插件 dispose 时必须 storage.close()(关闭 SQLite 连接),避免资源泄漏。
|
||||
|
||||
### 4. 文档与实际必须一致
|
||||
- 验收标准初稿写 `one-divine-lot.db`,实际实现用 `store.db` —— 归档前已修正。
|
||||
|
||||
## 遗留/后续
|
||||
|
||||
1. **盘中行情写 SQLite 验证**:本次验收时已收盘,无实时推送;开盘后应确认行情防抖写回 store.db(观察 updated_at / 文件 mtime);
|
||||
2. **R-007 交易记录关联**:strategy_holdings.holding_id 已就绪,可作为 trades 表关联锚点(R-007 实现时建 trades 表);
|
||||
3. **SqliteStore 迁移逻辑引用旧文件名**:migrateJson 仍检查 store.json 等(幂等跳过),保留用于新环境初始化。
|
||||
@@ -0,0 +1,27 @@
|
||||
# 迭代目标:06-数据存储SQLite(JSON → SQLite 存储引擎替换)
|
||||
|
||||
## 目标
|
||||
|
||||
将神之一手的数据存储从 JSON data store(store.json / store.market.json / store.schema.json)升级为 **SQLite 数据库**(node:sqlite),**仅存储引擎替换、对外行为不变**:引入 strategy_holdings(持仓生命周期表)+ market_quotes_cache 两表,存储层升级为单票持仓生命周期(openHolding/addShares/reduceShares/closeHolding),策略定义仍存 DSH settings,一次性迁移 + 启动自动迁移(幂等),JSON 迁移后废弃(迁移前自动备份)。
|
||||
|
||||
## 目标描述
|
||||
|
||||
- 范围:R-008(已定稿 2026-09-01,D1-D8 全部确认);不新增其他功能域;
|
||||
- 要解决的问题:JSON 全量读写无法支撑复杂查询/筛选(按 code/时间/策略),且未来交易记录/复盘等多数据集统一存储需要数据库底座(目标-003/005/006);
|
||||
- 引用需求:**R-008(已定稿,2026-09-01)**;
|
||||
- 实现方式:node:sqlite(Node ≥22.5,实测 22.23.1 可用)+ SqliteStore 封装隔离 experimental 风险 + 启动自动迁移(幂等)+ JSON 废弃(D1-D8);
|
||||
- 关联设计约束:技术约束-012(变更)、数据存储设计.md(第 9 节变更预告落实)。
|
||||
|
||||
## 目标讨论过程
|
||||
|
||||
- 2026-09-01 老师提出需求:准备上 SQLite,现在基于 JSON 文件(T-008 入池);
|
||||
- 2026-09-01 第一轮讨论定稿(D1-D8):动机=复杂查询+未来铺路;node:sqlite(实测可用);仅引擎替换;strategy_holdings+market_quotes_cache 两表(持仓生命周期表 + 行情极简两列缓存表);存储层单票生命周期操作;策略仍存 settings;一次性迁移脚本+启动自动迁移;JSON 迁移后废弃;P1;
|
||||
- 2026-09-01 实测验证:本机 Node v22.23.1,node:sqlite 可用(DatabaseSync,内置 SQLite 3.51.3),标记 Experimental;better-sqlite3 需编译(倾向稳妥+零依赖分发 → 选 node:sqlite);
|
||||
- 2026-09-01 三要素核对通过,转正为 R-008,进入迭代 06。
|
||||
|
||||
## 对老师的配合需求
|
||||
|
||||
- 确认迭代 06 计划与验收标准(PLAN-007);
|
||||
- 构建安装后重启 DSH 配合验证(首次启动自动迁移真实数据);
|
||||
- 验证迁移结果:store.json/store.market.json 数据正确落 SQLite(备份文件留存),份额分配/行情显示不回归;
|
||||
- 验证持仓生命周期:加仓/减仓/清仓操作后当前持仓与历史(holding_id/created_at/closed_at)正确,删除策略/一键清零后历史保留。
|
||||
@@ -0,0 +1,28 @@
|
||||
# 验收标准:06-数据存储SQLite(JSON → SQLite)
|
||||
|
||||
## 验收标准线
|
||||
|
||||
1. **存储介质切换**:数据落 SQLite(store.db),不再生成/更新 store.json / store.market.json / store.schema.json(迁移前备份除外);
|
||||
2. **两表结构**:strategy_holdings + market_quotes_cache 两表存在且结构符合技术实现方案(strategy_holdings 持仓生命周期表:holding_id 自增 + strategy_id/code/shares/created_at/closed_at;market_quotes_cache 仅 last_price/last_close 两列,无 JSON 列);
|
||||
3. **持仓生命周期正确**:openHolding/addShares/reduceShares/closeHolding 操作正确 —— 建仓生成 holding_id+created_at、加/减仓更新当前持仓 shares、清仓 closed_at 置位转历史;同策略同股票仅一笔当前持仓(部分唯一索引);删除策略/一键清零改为软关闭(历史保留);
|
||||
4. **行情不回归**:行情缓存查询/写回不变 —— 启动首屏有价(从 SQLite 加载)、盘中防抖写回、三级命中(内存→SQLite→REST 补拉)行为一致;
|
||||
5. **自动迁移正确**:旧 JSON(store.json / store.market.json / 旧 allocations.json 若存在)→ SQLite 数据一致(份额逐条核对、行情快照逐条核对),迁移前备份文件(*.json.bak)留存;
|
||||
6. **幂等**:重复启动不重复迁移(SQLite 有数据即跳过);迁移失败不破坏原 JSON;
|
||||
7. **JSON 废弃**:迁移成功后 store.json / store.market.json 不再读写(旧文件保留为备份,新数据只写 SQLite);
|
||||
8. **策略定义位置**:策略(id/name/visible/order)仍存 DSH settings,设置页交互不变;
|
||||
9. **不回归**:持仓/策略/行情/连接配置/交易记录等现有功能正常;
|
||||
10. **测试隔离**:回归测试在独立数据目录(ODL_TEST_DATA_DIR)执行,真实数据目录无写操作(技术约束-011)。
|
||||
|
||||
## 验收方法
|
||||
|
||||
- 独立数据目录(ODL_TEST_DATA_DIR)构造旧 JSON 数据(store.json / store.market.json)→ 启动插件 → 确认自动迁移完成、SQLite 数据与 JSON 一致、备份文件留存;
|
||||
- 反复重启确认幂等(不重复迁移、数据不丢);
|
||||
- 通过 API 验证份额 CRUD 与行情读写行为与改造前一致(对照迭代 04/05 回归);
|
||||
- 手动核对真实数据目录:迁移后 store.json / store.market.json 不再更新,store.db 为数据源;
|
||||
- 回归:全部持仓 / 策略 / 行情 / QMT 连接配置 / 交易记录正常。
|
||||
|
||||
## 验收目标
|
||||
|
||||
- 存储引擎替换闭环:JSON → SQLite(迁移正确 + 幂等 + 备份),对外行为零变化;
|
||||
- 为复杂查询/多数据集统一存储铺路(strategy_holdings 持仓生命周期表 + market_quotes_cache 就绪,holding_id 可作为 trades 关联锚点,trades 表 R-007 再建);
|
||||
- 设计约束同步落地:技术约束-012 与数据存储设计.md 更新为 SQLite 设计。
|
||||
@@ -0,0 +1,120 @@
|
||||
# 技术实现方案:07-交易记录本地存储(SQLite)+ 策略关联
|
||||
|
||||
> 依据:PLAN-008 | 需求:R-009(Q1-Q8 定稿)| 设计约束:技术约束-012(数据存储设计)、技术约束-011(测试隔离)
|
||||
|
||||
## 技术选型
|
||||
|
||||
- **存储**:沿用 SqliteStore(node:sqlite)——新增 trade_orders + trade_fills 两表(R-009 Q1);
|
||||
- **同步**:服务端 TradeSync 模块(定时 60s + 启动预热 + 前端今日轮询写穿;UPSERT 幂等);
|
||||
- **关联**:外键链推导(Q3 老师定稿):成交→委托(order_id 外键)→策略/holding(委托时间 join strategy_holdings 生命周期窗口)——两表零冗余 strategy_id/holding_id;
|
||||
- **历史查询**:api/trades.js 新增 trades/history 端点(查本地 SQLite);
|
||||
- **前端**:TradeRecordsTab 加策略过滤 + 历史范围查本地。
|
||||
|
||||
## 表结构(数据存储设计.md §10 落实)
|
||||
|
||||
```sql
|
||||
-- 交易委托(委托主行;order_id 唯一,UPSERT 幂等;零冗余 strategy_id/holding_id)
|
||||
CREATE TABLE IF NOT EXISTS trade_orders (
|
||||
order_id TEXT PRIMARY KEY, -- m_strOrderSysID
|
||||
trade_date TEXT NOT NULL, -- 交易日 YYYYMMDD(m_strInsertDate)
|
||||
code TEXT NOT NULL,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
exchange TEXT NOT NULL DEFAULT '',
|
||||
direction TEXT NOT NULL DEFAULT '', -- buy / sell
|
||||
direction_code INTEGER,
|
||||
opt_name TEXT NOT NULL DEFAULT '',
|
||||
status INTEGER,
|
||||
order_volume REAL NOT NULL DEFAULT 0,
|
||||
traded_volume REAL NOT NULL DEFAULT 0,
|
||||
limit_price REAL NOT NULL DEFAULT 0,
|
||||
traded_price REAL NOT NULL DEFAULT 0,
|
||||
amount REAL NOT NULL DEFAULT 0,
|
||||
insert_date TEXT NOT NULL DEFAULT '',
|
||||
insert_time TEXT NOT NULL DEFAULT '',
|
||||
insert_ts INTEGER NOT NULL, -- 派生:insert_date+insert_time 合成毫秒时间戳(join 持仓窗口用)
|
||||
cancel_info TEXT NOT NULL DEFAULT '',
|
||||
error_msg TEXT NOT NULL DEFAULT '',
|
||||
fetched_at INTEGER NOT NULL -- 同步时间戳
|
||||
);
|
||||
|
||||
-- 交易成交(trade_id 唯一,order_id 外键关联委托;零冗余 strategy_id/holding_id)
|
||||
CREATE TABLE IF NOT EXISTS trade_fills (
|
||||
trade_id TEXT PRIMARY KEY, -- m_strTradeID
|
||||
order_id TEXT NOT NULL, -- → trade_orders.order_id
|
||||
trade_date TEXT NOT NULL,
|
||||
code TEXT NOT NULL,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
exchange TEXT NOT NULL DEFAULT '',
|
||||
direction TEXT NOT NULL DEFAULT '',
|
||||
direction_code INTEGER,
|
||||
opt_name TEXT NOT NULL DEFAULT '',
|
||||
price REAL NOT NULL DEFAULT 0,
|
||||
volume REAL NOT NULL DEFAULT 0,
|
||||
amount REAL NOT NULL DEFAULT 0,
|
||||
commission_rate_wan REAL NOT NULL DEFAULT 0,
|
||||
trade_time TEXT NOT NULL DEFAULT '',
|
||||
fetched_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_trade_orders_date ON trade_orders (trade_date);
|
||||
CREATE INDEX IF NOT EXISTS idx_trade_orders_ts ON trade_orders (insert_ts);
|
||||
CREATE INDEX IF NOT EXISTS idx_trade_fills_order ON trade_fills (order_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_trade_fills_date ON trade_fills (trade_date);
|
||||
```
|
||||
|
||||
**策略归属推导(查询期,FK 链)**:
|
||||
|
||||
- 委托归属:`trade_orders JOIN strategy_holdings ON o.code=h.code AND h.created_at <= o.insert_ts AND (h.closed_at IS NULL OR o.insert_ts < h.closed_at)`;
|
||||
- 一码多策略消歧:命中多笔持仓时取 shares 最大者(确定性规则);
|
||||
- 成交归属 = 所属委托归属(fill → order → holding → strategy);
|
||||
- 无命中 → 未关联(strategy_id 返回 null)。
|
||||
|
||||
## 架构设计
|
||||
|
||||
```
|
||||
SqliteStore(改)
|
||||
├─ init() 建表:+trade_orders/trade_fills + 索引
|
||||
├─ upsertOrders(orders) / upsertFills(fills) # UPSERT 幂等
|
||||
├─ getOrderHistory({start,end,code,strategyId,direction}) # 委托 + 策略归属推导
|
||||
├─ getFillHistory({start,end,code,strategyId,direction}) # 成交(按委托归属 join)
|
||||
└─ close()
|
||||
|
||||
DataStore(改)
|
||||
└─ 委托交易记录方法(upsertTradeOrders/upsertTradeFills/queryTradeHistory)
|
||||
|
||||
TradeSync(新增)
|
||||
├─ start() : 启动预热同步一次 + 60s 定时
|
||||
├─ syncNow() : 拉今日 orders+trades → upsertOrders/upsertFills(幂等)
|
||||
└─ stop() : 清理定时器
|
||||
|
||||
api/trades.js(改)
|
||||
└─ trades/history → storage.queryTradeHistory(时间段/code/策略/方向过滤)
|
||||
|
||||
index.js(改)
|
||||
└─ 装配 TradeSync(dispose 停止)+ storage 注入 api runtime
|
||||
|
||||
client/views/TradeRecordsTab.jsx(改)
|
||||
├─ 策略过滤下拉(全部/各策略/未关联)——今日实时过滤前端、历史走服务端过滤
|
||||
└─ 历史范围:调 trades/history 查本地库(不再占位)
|
||||
```
|
||||
|
||||
## 实现步骤
|
||||
|
||||
1. **文档骨架**:PLAN-008 + 迭代 07(本步);
|
||||
2. **设计约束**:数据存储设计.md §10 + 技术方案约束新增条目;
|
||||
3. **SqliteStore**:建表 + upsertOrders/upsertFills + getOrderHistory/getFillHistory(策略归属 join);
|
||||
4. **DataStore**:委托交易记录方法;
|
||||
5. **TradeSync**:定时同步(60s + 启动预热 + UPSERT 幂等);
|
||||
6. **api/trades.js**:trades/history 端点;
|
||||
7. **index.js**:装配 TradeSync + storage 注入 runtime;
|
||||
8. **前端**:TradeRecordsTab 策略过滤 + 历史本地展示;
|
||||
9. **构建测试**:pnpm run build + typecheck + 独立数据目录回归(技术约束-011);
|
||||
10. **验收 + 复盘**。
|
||||
|
||||
## 涉及设计约束
|
||||
|
||||
| 约束 | 内容 |
|
||||
|---|---|
|
||||
| 技术约束-012 | 数据存储遵循数据存储设计.md(本迭代增补 §10 交易记录表设计) |
|
||||
| 技术约束-011 | 测试/回归脚本禁止在真实数据上写操作:ODL_TEST_DATA_DIR 独立数据目录 |
|
||||
| 技术约束-001/003/004/010 | 数据源抽象 / 直连 REST / webServer 自开路由 / 服务端中转+前端轮询(沿用) |
|
||||
| 产品约束-007 | 方向语义红买绿卖(沿用 R-007) |
|
||||
@@ -0,0 +1,61 @@
|
||||
# 迭代复盘:07-交易记录本地存储(SQLite)+ 策略关联
|
||||
|
||||
> 复盘日期:2026-09-01 | 迭代状态:**已完成(验收通过)**
|
||||
> 关联需求:R-009(交易记录本地存储 + 策略关联,已定稿)
|
||||
> 关联计划:PLAN-008(计划-交易记录本地存储SQLite与策略关联)
|
||||
|
||||
## 结果
|
||||
|
||||
迭代 07 达成:QMT 当日交易数据(委托 + 成交)本地持久化到 SQLite(trade_orders + trade_fills 两表,跨日积累成历史库),按外键链(成交→委托→策略→holding)与策略体系关联,支持按策略过滤 / 复盘交易;交易记录 tab 历史范围从「接口开发中」占位切换为本地历史查询。
|
||||
|
||||
## 过程事实
|
||||
|
||||
1. **需求定稿(R-009)**:老师提出 → AI 登记(讨论中)提出 Q1-Q8 → 老师修正关联方式(外键链 + 两表零冗余)→ 确认定稿进入迭代 07;
|
||||
2. **表结构**:trade_orders(委托主行,order_id 主键 + insert_ts 派生时间列)+ trade_fills(成交明细,trade_id 主键、order_id 关联委托)两表;**零冗余 strategy_id/holding_id**(Q3 老师定稿);
|
||||
3. **策略归属推导(FK 链)**:查询期委托时间(insert_ts)join strategy_holdings 生命周期窗口(created_at ≤ t < closed_at)推导;一码多策略取份额最大;未命中=未关联;attachStrategyAttribution 供今日实时委托附加归属;
|
||||
4. **TradeSync 同步**:启动预热 + 60s 定时拉当日 orders+trades → UPSERT 落库(幂等,状态覆盖更新);前端今日轮询命中 orders 端点时写穿(机会式);
|
||||
5. **本地历史查询**:api/trades.js 新增 trades/history 端点(时间段/code/策略/方向过滤,策略过滤走 FK 链 join);
|
||||
6. **前端**:TradeRecordsTab 加策略过滤下拉(全部/各策略/未关联)+ 历史范围查本地库(不再占位);
|
||||
7. **验证**:typecheck 通过、构建成功、独立数据目录回归测试 17/17 通过 + TradeSync 集成测试 5/5 通过(幂等/归属推导/过滤/写穿)。
|
||||
|
||||
## 经验教训(复盘沉淀)
|
||||
|
||||
### 1. 时间基准必须一致(本次踩坑)
|
||||
- **教训**:insert_ts 合成先用 Date.UTC(),而 strategy_holdings.created_at 是本地 Date.now() —— 时区差导致持仓窗口匹配失败(回归测试 6 项失败);
|
||||
- **沉淀**:同一库内时间戳必须同一基准(本地时间);跨模块时间比较前先核对基准。
|
||||
|
||||
### 2. 测试数据要构造真实时间窗
|
||||
- 第一次归属推导测试失败是因为用「当前时间」建仓(23:32)而委托是 09:30 —— 窗口本就不覆盖,代码是对的、测试数据不对;
|
||||
- **沉淀**:生命周期窗口类测试必须用 SQL 精确控制 created_at/closed_at,模拟真实时间关系。
|
||||
|
||||
### 3. 大改组件优先整体重写(前端)
|
||||
- 迭代中对 TradeRecordsTab 做多次定点替换时部分替换未生效,产生中间态(引用未定义组件);
|
||||
- **沉淀**:结构性大改(新增过滤/分支重构)直接整文件重写更稳,避免局部替换残留。
|
||||
|
||||
|
||||
### 4. QMT 委托/成交 code 无后缀 + 无独立交易日字段(真实数据发现)
|
||||
- **发现**:重启后启动预热同步的真实委托/成交,code 为无后缀 `001330`(持仓体系是 `001330.SZ`),且委托无 `m_strTradeDate`(交易日 = `m_strInsertDate`)—— 直接导致 FK 链 join 匹配不上(博纳影业被判未关联)+ 历史按时间段过滤漏委托;
|
||||
- **修复**:QmtBridgeRestDataSource 加 `normalizeInstrumentCode`(6 位数字 + 交易所后缀;SH/SZ/BJ;无交易所按首位推断 6/9→SH、0/3→SZ;已带后缀保留)+ mapOrder 补 tradeDate(insertDate 兜底)+ mapTrade code 归一化;
|
||||
- **沉淀**:QMT 各接口的证券代码格式不一致(委托/成交无后缀、持仓带后缀),语义化映射层必须统一归一化;交易「日」概念在委托接口 = insertDate(无独立 tradeDate 字段)。
|
||||
|
||||
|
||||
### 5. 迁移持仓 created_at = 迁移时间戳 → FK 链窗口失真(真实数据发现 + 方案 A 修正)
|
||||
- **发现**:迁移自 JSON 的持仓 created_at 全部是迁移时刻时间戳(2026-09-01 17:36),晚于当日真实交易时间 → 当日委托 join 窗口不匹配 → 全部判「未关联」;
|
||||
- **决策(方案 A,2026-09-01 老师确认)**:当前持仓(closed_at IS NULL)只做同 code 匹配(不要求 created_at ≤ 委托时间,现在持有=当日交易可归);已清仓(closed_at 非空)才按时间窗口(created_at ≤ t < closed_at)判断;
|
||||
- **实现**:_resolveStrategyAttribution 窗口条件分支;回归测试覆盖迁移时间戳场景(17/17 通过);
|
||||
- **沉淀**:迁移数据的 created_at 语义 = 迁移时间而非真实建仓时间,时间窗口类推导必须考虑该失真;当前持仓用「存在即归属」更贴合业务语义。
|
||||
|
||||
|
||||
### 6. 归属判定改「手动设置」(R-009 二次定稿,老师拍板)
|
||||
- **发现**:算法推导(时间窗 + 份额最大)无法区分一票多策略(300057.SZ 同时分属 grid-supermarket 与 manual-t 各 1000 股,明天有成交不知道该归谁);
|
||||
- **决策(老师二次定稿 Q1-Q4)**:归属由用户在**交易记录 tab 手动设置**(全手动选、可随时改、以最终为准);trade_orders 冗余存 strategy_id + holding_id;**UPSERT 不覆盖归属列**(手动指定为插件逻辑);
|
||||
- **实现**:trade_orders 加 strategy_id/holding_id 列(存量库 ALTER 迁移,插件启动时执行,只读打开容忍);setOrderAttribution + getAttributionCandidates(候选 = 该 code 当前持仓策略)+ api 两端点(orders/set-attribution、orders/attribution-candidates);前端 TradeRecordsTab 归属列下拉(候选含策略名 + 份额);
|
||||
- **验证**:37 项测试通过(核心 14 含 UPSERT 不覆盖归属/归属可改/候选列表/持久化过滤);
|
||||
- **沉淀**:算法只能给候选,归属是人的决定(符合「人机合一」目标-007);手工指定的数据不能被自动同步覆盖——UPSERT 语义要区分「系统字段」与「用户字段」。
|
||||
|
||||
## 遗留/后续
|
||||
|
||||
1. **QMT Bridge 历史接口**:老师完善后,今日实时可扩展为历史接口查询(本地库仍为兜底/积累);
|
||||
2. **手动修正策略关联入口**:本期未做(自动推导 + 未关联兜底已满足复盘),后续按需;
|
||||
3. **导出/清理**:本地历史库持续积累,导出/清理管理界面后续迭代;
|
||||
4. **盘中行情写 SQLite 验证**(迭代 06 遗留):开盘后确认行情防抖写回 store.db(updated_at / mtime)。
|
||||
@@ -0,0 +1,28 @@
|
||||
# 迭代目标:07-交易记录本地存储(SQLite)+ 策略关联
|
||||
|
||||
> 迭代编号:07 | 创建:2026-09-01 | 状态:进行中
|
||||
> 依据计划:PLAN-008 | 需求:R-009(已定稿,2026-09-01)
|
||||
|
||||
## 目标描述
|
||||
|
||||
将 QMT 当日交易数据(委托 + 成交)本地持久化到 SQLite(trade_orders + trade_fills 两表,跨日积累成历史库),并按外键链(成交→委托→策略→holding)与插件策略体系关联,支持按策略过滤 / 复盘交易;同时解决 R-007 历史范围「接口开发中」占位问题(本地积累后历史可查)。
|
||||
|
||||
## 目标分解
|
||||
|
||||
1. **两表落地**:trade_orders(委托主行)+ trade_fills(成交明细),两表零冗余 strategy_id/holding_id,委托表含派生时间列 insert_ts(join 持仓窗口用);
|
||||
2. **定时同步**:服务端 TradeSync(启动预热 + 60s 定时 + UPSERT 幂等);
|
||||
3. **外键链关联**:策略/持仓归属 = 委托时间 join strategy_holdings 生命周期窗口推导(一码多策略取份额最大,未命中=未关联);
|
||||
4. **本地历史查询**:trades/history 端点(时间段/code/策略/方向过滤);
|
||||
5. **前端**:策略过滤下拉 + 历史范围查本地库。
|
||||
|
||||
## 讨论过程
|
||||
|
||||
- 2026-09-01 老师提出需求(在 SQLite 添加交易记录表,存储与策略关联的交易记录);
|
||||
- 2026-09-01 AI 登记 R-009(讨论中),提出 Q1-Q8 技术建议(两表结构/同步机制/策略关联/历史查询/积累范围/UI/状态更新/文档约束);
|
||||
- 2026-09-01 老师修正(Q3):关联链 = 成交→委托→策略→holding,两表零冗余,查询期按持仓生命周期窗口推导;
|
||||
- 2026-09-01 老师确认 Q1-Q8 定稿,R-009 转已定稿,进入本迭代。
|
||||
|
||||
## 对老师的配合需求
|
||||
|
||||
- 无阻塞依赖;QMT Bridge 历史接口未就绪不影响本迭代(本地历史库 = 插件运行期逐日积累);
|
||||
- 验收时可提供当日真实交易数据做端到端验证(沿用 R-007 模式)。
|
||||
@@ -0,0 +1,26 @@
|
||||
# 验收标准:07-交易记录本地存储(SQLite)+ 策略关联
|
||||
|
||||
## 验收标准线
|
||||
|
||||
1. **两表落地**:store.db 存在 trade_orders(order_id 主键 + insert_ts 派生列)+ trade_fills(trade_id 主键、order_id 关联委托)两表;**两表均无 strategy_id / holding_id 列**(零冗余,Q3 老师定稿);
|
||||
2. **同步落库**:启动预热 + 60s 定时同步今日 orders+trades 入库;UPSERT 幂等(重复同步不产生重复行、已存在委托状态/成交量覆盖更新);
|
||||
3. **策略归属推导(FK 链)**:查询时委托时间 join strategy_holdings 生命周期窗口(created_at ≤ t < closed_at)得到策略/持仓归属;一码多策略取份额最大;无命中=未关联;历史委托归属稳定(清仓/删策略转历史不删行);
|
||||
4. **本地历史查询**:trades/history 端点按 { start, end, code, strategyId, direction } 过滤返回 { orders, fills }(策略过滤走 FK 链 join);
|
||||
5. **前端**:交易记录 tab 加策略过滤下拉(全部/各策略/未关联);历史范围展示本地库数据(不再「接口开发中」占位);今日仍走 QMT 实时 + 写穿本地;
|
||||
6. **不回归**:今日实时展示(单表合并/费用/排序/展开)与持仓/策略/行情/连接配置功能正常;
|
||||
7. **测试隔离**:回归测试在独立数据目录(ODL_TEST_DATA_DIR)执行,真实数据目录无写操作(技术约束-011)。
|
||||
|
||||
## 验收方法
|
||||
|
||||
- 独立数据目录(ODL_TEST_DATA_DIR)构造今日委托/成交样例 → 启动插件 → 确认同步落库、UPSERT 幂等(重复同步行数不变、状态覆盖);
|
||||
- 构造持仓生命周期样例(当前持仓 + 已清仓)→ 确认策略归属推导正确(时间窗口命中、一码多策略取份额最大、未关联兜底);
|
||||
- API 实测:trades/history 按时间段/code/策略/方向过滤返回正确;
|
||||
- 前端:策略过滤下拉生效(今日实时 + 历史本地);历史范围展示本地数据;
|
||||
- 回归:今日实时展示与现有功能正常;
|
||||
- 手动核对真实数据目录:store.db 中 trade_orders/trade_fills 持续积累,无 JSON 文件新生。
|
||||
|
||||
## 验收目标
|
||||
|
||||
- 交易记录本地持久化闭环:当日实时 + 跨日历史本地可查;
|
||||
- 外键链策略关联正确:按策略过滤 / 复盘交易就绪(R-007 Q10 落地);
|
||||
- 设计约束同步落地:数据存储设计.md §10 + 技术方案约束新增条目。
|
||||
@@ -0,0 +1,71 @@
|
||||
# 技术实现方案:08-策略持仓行展开关联交易记录(Holding → 交易汇总)
|
||||
|
||||
> 依据:PLAN-009 | 需求:R-010(Q1-Q3 定稿)| 设计约束:技术约束-012、技术约束-011(测试隔离)
|
||||
|
||||
## 技术选型
|
||||
|
||||
- 复用 R-009 的 holding_id 关联(trade_orders.holding_id);
|
||||
- 服务端:strategy-positions 附加 holding_id + trades/by-holding 端点;
|
||||
- 前端:StrategyTab 持仓行展开(懒加载内嵌汇总表)。
|
||||
|
||||
## 数据流
|
||||
|
||||
```
|
||||
策略持仓 tab:
|
||||
load() → strategy-positions(每行含 holding_id)
|
||||
用户点击持仓行 → 展开 → trades/by-holding?holdingId=13 → 委托汇总列表
|
||||
懒加载:不展开不请求
|
||||
```
|
||||
|
||||
## 实现细节
|
||||
|
||||
### 1. PositionManager.getStrategyPositions 附加 holding_id
|
||||
|
||||
```js
|
||||
async getStrategyPositions(strategyId) {
|
||||
const [positions, dataset, holdings] = await Promise.all([
|
||||
this.getAllPositions(),
|
||||
this.storage.getDataset(strategyId),
|
||||
this.storage.getCurrentHoldings(strategyId), // 含 holding_id
|
||||
]);
|
||||
const shareMap = new Map(dataset.map(d => [d.code, d.shares]));
|
||||
const holdingMap = new Map(holdings.map(h => [h.code, h.holdingId]));
|
||||
return positions.map(p => {
|
||||
const shares = shareMap.get(p.code) ?? 0;
|
||||
return shares > 0 ? { ...p, shares, holdingId: holdingMap.get(p.code) ?? null } : null;
|
||||
}).filter(Boolean);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. SqliteStore.getOrdersByHolding
|
||||
|
||||
```js
|
||||
getOrdersByHolding(holdingId) {
|
||||
this.init();
|
||||
return this.db.prepare(
|
||||
'SELECT * FROM trade_orders WHERE holding_id=? ORDER BY insert_ts DESC'
|
||||
).all(holdingId).map(r => this._mapOrderRow(r));
|
||||
}
|
||||
```
|
||||
|
||||
### 3. api/trades.js:trades/by-holding 端点
|
||||
|
||||
```js
|
||||
case 'trades/by-holding':
|
||||
return await storage.getTradeOrdersByHolding(args.holdingId);
|
||||
```
|
||||
|
||||
### 4. StrategyTab 展开 UI
|
||||
|
||||
- 持仓行加展开箭头(类似交易记录 tab);
|
||||
- 展开时调 trades/by-holding,内嵌小表格显示委托汇总:
|
||||
时间 / 方向 / 状态 / 委托量 / 成交量 / 均价 / 金额 / 费用;
|
||||
- 只显示委托汇总(ORDER_STATUS 中文映射复用);
|
||||
- 懒加载:展开时才请求,折叠清空。
|
||||
|
||||
## 涉及设计约束
|
||||
|
||||
| 约束 | 内容 |
|
||||
|---|---|
|
||||
| 技术约束-012 | 数据存储遵循数据存储设计.md(复用 trade_orders.holding_id) |
|
||||
| 技术约束-011 | 测试用独立数据目录(ODL_TEST_DATA_DIR) |
|
||||
@@ -0,0 +1,38 @@
|
||||
# 迭代复盘:08-策略持仓行展开关联交易记录(Holding → 交易汇总)
|
||||
|
||||
> 复盘日期:2026-09-02 | 迭代状态:**已完成(老师确认)**
|
||||
> 关联需求:R-010(策略持仓行展开关联交易记录,已定稿)
|
||||
> 关联计划:PLAN-009(计划-策略持仓行展开关联交易记录)
|
||||
|
||||
## 结果
|
||||
|
||||
迭代 08 达成:策略持仓 tab 每个持仓行(Holding)可展开,展开显示与该 holding 关联的交易记录(委托汇总,不分笔成交);持仓行新增成本价(avgPrice)+ 最后一笔成交价(lastTradePrice)两列;神之一手全部 tab 激活时隐藏 AI 对话输入框(纯 CSS)。实现「持仓 ↔ 交易」双向追溯。
|
||||
|
||||
## 过程事实
|
||||
|
||||
1. **需求定稿(R-010)**:老师提出持仓行展开看关联交易(只要委托汇总)→ AI 登记 Q1-Q3(附加 holding_id / by-holding 端点 / 展开 UI)→ 老师确认 → 补充 Q4(成本价 + 最后一笔成交价,取最新有成交的 tradedPrice,无则默认成本价);
|
||||
2. **服务端**:strategy-positions 附加 holding_id(PositionManager 查 strategy_holdings)+ 计算 avgPrice/lastTradePrice(查该 holding 关联委托,取最新有成交的 tradedPrice);SqliteStore.getOrdersByHolding + DataStore 委托;api trades/by-holding 端点;
|
||||
3. **前端**:StrategyTab 持仓行展开(箭头 + 懒加载 + 内嵌委托汇总表,含交易日/时间列);成本价/最后一笔成交价两列;
|
||||
4. **隐藏输入框**:借鉴 dsh-context 插件的纯 CSS 方案(:has(.odl-root) 命中时隐藏 composer)——神之一手 tab 激活时隐藏输入框,chat/其他 tab 正常显示;
|
||||
5. **验证**:回归测试(by-holding 查询 6 项 + lastTradePrice 计算 4 项)通过、真实数据验证(001330.SZ holding 13 → 博纳影业卖出委托;成本价 6.84 / 最后成交 6.00)、构建 + typecheck 通过。
|
||||
|
||||
## 经验教训(复盘沉淀)
|
||||
|
||||
### 1. 隐藏宿主 UI 优先借鉴成熟插件方案
|
||||
- 曾深挖 DSH composer chain / selector 机制(复杂度高、依赖内部 store),后经老师提示参考 dsh-context 插件——它用一行纯 CSS(:has() 选择器)实现「特定 tab 激活时隐藏输入框」,零宿主改动、零风险;
|
||||
- **沉淀**:改宿主 UI 前先看同类插件怎么做的;纯 CSS :has() 是最优雅的 view 条件渲染方案。
|
||||
|
||||
### 2. 前端声明顺序 TDZ(本次多次踩坑)
|
||||
- 迭代 07/08 多次遇到「Cannot access X before initialization」(TDZ):useEffect 依赖数组/回调在渲染时求值,引用了后声明的 const;
|
||||
- **沉淀**:React 组件内所有 useCallback/useEffect 的依赖引用必须在其声明之后;新增代码时严格核对声明顺序,构建后浏览器验证。
|
||||
|
||||
### 3. 服务端 vs 客户端生效机制
|
||||
- 服务端改动(PositionManager/API/SqliteStore)需重启 DSH 生效;客户端 bundle 刷新即载(rev 变化自动同步);
|
||||
- **沉淀**:改完先确认服务端端点/响应是新行为,再让老师重启。
|
||||
|
||||
## 遗留/后续
|
||||
|
||||
1. **持仓展开的交易汇总**:当前只显示该 holding 关联的委托汇总;后续可按 holding 聚合复盘(目标-006);
|
||||
2. **关注列表 tab**:仍为占位(PlaceholderTab),后续迭代实现;
|
||||
3. **全部持仓 tab**:未做展开(老师只要求策略持仓 tab);如需可复用同样模式;
|
||||
4. **迭代 07 遗留**:QMT Bridge 历史接口(老师完善后接入)、盘中行情写 SQLite 验证(开盘后确认)。
|
||||
@@ -0,0 +1,24 @@
|
||||
# 迭代目标:08-策略持仓行展开关联交易记录(Holding → 交易汇总)
|
||||
|
||||
> 迭代编号:08 | 创建:2026-09-02 | 状态:进行中
|
||||
> 依据计划:PLAN-009 | 需求:R-010(已定稿,2026-09-02)
|
||||
|
||||
## 目标描述
|
||||
|
||||
策略持仓 tab 每个持仓行(Holding)可展开,展开显示与该 holding 关联的交易记录(仅委托汇总,不分笔成交),实现「持仓 ↔ 交易」双向追溯(R-009 反向)。
|
||||
|
||||
## 目标分解
|
||||
|
||||
1. **strategy-positions 附加 holding_id**:持仓行带出关联锚点;
|
||||
2. **trades/by-holding 端点**:按 holding_id 查委托汇总;
|
||||
3. **持仓行展开 UI**:展开显示委托汇总表(懒加载)。
|
||||
|
||||
## 讨论过程
|
||||
|
||||
- 2026-09-02 老师提出需求(持仓行展开看关联交易,只要委托汇总);
|
||||
- 2026-09-02 AI 登记 R-010(讨论中),提出 Q1-Q3(附加 holding_id / by-holding 端点 / 展开 UI);
|
||||
- 2026-09-02 老师确认 Q1-Q3 定稿,进入本迭代。
|
||||
|
||||
## 对老师的配合需求
|
||||
|
||||
- 无阻塞依赖;验收时用真实持仓(如 001330.SZ holding 13)验证展开效果。
|
||||
@@ -0,0 +1,23 @@
|
||||
# 验收标准:08-策略持仓行展开关联交易记录(Holding → 交易汇总)
|
||||
|
||||
## 验收标准线
|
||||
|
||||
1. **strategy-positions 附加 holding_id**:每个持仓行含 holding_id(同策略同 code 当前持仓);
|
||||
2. **trades/by-holding 端点**:按 holding_id 返回该 holding 的委托汇总(时间/方向/状态/量/价/金额/费用;无成交委托也显示);
|
||||
3. **持仓行展开**:策略持仓 tab 每行可展开(箭头指示),展开显示委托汇总表;
|
||||
4. **仅汇总不分笔**:展开内容只显示委托汇总,无分笔成交明细;
|
||||
5. **懒加载**:不展开不请求 trades/by-holding;
|
||||
6. **不回归**:策略持仓份额操作(添加/移出/全部移入)、交易记录 tab、行情等现有功能正常;
|
||||
7. **测试隔离**:回归测试在独立数据目录执行(技术约束-011)。
|
||||
|
||||
## 验收方法
|
||||
|
||||
- 独立数据目录构造持仓 + 委托(holding_id 关联)→ 启动 → 验证 strategy-positions 含 holding_id、trades/by-holding 返回正确;
|
||||
- 真实数据:网格超市 tab 展开 001330.SZ(holding 13)→ 看到博纳影业卖出委托汇总;
|
||||
- 前端:展开/折叠、懒加载、汇总表列正确;
|
||||
- 回归:份额操作、交易记录、行情不回归。
|
||||
|
||||
## 验收目标
|
||||
|
||||
- 持仓 ↔ 交易双向追溯闭环(R-009 交易→持仓 + 本迭代持仓→交易);
|
||||
- 为按 holding 复盘交易铺路(目标-006)。
|
||||
@@ -0,0 +1,82 @@
|
||||
# 技术实现方案:09-Tab 设置统一管理
|
||||
|
||||
> 迭代编号:09 | 依据:PLAN-010、R-011、产品约束-009、UI约束-003、技术约束-014
|
||||
|
||||
## 1. 数据模型(src/settings.js)
|
||||
|
||||
### 1.1 tabs 有序数组(新 schema)
|
||||
|
||||
```js
|
||||
tabs: z.array(z.object({
|
||||
id: z.string().required(), // 唯一 id:'tab-all-positions' / 'tab-strategy-<id>'
|
||||
kind: z.enum(['builtin', 'strategy']).required(),
|
||||
refKey: z.string().optional(), // builtin 专用:'allPositions' | 'tradeRecords' | 'watchlist'
|
||||
refId: z.string().optional(), // strategy 专用:策略 id
|
||||
name: z.string().required(), // 展示名(策略行 join 时刷新)
|
||||
visible: z.boolean().default(true),
|
||||
order: z.number().default(0),
|
||||
})).default(DEFAULT_TABS)
|
||||
```
|
||||
|
||||
### 1.2 归一化迁移(读取时)
|
||||
|
||||
getTabs(scope) 逻辑:
|
||||
- 读取 settings.tabs:若为数组 → 直接返回(按 order 排序);
|
||||
- 若为旧布尔对象(或缺失)→ 生成默认内置三条(全部持仓/交易记录/关注列表,保留原显隐值),策略按 strategies 旧 order 接续追加(旧 hidden → visible=true);
|
||||
- 首次写入时落库归一化后的数组(幂等,不重复迁移)。
|
||||
|
||||
### 1.3 strategies 收窄
|
||||
|
||||
strategies schema 去掉 visible/order(仅 {id, name});getStrategies 不再排序/过滤 visible;策略展示名统一由 getTabs join strategies 得出(Q4 自动跟随改名)。
|
||||
|
||||
## 2. API 层(src/api/strategies.js)
|
||||
|
||||
| 端点 | 变更 |
|
||||
|---|---|
|
||||
| tabs | 返回合并后的完整 tabs 数组(含策略行 name join) |
|
||||
| tabs/update | 整表更新 { tabs }(顺序 + 显隐);策略行 name 由服务端 join 刷新,客户端可只传 id 顺序 |
|
||||
| strategies/add | 联动 append tab 条目(末尾,order = max+1) |
|
||||
| strategies/remove | 联动删除对应 tab 条目(refId === 策略 id) |
|
||||
| strategies/move | **废弃**(从 METHODS 移除) |
|
||||
| strategies/update | 仅剩重命名(联动刷新 tabs 中策略行 name) |
|
||||
|
||||
## 3. 客户端注册(src/client/index.js)
|
||||
|
||||
合并 registerGeneralTabs + registerStrategyTabs → registerAllFromTabs(tabs):
|
||||
```js
|
||||
const sorted = tabs.slice().sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
||||
for (const t of sorted) {
|
||||
if (t.visible === false) continue;
|
||||
const render = t.kind === 'builtin' ? GENERAL_RENDER[t.refKey] : (props) => createElement(StrategyTab, { ...props, strategyId: t.refId, strategyName: t.name });
|
||||
// slots.register conversation.view, order: t.order
|
||||
}
|
||||
```
|
||||
- GENERAL_RENDER = { allPositions, tradeRecords, watchlist } 映射常量;
|
||||
- 移除硬编码 order 10/11/12 与 13+ 间隔(直接用 t.order);
|
||||
- fetchTabConfig → fetchTabs 返回数组。
|
||||
|
||||
## 4. 设置页 UI(src/client/views/SettingsSection.jsx)
|
||||
|
||||
### 4.1 「Tab 设置」子 tab
|
||||
|
||||
- 子 tab 导航:general → tabs(更名),标签「Tab 设置」;
|
||||
- 表格列:拖动手柄(≡)| 名称(内置行带「内置」徽标、策略行带「策略」徽标)| 显示(Switch);
|
||||
- **无重命名/删除按钮(任何行)**;
|
||||
- 拖动(原生 DnD):
|
||||
- tr draggable,onDragStart 记 id;onDragOver 阻止默认 + 计算插入位;onDrop 重排数组;
|
||||
- 落点 → 立即调 tabs/update(整表提交)→ 提示「已保存,刷新页面后生效」;
|
||||
- 显隐开关:切换 → 立即调 tabs/update → 提示。
|
||||
|
||||
### 4.2 策略分组瘦身
|
||||
|
||||
- 移除排序箭头列(↑↓)与显示列(Switch);
|
||||
- 保留:新增输入框 + 新增按钮 / 重命名 / 删除(确认弹窗 + 份额回未分配沿用);
|
||||
- 顶部加提示:顺序与显示请在「Tab 设置」中调整。
|
||||
|
||||
## 5. 兼容与风险
|
||||
|
||||
- **迁移**:读取归一化幂等;旧策略 visible=false 迁移后 visible=true(Q3,产品约束-009 一致);
|
||||
- **名称 join**:tabs 存 refId,展示时 join strategies;策略改名后 tabs 行 name 自动跟随(Q4);
|
||||
- **注册**:refresh = 刷新页面重载插件(无事件机制,沿用);
|
||||
- **RPC**:strategies/move 废弃需同步移除客户端调用(settings UI 不再有箭头);
|
||||
- **回归**:技术约束-011 —— 写操作测试用独立数据目录(ODL_TEST_DATA_DIR / dataDir 参数)。
|
||||
@@ -0,0 +1,41 @@
|
||||
# 迭代复盘:09-Tab 设置统一管理(内置 + 策略分组,显示/隐藏 + 拖动排序)
|
||||
|
||||
> 复盘日期:2026-09-02 | 迭代状态:**已完成(老师确认)**
|
||||
> 关联需求:R-011(Tab 设置:统一管理所有 tab,已定稿 Q1-Q5)
|
||||
> 关联计划:PLAN-010(计划-Tab设置统一管理)
|
||||
|
||||
## 结果
|
||||
|
||||
迭代 09 达成:设置页「通用设置」升级为「Tab 设置」,成为所有会话 tab(系统内置 + 策略分组)**唯一的顺序与显隐入口**:两类 tab 混排一张表,每行 = 拖动手柄(原生 HTML5 DnD)+ 名称(内置带「内置」徽标、策略带「策略」徽标)+ 显示/隐藏开关;**任何 tab 均不支持重命名与删除**;拖动落点立即持久化;策略分组子 tab 瘦身为 新增/重命名/删除。
|
||||
|
||||
## 过程事实
|
||||
|
||||
1. **需求定稿(R-011)**:老师提出设置页默认 tab 与策略 tab 一起排序(内置标记、不可重命名/删除)→ AI 分析现状(两套数据、两套 order 空间,内置恒在策略前)→ 提出统一 tabs 有序数组方案 → 老师确认 D1-D7 + Q1-Q5(拖动落点立即持久化 / 新增追加末尾 / 旧隐藏策略迁移后显示 / 名称 join 自动跟随 / 全部 tab 禁重命名删除);
|
||||
2. **数据层(settings.js)**:tabs 从布尔对象 → 统一有序数组(kind: builtin|strategy + refKey/refId + visible + order);strategies 收窄为 {id, name};旧格式读取时静默归一化迁移(schema union 兼容存量 + normalizeTabs 转换);addStrategy/removeStrategy 联动 tabs;
|
||||
3. **API 层(strategies.js)**:tabs/update 语义改整表(顺序 + 显隐);strategies/add 联动追加 tab(末尾);strategies/remove 联动删除 tab 条目;废弃 strategies/move;
|
||||
4. **客户端注册(client/index.js)**:合并 registerGeneralTabs + registerStrategyTabs 为统一注册(读 tabs 数组按 order 排序、过滤 visible,builtin 走内置 render、strategy 走 StrategyTab),移除硬编码 order 间隔(10/11/12 与 13+);
|
||||
5. **设置页 UI(SettingsSection.jsx)**:「Tab 设置」子 tab(混排表格 + 徽标 + 显隐开关 + 拖动排序 + 落点立即持久化);策略分组瘦身(移除排序箭头与显隐开关,加指引提示);
|
||||
6. **验证**:回归测试 35 项全通过(scripts/test-r011-tabs.mjs 数据层 23 项 + test-r011-api.mjs API 层 12 项)+ 真实 schema resolve 验证(旧配置归一化正确)+ 构建 + typecheck 通过。
|
||||
|
||||
## 经验教训(复盘沉淀)
|
||||
|
||||
### 1. schemastery 无 z.enum / .optional(),schema 兼容存量用 z.union 双分支
|
||||
- schemastery 只有 z.union([z.const(...), ...]) 无 z.enum,可选字段不调 .required() 即可;
|
||||
- **关键**:DSH settings 的 resolve 会用 schema 校验存量 user 层——新 schema 直接替换会导致旧配置(布尔对象 tabs)校验失败、插件启动报错;
|
||||
- **沉淀**:settings schema 变更必须先验证存量兼容——用 z.union 双分支(旧格式 + 新格式),旧值通过校验、读取时再归一化;改动 settings schema 前跑 schema-resolve 测试。
|
||||
|
||||
### 2. 读取时归一化 + schema 双分支 = 无感迁移
|
||||
- 本次没有写一次性迁移脚本,而是「schema 接受旧格式 + normalizeTabs 读取时转新」:存量用户升级零操作、零报错;
|
||||
- 归一化结果在首次写操作(updateTabs/add/remove)时落库为数组,之后自然持久化;
|
||||
- **沉淀**:settings 结构变更优先「schema 兼容 + 读取归一化」而非迁移脚本,符合 R-006/R-008 的迁移经验(幂等、无需用户操作)。
|
||||
|
||||
### 3. 前端字符串与 JSX 引号冲突
|
||||
- 写 JSX 组件代码时,代码里同时有 JS 单双引号与 JSX 属性引号,用模板字符串包整段时内部反引号/插值冲突,多次报错;
|
||||
- **沉淀**:批量生成代码用数组 join(每行独立字符串),避免模板字符串嵌套转义地狱。
|
||||
|
||||
## 遗留/后续
|
||||
|
||||
1. **Tab 设置拖动交互**:当前实现为整行 draggable(原生 DnD),落点插入到目标行位置;后续可优化为拖动手柄专属拖拽 + 拖拽中的视觉反馈(插入线);
|
||||
2. **关注列表 tab**:仍为占位(PlaceholderTab),后续迭代实现;
|
||||
3. **迁移落库时机**:旧配置首次读取不落库(纯读),首次写操作时才持久化新数组——如老师希望启动即落库可加一次性迁移(当前行为无感知差异,可接受);
|
||||
4. **多 DSH 实例/多用户**:tabs 顺序为全局设置(非按会话),符合现状(strategies 亦为全局)。
|
||||
@@ -0,0 +1,26 @@
|
||||
# 迭代目标:09-Tab 设置统一管理(内置 + 策略分组,显示/隐藏 + 拖动排序)
|
||||
|
||||
> 迭代编号:09 | 创建:2026-09-02 | 状态:进行中
|
||||
> 依据计划:PLAN-010 | 需求:R-011(已定稿,2026-09-02,Q1-Q5 确认)
|
||||
|
||||
## 目标描述
|
||||
|
||||
将设置页「通用设置」升级为「Tab 设置」,成为所有会话 tab(系统内置 + 策略分组)**唯一的顺序与显隐入口**:两类 tab 混排一张表,每行 = 拖动手柄 + 名称 + 显示/隐藏开关;任何 tab 均不支持重命名与删除;拖动落点立即持久化。
|
||||
|
||||
## 目标分解
|
||||
|
||||
1. **数据模型统一**(settings.js):tabs 布尔对象 → 有序数组,旧格式自动归一化;strategies 收窄为 {id, name};
|
||||
2. **API 层**(api/strategies.js):tabs/update 整表(顺序 + 显隐);strategies/add 联动追加 tab;strategies/remove 联动删除 tab;废弃 strategies/move;
|
||||
3. **客户端注册**(client/index.js):合并为统一注册,读 tabs 数组(order 排序 + visible 过滤),移除硬编码 order;
|
||||
4. **设置页 UI**(SettingsSection.jsx):「Tab 设置」子 tab(混排 + 徽标 + 显隐开关 + 拖动排序)+ 策略分组瘦身(仅 新增/重命名/删除 + 指引提示)。
|
||||
|
||||
## 讨论过程
|
||||
|
||||
- 2026-09-02 老师提出需求:设置页默认几个 tab 与策略 tab 一起排序,内置标记、不可重命名/删除;
|
||||
- 2026-09-02 AI 分析现状(两套数据/两套 order 空间),提出统一 tabs 有序数组方案;
|
||||
- 2026-09-02 老师确认 D1-D7:Tab 设置只做 显示/隐藏 + 排序(拖动),策略命名/删除留策略分组;
|
||||
- 2026-09-02 老师确认 Q1-Q5:拖动落点立即持久化 / 新增追加末尾 / 旧隐藏策略迁移后显示 / 名称 join 自动跟随 / 全部 tab 禁重命名删除,R-011 定稿。
|
||||
|
||||
## 对老师的配合需求
|
||||
|
||||
- 验收时验证:拖动排序落点立即生效(刷新后 tab 栏按新顺序);显隐开关切换后刷新生效;策略分组瘦身后的新增/重命名/删除仍正常;老配置自动迁移无报错。
|
||||
@@ -0,0 +1,26 @@
|
||||
# 验收标准:09-Tab 设置统一管理
|
||||
|
||||
> 迭代编号:09 | 依据:PLAN-010 验收要点 + R-011 定稿
|
||||
|
||||
## 验收标准线
|
||||
|
||||
1. 「通用设置」子 tab 更名为「Tab 设置」,表格混排内置 + 策略全部 tab;
|
||||
2. 内置行带「内置」徽标、策略行带「策略」徽标;
|
||||
3. 每行可拖动排序,落点**立即持久化**(刷新后会话 tab 栏按新顺序,内置与策略可交错);
|
||||
4. 每行有显示/隐藏开关,切换立即持久化(刷新后隐藏的 tab 消失/显示的出现);
|
||||
5. **任何行均无重命名/删除按钮**;
|
||||
6. 策略分组子 tab 只留 新增/重命名/删除,无排序箭头/显隐开关,有指引提示;
|
||||
7. 新增策略出现在 Tab 设置列表末尾;删除策略后对应 tab 条目消失(份额回未分配沿用);
|
||||
8. 老配置(布尔对象 tabs + 策略 order/visible)读取自动归一化,无报错、无数据丢失;
|
||||
9. 会话 tab 栏按新顺序与显隐注册;
|
||||
10. 现有功能不回归(全部持仓/交易记录/策略持仓/QMT 切换/删除策略份额回退)。
|
||||
|
||||
## 验收方法
|
||||
|
||||
- 构建:pnpm run build + typecheck 通过;
|
||||
- 单元/回归脚本:独立数据目录跑策略 CRUD + tabs 读写(技术约束-011);
|
||||
- 手动(需 DSH 运行 + 老师确认):设置页 Tab 设置拖动排序 / 显隐切换 / 策略分组瘦身 / 老配置迁移;会话 tab 栏顺序与显隐验证。
|
||||
|
||||
## 验收目标
|
||||
|
||||
- 全部 10 条验收标准线通过,迭代 09 标记「验收通过」,R-011 实现状态更新,归档流程走查。
|
||||
@@ -0,0 +1,43 @@
|
||||
# 技术实现方案:10-UI 适配 DSH 主题
|
||||
|
||||
> 迭代编号:10 | 依据:PLAN-011、R-012
|
||||
|
||||
## 1. 主题机制(已查明)
|
||||
|
||||
- 宿主深色主题挂 `body[data-ds-dark-theme]`;
|
||||
- 宿主注入 `--dsw-*` CSS 变量(定义在宿主 runtime,随 light/dark/system 切换);
|
||||
- 插件内联 style 直接引用 `var(--dsw-alias-xxx)` 即可自动适配。
|
||||
|
||||
## 2. 语义映射(实施基准,来自 R-012)
|
||||
|
||||
```
|
||||
背景: #fff(表面) → var(--dsw-alias-bg-layer-1)
|
||||
遮罩: rgba(0,0,0,.4) → var(--dsw-alias-bg-mask-1)
|
||||
hover面: #f5f5f5 → var(--dsw-alias-interactive-bg-hover)
|
||||
激活绿底: #e8f5e9/#f1f8f2 → color-mix(in srgb, var(--dsw-alias-state-success-primary) 10%, transparent)
|
||||
错误红底: #fdecea → color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent)
|
||||
信息蓝底: #e3f2fd → color-mix(in srgb, var(--dsw-alias-state-business-primary) 10%, transparent)
|
||||
主文字: #333/#222 → var(--dsw-alias-label-primary)
|
||||
次文字: #555/#666 → var(--dsw-alias-label-secondary)
|
||||
弱文字: #888/#999/#aaa → var(--dsw-alias-label-tertiary)
|
||||
蓝字: #1565c0 → var(--dsw-alias-state-business-primary)
|
||||
细线: #eee → var(--dsw-alias-border-l1)
|
||||
描边: #ddd/#ccc → var(--dsw-alias-border-l2)
|
||||
红(涨/错/删): #d32f2f/#c62828 → var(--dsw-alias-state-error-primary)
|
||||
绿(跌/成/主): #2e7d32 → var(--dsw-alias-state-success-primary)
|
||||
实心按钮字: #fff → var(--dsw-alias-button-contrast-fill)
|
||||
```
|
||||
|
||||
## 3. 替换细则
|
||||
|
||||
- 替换范围:inline style 对象、模板字符串中的颜色字面量;
|
||||
- `color-mix(in srgb, var(--xxx) 10%, transparent)` 用于需要「淡底 + 文字同色」的提示/激活态;
|
||||
- 带透明度的原色(如 rgba(255,215,0,.5) 价格闪动高亮)保留动画语义,改 `color-mix(in srgb, var(--dsw-alias-state-warn-primary) 50%, transparent)`;
|
||||
- 注释中色值仅作说明可保留(不影响运行,验收时以无运行色为准);
|
||||
- 每文件替换后 `node --check` 校验 JSX 语法。
|
||||
|
||||
## 4. 风险
|
||||
|
||||
- **语义偏差**:个别颜色无法精确定位语义 → 保留原色并在验收清单标注,交老师确认;
|
||||
- **color-mix 兼容性**:现代浏览器(Chrome 111+/Safari 16.2+)支持,DSH 目标为 Chromium 系,可接受;
|
||||
- **fallback**:`var(--xxx, <原色>)` 提供兜底,宿主 token 缺失时视觉不变(更安全)。
|
||||
@@ -0,0 +1,38 @@
|
||||
# 迭代复盘:10-UI 适配 DSH 主题(浅色 / 深色 / 跟随系统)
|
||||
|
||||
> 复盘日期:2026-09-02 | 迭代状态:**已完成(老师确认)**
|
||||
> 关联需求:R-012(UI 适配 DSH 主题,已定稿:暂定跟随系统)
|
||||
> 关联计划:PLAN-011(计划-UI主题适配)
|
||||
|
||||
## 结果
|
||||
|
||||
迭代 10 达成:神之一手客户端 11 个文件共 141 处硬编码颜色全部替换为宿主 `--dsw-*` token(含 fallback),插件在 DSH 浅色 / 深色 / 跟随系统主题下自动适配;不自行维护主题偏好(跟随宿主);仅色值 token 化,布局与交互不变。
|
||||
|
||||
## 过程事实
|
||||
|
||||
1. **需求定稿(R-012)**:老师提出适配 DSH 浅/深/系统主题 → AI 查明宿主机制(body[data-ds-dark-theme] + --dsw-* token,深色挂 data-ds-dark-theme、alias token 双值定义)→ 老师确认「暂定跟随系统」(D1-D5 + 语义映射表);
|
||||
2. **替换实施**:PriceCell / LoadState / Toast / PlaceholderTab / RangeSelector / AllPositionsTab / StrategyTab / TradeRecordsTab / QmtConnectionChip / SettingsSection 共 10 文件(+market 目录无颜色);
|
||||
3. **语义映射执行**:白底→bg-layer-1、淡灰底→bg-layer-2、hover→interactive-bg-hover、主/次/弱文字→label-primary/secondary/tertiary、边框→border-l1/l2/l3/l4、红(涨/删/错)→state-error-primary、绿(跌/成/激活)→state-success-primary、蓝(信息/业务)→state-business-primary、实心按钮字→button-contrast-fill、遮罩→bg-mask-1、阴影→shadow-lv3;
|
||||
4. **淡色底**:激活/提示底色用 color-mix(in srgb, var(--语义色) 10-12%, transparent),深浅主题自适应;
|
||||
5. **验证**:typecheck + build 通过;headless Chrome 实证宿主 token 系统完整(238 处 dsw-alias 引用、浅/深双值定义、data-ds-dark-theme 选择器);残留硬编码色 = 0。
|
||||
|
||||
## 经验教训(复盘沉淀)
|
||||
|
||||
### 1. 宿主主题机制:body[data-ds-dark-theme] + --dsw-* token(已实证)
|
||||
- DSH 主题不是 data-theme 属性切换,而是宿主在深色时给 body 挂 `data-ds-dark-theme`,token 以「alias 链 → static 值」双主题注入(light: neutral-bluish-00 白系;dark: neutral-bluish-875 深系);
|
||||
- **沉淀**:插件适配宿主主题只须引用 `var(--dsw-alias-xxx, fallback)`,fallback 保证 token 缺失时浅色可用;不要自建主题偏好。
|
||||
|
||||
### 2. var() 带 fallback 是安全的迁移策略
|
||||
- 每处替换写成 `var(--dsw-alias-xxx, #原色)`:宿主 token 定义齐全时自动适配;万一某 token 缺失(宿主版本差异),退回原浅色值不破相;
|
||||
- **沉淀**:对宿主 token 的依赖一律带 fallback,兼容宿主版本演进。
|
||||
|
||||
### 3. 批量替换的 edit 冲突处理
|
||||
- 多个相同 style 片段(如表头、输入框、删除按钮)导致 old_string 多处匹配:用 replace_all 处理真正相同的模式,或用带上下文的更精确 old_string;
|
||||
- **沉淀**:批量替换前先 grep 去重确认唯一性,相同模式直接用 replace_all,不同上下文逐条处理。
|
||||
|
||||
## 遗留/后续
|
||||
|
||||
1. **语义色待老师验收**:深色下个别语义色(state-error 红 / state-success 绿在深色底的对比度、紫/蓝徽标)观感需老师切主题确认;若个别不满意可后续加 `--odl-*` 覆盖(D5 暂缓项);
|
||||
2. **color-mix 兼容性**:现代 Chromium 支持;若遇旧内核浏览器个别淡底失效,fallback 无(color-mix 无 fallback 语法)——可后续降级处理;
|
||||
3. **Toast 样式**:随宿主语义色变化,实心绿/红底 + 白字在深色下对比度已由 token 保证;
|
||||
4. **shadows**:boxShadow 用了 shadow-lv3 token(宿主完整 shadow 值),个别较浅卡片阴影在深色下可能几乎不可见——可后续微调。
|
||||
@@ -0,0 +1,18 @@
|
||||
# 迭代目标:10-UI 适配 DSH 主题(浅色 / 深色 / 跟随系统)
|
||||
|
||||
> 迭代编号:10 | 创建:2026-09-02 | 状态:进行中
|
||||
> 依据计划:PLAN-011 | 需求:R-012(已定稿,2026-09-02,暂定跟随系统)
|
||||
|
||||
## 目标描述
|
||||
|
||||
神之一手客户端 UI 全部硬编码色值替换为宿主 `--dsw-*` token,使插件在 DSH 浅色 / 深色 / 跟随系统主题下均可读、协调,随主题自动切换。
|
||||
|
||||
## 目标分解
|
||||
|
||||
1. 10 个文件 141 处硬编码色按语义映射替换为宿主 token / color-mix;
|
||||
2. 涨跌红涨绿跌 → 宿主 state-error/success;主按钮/徽标/提示底色 → 宿主语义色;
|
||||
3. 构建 + typecheck + 深浅主题人工验收。
|
||||
|
||||
## 对老师的配合需求
|
||||
|
||||
- 验收:DSH 设置切换 浅色/深色/跟随系统,检查各页面可读性与协调性。
|
||||
@@ -0,0 +1,22 @@
|
||||
# 验收标准:10-UI 适配 DSH 主题
|
||||
|
||||
> 迭代编号:10 | 依据:PLAN-011 验收要点 + R-012
|
||||
|
||||
## 验收标准线
|
||||
|
||||
1. 浅色主题下插件各页面观感与现状基本一致(无突兀色差);
|
||||
2. 深色主题下所有页面可读(背景/文字/边框/按钮/涨跌/徽标/提示/下拉菜单协调);
|
||||
3. DSH 设置切换 浅色/深色/跟随系统 实时生效;
|
||||
4. 涨跌色(红涨绿跌)在深浅两主题下均醒目可辨;
|
||||
5. 运行代码无残留硬编码色(#xxx / rgb / rgba),注释可留;
|
||||
6. 布局与交互不变(仅色值);
|
||||
7. build + typecheck 通过。
|
||||
|
||||
## 验收方法
|
||||
|
||||
- build + typecheck;
|
||||
- 老师切 DSH 浅/深主题人工检查:设置页(Tab 设置/策略分组/QMT 卡片)、全部持仓、交易记录、策略持仓、会话头部 QMT chip 下拉、Toast。
|
||||
|
||||
## 验收目标
|
||||
|
||||
- 7 条验收线通过,迭代 10 标记「验收通过」,R-012 更新实现状态。
|
||||
@@ -0,0 +1,140 @@
|
||||
# 技术实现方案:11-策略自定义字段配置
|
||||
|
||||
> 迭代编号:11 | 依据:PLAN-012 + R-013(目标数据模型 / 端到端示例)+ 技术约束-015 / 产品约束-010 / UI约束-005
|
||||
|
||||
## 1. 数据层
|
||||
|
||||
### 1.1 settings.strategies 扩展 configSchema(src/settings.js)
|
||||
|
||||
```js
|
||||
// strategySchema 中 strategies 项扩展:
|
||||
strategies: z.array(z.object({
|
||||
id: z.string().required(),
|
||||
name: z.string().required(),
|
||||
configSchema: z.array(z.object({
|
||||
key: z.string().required(), // 稳健标识(英文 slug)
|
||||
label: z.string(), // UI 展示名(可中文)
|
||||
type: z.union([z.const('text'), z.const('number'), z.const('boolean'), z.const('enum')]).required(),
|
||||
enum: z.array(z.string()), // 仅 type=enum 时使用
|
||||
def: z.any(), // 默认值(text=string / number=number / boolean=boolean / enum=枚举项)
|
||||
unit: z.string().default(''), // 单位(2026-09-02 加:可为空;如 % / 元 / 手;展示时拼在值后)
|
||||
})).default([]), // 缺省 []
|
||||
})).default(DEFAULT_STRATEGIES),
|
||||
```
|
||||
|
||||
- 读取归一化:旧项无 configSchema → 补默认空数组(读取时或 schema default 保证);
|
||||
- addStrategy 返回值默认带 configSchema: [];renameStrategy / updateStrategies 整表更新天然携带;
|
||||
- 兼容:settings 旧数据({id,name})经 schemastery default/union 归一化,无迁移脚本。
|
||||
|
||||
### 1.2 strategy_holdings 新增 values 列 + 读写(src/storage/SqliteStore.js)
|
||||
|
||||
```js
|
||||
// init() 内补列(幂等,沿用 _ensureTradeAttributionColumns 模式;只读连接容忍)
|
||||
_ensureHoldingValuesColumn() {
|
||||
const cols = new Set(this.db.prepare('PRAGMA table_info(strategy_holdings)').all().map(c => c.name));
|
||||
if (!cols.has('values')) this.db.exec('ALTER TABLE strategy_holdings ADD COLUMN values TEXT');
|
||||
}
|
||||
|
||||
readValues(holdingId) // SELECT values FROM strategy_holdings WHERE holding_id=? → JSON.parse ?? null
|
||||
writeValues(holdingId, values) // UPDATE strategy_holdings SET values=? WHERE holding_id=? → JSON.stringify
|
||||
```
|
||||
|
||||
- openHolding/addShares/reduceShares/closeHolding **不碰 values 列**(份额生命周期与字段值独立);
|
||||
- getCurrentHoldings / getHoldingHistory 的 _mapHolding 附加 values 字段(解析 JSON)。
|
||||
|
||||
## 2. API 层(src/api/strategies.js)
|
||||
|
||||
```js
|
||||
// 既有:strategy-positions → manager.getStrategyPositions(strategyId)
|
||||
// PositionManager 返回项附加 values(from SqliteStore 映射)
|
||||
// 新增端点:
|
||||
case 'holdings/values-update':
|
||||
return await manager.updateHoldingValues(args.holdingId, args.values);
|
||||
```
|
||||
|
||||
```js
|
||||
// src/position/PositionManager.js —— updateHoldingValues(holdingId, values):
|
||||
// 1) 定位 holdings 行(须存在且当前持仓 closed_at IS NULL;否则 404)
|
||||
// 2) 服务端校验(按所属策略 configSchema):
|
||||
// number → Number.isFinite(Number(v));enum → enum.includes(v);boolean → typeof v === 'boolean'
|
||||
// 未定义 key 的额外键放行(可扩展);空值/缺省可写入
|
||||
// 3) 校验失败抛 { code: 'field-validation' },成功 writeValues 并返回 { holdingId, values }
|
||||
```
|
||||
|
||||
## 3. 设置页 UI(src/client/views/SettingsSection.jsx)
|
||||
|
||||
- 「策略分组」子 tab 策略行加展开手柄(▶/▸ toggle,样式沿用 expand 惯例);
|
||||
- 展开区:
|
||||
- 字段列表表格:展示名 | key | 类型 | 枚举选项 | 默认值 | 操作(删除)—— 空态提示「尚未配置自定义字段」;
|
||||
- 添加/编辑字段表单:字段名(label)→ 自动生成 key(拼音 slug,复用 generateStrategyId 思路)/ 可手改校验唯一、类型下拉(文本/数字/布尔/枚举)、枚举选项(type=enum 时逗号分隔输入)、默认值(按类型渲染输入);
|
||||
- 行内编辑/删除:编辑回填表单,删除后保存生效;
|
||||
- 保存:整表提交 strategies/update(映射为 { ...s, configSchema }),成功 Toast + 刷新(沿用「刷新页面后生效」机制)。
|
||||
|
||||
## 4. 策略持仓 tab UI(src/client/views/StrategyTab.jsx)
|
||||
|
||||
> 2026-09-02 演进(老师选 A + 列化):自定义字段**不再放展开区**,直接作为表格列展示;值编辑 = **点击字段单元格内联编辑**;展开区(R-010)仅保留交易明细。
|
||||
|
||||
- 自定义字段列按列配置(§7)渲染,取值 = values[key] ?? def(boolean→开/关,空→—);
|
||||
- 点击字段单元格(有 holding 锚点行)进入内联编辑:text/number=输入框(Enter 保存 / Esc 取消 / blur 保存)、boolean=开关(点击即提交切换)、enum=下拉(选择即提交);
|
||||
- 保存调 one-divine-lot/holdings/values-update(按该行现有 values 合并,仅改本字段;空值删键回退 def);成功刷新,失败 Toast;
|
||||
- 单元格点击 stopPropagation,不与行展开(tr onClick)冲突;无 holding 锚点(未分配)的字段列不可编辑(虚线标识可点);
|
||||
|
||||
## 5. 回归脚本(scripts/test-r013-custom-fields.mjs)
|
||||
|
||||
- ODL_TEST_DATA_DIR 独立数据目录(技术约束-011);
|
||||
- 用例:addStrategy 带 configSchema(四类型)→ openHolding ×2 同策略不同 code → writeValues 各自值 → 读回校验 → 校验拦截(数字非法 / 枚举越界)→ 旧策略无 configSchema 持仓行 readValues 为 NULL;
|
||||
- 断言存储与读取一致、校验拒绝。
|
||||
|
||||
## 7. 表格列显隐配置(迭代 11 范围扩展,2026-09-02 老师确认)
|
||||
|
||||
> 背景:R-013 落地后老师要求——策略持仓表的**列可显示/隐藏配置**:不限于自定义字段列,还包含持仓表原有基础数据列;除 代码/名称/操作(+展开箭头)外全部支持;**每策略独立配置**;入口 = 策略持仓 tab 顶部「列设置」;支持**列排序**。并入本次迭代。
|
||||
|
||||
### 7.1 数据模型(定稿)
|
||||
|
||||
```js
|
||||
// settings 新增 strategyColumns(仅存与默认不同的覆盖;读取时归一化)
|
||||
strategyColumns: {
|
||||
[strategyId]: [
|
||||
{ key: 'lastPrice', visible: true }, // 基础数据列(可配置区)
|
||||
{ key: 'pctChange', visible: true },
|
||||
{ key: 'lastClose', visible: false },
|
||||
{ key: 'avgPrice', visible: true },
|
||||
{ key: 'lastTradePrice', visible: true },
|
||||
{ key: 'shares', visible: true },
|
||||
{ key: 'gridGap', visible: true }, // 自定义字段列(key = configSchema.key)
|
||||
{ key: 'gridAmount', visible: true },
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
**列分类**:
|
||||
- **固定列**:展开箭头 | 代码 | 名称(恒显、锁最前)| 操作(锁最后,含 全部移入/移出)—— 不可配置;
|
||||
- **可配置区**:基础数据列(现价/涨幅/昨收/成本价/最后一笔成交价/份额)+ 该策略全部自定义字段列(key=configSchema.key)—— 显隐 + 顺序可配置;
|
||||
- 自定义字段列**存在性**仍由 configSchema 决定(策略未配该字段则无此列);configSchema 增删字段 → 列配置读取时自动跟随(新增字段默认追加末尾、删除字段自然移除)。
|
||||
|
||||
**显隐单一数据源**(老师确认 2026-09-02):visible **只存在 strategyColumns**,configSchema **不加 visible**;展开区编辑区恒显示该策略全部字段(编辑入口需全量),表格列显隐由列配置单独管——避免双显隐源打架。
|
||||
|
||||
**默认值/兼容**:未配置 strategyColumns 的策略 → 归一化为 基础列全显(默认序)+ configSchema 字段列全显(configSchema 序);老数据缺字段 → 补默认。
|
||||
|
||||
### 7.2 涉及改动
|
||||
|
||||
1. **src/settings.js**:基础列目录常量 COLUMN_META(key/label);strategySchema + strategyColumns;读取归一化 normalizeStrategyColumns(基础列 + 该策略 configSchema 字段合成全列 → 应用覆盖 → 返回有序可见列);更新 API;
|
||||
2. **src/api/strategies.js**:新增 strategy-columns(读归一化结果)/ strategy-columns/update {strategyId, columns};
|
||||
3. **src/client/views/StrategyTab.jsx**:表格动态列化——表头/行按列配置渲染(固定列 + 可见可配置列),行数据映射每列取值;
|
||||
4. **新组件 ColumnSettingsPopover**(views/):tab 顶部「列设置」按钮 → 弹层:可配置列列表(标签 + 显隐勾选 + 拖动排序)+ 立即持久化 strategy-columns/update;
|
||||
5. 展开区(§4)不再显示自定义字段(已列化),仅保留交易明细;字段值编辑 = 表格字段单元格点击内联编辑(§4 演进);
|
||||
6. 回归脚本 + typecheck/build。
|
||||
|
||||
### 7.3 列取值(StrategyTab 渲染参考)
|
||||
|
||||
| 列 key | 取值 |
|
||||
|---|---|
|
||||
| lastPrice / lastClose / pctChange | getPrice(code) 行情 |
|
||||
| avgPrice / lastTradePrice | p 的字段(QMT 成本价 / R-010 最后一笔成交价) |
|
||||
| shares | p.shares |
|
||||
| 自定义字段 key | p.values?.[key] ?? def(展示同展开区:boolean→开/关,空→—) |
|
||||
|
||||
## 6. 验证
|
||||
|
||||
- build + typecheck 通过;
|
||||
- 老师人工验收(见验收标准)。
|
||||
@@ -0,0 +1,75 @@
|
||||
# 程序结构设计:11-策略自定义字段配置(含结构归类优化)
|
||||
|
||||
> 迭代编号:11 | 2026-09-02 | 依据:技术约束-016(目录归类)、R-013、PLAN-012
|
||||
|
||||
## 背景:结构审查发现与优化
|
||||
|
||||
2026-09-02 结构审查发现:src/component/ 平铺 10 个服务端模块,偏离迭代 01/02 程序结构设计的语义分域(data-source/、position/、storage/);`component` 命名语义含混(UI 组件实际在 client/views/);归类演进无文档记录;存在死代码。
|
||||
|
||||
## 优化动作(2026-09-02 落地)
|
||||
|
||||
1. **归类还原**:component/ 平铺 → 按功能域分目录:
|
||||
|
||||
```
|
||||
src/
|
||||
├── index.js # 插件入口(组装各模块)
|
||||
├── settings.js # 设置管理(策略 + tabs + QMT 连接配置)
|
||||
├── api/ # 服务端 HTTP API(webServer /odl/api/*),按领域拆子文件
|
||||
│ ├── index.js # 路由入口(合并各领域 + 405/404/500)
|
||||
│ ├── common.js # 公共工具
|
||||
│ ├── positions.js # 持仓域
|
||||
│ ├── strategies.js # 策略/份额/tabs 域
|
||||
│ ├── qmt-connections.js # QMT 连接配置域
|
||||
│ ├── market.js # 行情域
|
||||
│ └── trades.js # 交易记录域
|
||||
├── data-source/ # 数据源抽象层(技术约束-001/003)
|
||||
│ ├── QmtBridgeRestDataSource.js # QMT Bridge REST 适配器
|
||||
│ ├── data-source-types.js # 统一数据模型(Position/DataSource 接口)
|
||||
│ └── QmtHealthMonitor.js # QMT 连接健康检查(数据源健康缓存)
|
||||
├── storage/ # 存储层(R-008 SQLite)
|
||||
│ ├── SqliteStore.js # SQLite 存储封装(node:sqlite)
|
||||
│ └── DataStore.js # 存储门面(对外兼容 API + 迁移)
|
||||
├── position/ # 分仓业务逻辑
|
||||
│ └── PositionManager.js # 持仓↔策略份额分配、聚合视图
|
||||
├── market/ # 行情域
|
||||
│ ├── MarketDataHub.js # 行情缓存(内存 + 落盘 + 查询)
|
||||
│ └── MarketFeed.js # 行情获取(WS + REST 轮询 + prime)
|
||||
├── trades/ # 交易域
|
||||
│ └── TradeSync.js # 交易记录定时同步(QMT → SQLite)
|
||||
└── client/ # 客户端(UI)
|
||||
├── index.js # 客户端入口:tab/settings 注册
|
||||
├── market/ # MarketDataProvider.jsx(行情 context)
|
||||
└── views/ # React 组件(StrategyTab / SettingsSection / Toast 等)
|
||||
```
|
||||
|
||||
2. **死代码清理**:删除 src/component/AllocationStorage.js(旧 allocations.json 存储,0 引用);删除 DataStore.setDataset/removeDataset(PositionManager 适配单票生命周期后无调用方);
|
||||
3. **构建与脚本同步**:tsdown.config.ts entry 更新为新目录 glob;scripts/*.mjs 导入路径更新;
|
||||
4. **验证**:typecheck + build 通过;test-r009 回归 14/14 通过(独立数据目录)。
|
||||
|
||||
## 归类规则(沉淀为技术约束-016)
|
||||
|
||||
- 服务端代码禁止平铺,按功能域分目录:data-source / storage / position / market / trades;
|
||||
- api/ 按领域拆子文件;client/ 仅放 UI(views/ 组件 + market/ provider);
|
||||
- 文件命名 = 类名 PascalCase + .js/.jsx;
|
||||
- 新增服务端模块必须先落对应域目录,无合适域时先讨论补域,不得回退平铺。
|
||||
|
||||
## R-013 新增改动落点(本迭代实施)
|
||||
|
||||
```
|
||||
src/settings.js # +configSchema(策略 schema 扩展)
|
||||
src/storage/SqliteStore.js # +_ensureHoldingValuesColumn + readValues/writeValues
|
||||
src/position/PositionManager.js # +updateHoldingValues(按 configSchema 校验)
|
||||
src/api/strategies.js # +holdings/values-update 端点;strategy-positions 附加 values
|
||||
src/client/views/SettingsSection.jsx # 「策略分组」策略行展开配置字段
|
||||
src/client/views/StrategyTab.jsx # 持仓行展开区「自定义字段」编辑
|
||||
scripts/test-r013-custom-fields.mjs # 回归脚本(独立数据目录)
|
||||
```
|
||||
|
||||
## 后续动作(2026-09-02 结构约束对齐)
|
||||
|
||||
归类重构落地后,将活动文档中的旧路径引用同步到新结构(历史档案按追加式原则不改写):
|
||||
- docs/03-设计约束/数据存储设计.md:三处「对应实现」表 12 处 src/component/* → 新域目录(storage/position/market/trades);
|
||||
- docs/05-需求池/R-013.md:涉及改动面 SqliteStore → src/storage/;
|
||||
- docs/02-计划/计划-策略自定义字段配置.md:文件树 → storage/SqliteStore.js;
|
||||
- 本迭代技术实现方案:1.2 节标题路径 + §2 PositionManager 路径标注 → 新目录;
|
||||
- 保留不改:迭代 04 说明/复盘、R-008 归档、旧计划中的 component/ 描述(历史事实)。
|
||||
@@ -0,0 +1,48 @@
|
||||
# 迭代复盘:11-策略自定义字段配置(定义随策略,值落库 + 列显隐)
|
||||
|
||||
> 复盘日期:2026-09-02 | 迭代状态:**已完成(老师确认)**
|
||||
> 关联需求:R-013(策略自定义字段配置,已定稿 Q1-Q4 + D6)
|
||||
> 关联计划:PLAN-012(计划-策略自定义字段配置)
|
||||
|
||||
## 结果
|
||||
|
||||
迭代 11 达成:策略支持**自定义字段**(定义随策略 configSchema 存 settings 不落库,值落 strategy_holdings.values JSON 列),字段作为策略持仓表**列**展示,支持**列显隐/排序**(每策略独立配置)、**单元格点击编辑**(含单位、可编辑视觉记号)。配置页「策略分组」策略行展开配置字段。旧策略(无 configSchema)行为与现状一致。
|
||||
|
||||
## 过程事实
|
||||
|
||||
1. **需求定稿(R-013)**:老师提出每个策略可加自定义字段 → 多轮讨论收敛(定义不落库随策略取 / 值落库 / 类型化 text·number·boolean·enum / 枚举有用 / 旧策略兼容 Q4 / 值编辑入口 D6)→ 老师确认定稿,端到端示例入档;
|
||||
2. **范围扩展(列显隐,老师 2026-09-02)**:字段升级为持仓表列 + 统一显隐管理(不限于自定义字段,含基础数据列);除 代码/名称/操作 外全部可配置;每策略独立;入口 = tab 顶部「列设置」;支持列排序 → visible 单一数据源(strategyColumns,configSchema 不加 visible)老师确认;
|
||||
3. **实现**:settings.js(configSchema + COLUMN_META + strategyColumns + 归一化)、SqliteStore(values 列幂等 ALTER + readValues/writeValues)、DataStore(透传)、PositionManager(updateHoldingValues 按 configSchema 校验 + getStrategyPositions 附 values)、api(holdings/values-update + strategy-columns 端点)、index.js(注入 getStrategySchema);
|
||||
4. **UI**:StrategyFieldsEditor(设置页字段定义编辑器)、StrategyTab 动态列化 + 列设置弹层 ColumnSettingsPopover + 单元格点击编辑 FieldCellEditor;自定义字段从展开交易明细移出(老师指示,字段已列化);
|
||||
5. **UI 细节(老师验收反馈迭代)**:✎ 可编辑记号(值后)、编辑态 ✓/✕ 确认按钮、切换编辑列宽稳定(输入框内容宽度)、数字输入框统一 5 位宽、字段 unit 单位(可为空,展示拼值后);
|
||||
6. **修复**:配置页白屏(SettingsSection Fragment/StrategyFieldsEditor 未导入 → 运行时 ReferenceError,checkJs:false 未捕获);z.dict dts 推断引用 cosmokit(显式类型注解);
|
||||
7. **验证**:typecheck + build 通过;回归 test-r013-custom-fields 21/21、test-r013-columns 16/16、test-r011-tabs 23/23、test-r009 14/14;
|
||||
8. **提交**:aac16b9(R-013 完整实现 + 列显隐 + 单元格编辑),已推送 origin/main。
|
||||
|
||||
## 经验教训(复盘沉淀)
|
||||
|
||||
### 1. JSX 运行时错误 vs typecheck 盲区(白屏根因)
|
||||
- 项目 checkJs:false → .jsx 不校验未导入标识符;`<Fragment>` 未 import / 组件未 import 只在运行时 ReferenceError → 整个组件树崩溃白屏;
|
||||
- **沉淀**:jsx 改动必须自查「使用的 JSX 大写标签是否有 import/定义」(typecheck 不兜底);已建议加 client 构建期 lint 或保持自查清单。
|
||||
|
||||
### 2. z.dict 的 dts 推断陷阱
|
||||
- schemastery z.dict 返回值类型引用 cosmokit Dict → dts 生成报「cannot be named without reference」;
|
||||
- **沉淀**:settings schema 用 z.dict 时给变量加显式 Schema 类型注解(@type import('@deepseek-ai/schemastery').Schema<any>)规避。
|
||||
|
||||
### 3. SQLite 关键字列名
|
||||
- values 是 SQLite 保留字(INSERT VALUES)→ ALTER/SELECT/UPDATE 裸写报语法错误;
|
||||
- **沉淀**:列名需双引号 `"values"`(PRAGMA 检测用裸名 OK);若可改名优先避开关键字。
|
||||
|
||||
### 4. .map(this._mapHolding) 的 this 丢失
|
||||
- 方法内新增调用 this._parseValues 后,`.map(this._mapHolding)` 裸传方法引用 → this 丢失 TypeError;
|
||||
- **沉淀**:map 回调若依赖 this,改模块级纯函数或 `(row) => this._mapHolding(row)`。
|
||||
|
||||
### 5. 编辑态控件宽度 vs 表格列宽稳定
|
||||
- 固定宽输入框(90px)进入编辑使列宽跳变;改内容宽度(ch 随文本)+ 数字固定 5 位宽 + 紧凑 ✓/✕ 后变化最小;
|
||||
- **沉淀**:表格内联编辑控件宽度应贴近展示态(内容宽度/位数基准),按钮用紧凑 inline(浮层会溢出遮挡相邻单元格)。
|
||||
|
||||
## 遗留/后续
|
||||
|
||||
1. **T-006 数据池 + 策略表格动态字段配置**(需求池起草):本迭代的列显隐/字段列化是其「表格列配置」子集的落地,数据池中间层仍未做——后续如需「列名→数据池字段映射/多源字段」再评估;
|
||||
2. **列排序交互**:本期列设置用 ↑↓ 箭头(零依赖);如老师要 Tab 设置同款拖拽可后续换 DnD;
|
||||
3. **值校验服务端已实现**,前端编辑即时提示靠服务端错误 Toast——如需前端预校验可后续加。
|
||||
@@ -0,0 +1,21 @@
|
||||
# 迭代目标:11-策略自定义字段配置(定义随策略,值落库)
|
||||
|
||||
> 迭代编号:11 | 创建:2026-09-02 | 状态:进行中
|
||||
> 依据计划:PLAN-012 | 需求:R-013(已定稿,2026-09-02,老师确认 Q1-Q4 + D6)
|
||||
|
||||
## 目标描述
|
||||
|
||||
给策略增加**可自定义、可扩展**的字段能力:每个策略在设置页「策略分组」子 tab 配置自定义字段定义(configSchema:key/label/type/enum/默认值,随策略定义存 settings 不落库);该策略下每个持仓(strategy_holdings 行)按所属策略的定义存取一份键值对值(新增 values JSON 列落库);自定义字段作为策略持仓表**列**展示,点击单元格内联编辑值(列显隐与顺序每策略独立配置)。旧策略(无定义)行为与现状完全一致。
|
||||
|
||||
## 目标分解
|
||||
|
||||
1. 数据层:settings.strategies 扩展 configSchema(schemastery,type union text|number|boolean|enum),读取归一化(旧项缺省 []);strategy_holdings 新增 values TEXT 列(幂等 ALTER)+ readValues/writeValues;
|
||||
2. API 层:strategy-positions 每行附 values;新增 holdings/values-update {holdingId, values} 写回(服务端按 configSchema 校验:数字/枚举/布尔);
|
||||
3. 设置页 UI:「策略分组」子 tab 策略行可展开 → 字段列表 + 添加/编辑/删除字段表单 + 保存;
|
||||
4. 策略持仓 UI:自定义字段列表格列展示 + 单元格点击内联编辑(四类型);列显隐/顺序每策略独立配置(顶部「列设置」);
|
||||
5. 回归脚本(独立数据目录,技术约束-011)+ 验证 + 验收复核。
|
||||
|
||||
## 对老师的配合需求
|
||||
|
||||
- 验收:设置页配置字段定义(四种类型各一 + 枚举)→ 策略持仓 tab 持仓行展开编辑值 → 重启验证持久化;
|
||||
- 提供:是否接受「旧策略无定义」行为样(默认接受,Q4 已确认)。
|
||||
@@ -0,0 +1,22 @@
|
||||
# 验收标准:11-策略自定义字段配置
|
||||
|
||||
> 迭代编号:11 | 依据:PLAN-012 验收要点 + R-013
|
||||
|
||||
## 验收标准线
|
||||
|
||||
1. 设置页「策略分组」:策略行可展开,添加 / 编辑 / 删除字段(四种类型:文本/数字/布尔/枚举,含枚举选项与默认值)保存后生效,重启后定义仍在;
|
||||
2. 策略持仓 tab:自定义字段以**列**形式展示(仅该策略配置了字段时),点击字段单元格内联编辑(文本/数字=输入框、布尔=开关、枚举=下拉),Enter/选择/开关即保存,刷新后仍在(已落库);展开区仅交易明细,不再含自定义字段;
|
||||
3. 同策略多行各有各的值(600719 与 300057 互不影响);不同策略字段集互不影响;
|
||||
4. 旧策略(无 configSchema):持仓行不渲染字段区,现有操作(加仓/减仓/清仓/展开交易记录)与现状完全一致;
|
||||
5. 服务端校验生效:数字填非数字、枚举填选项外 → 拒绝保存并 Toast 提示;空值/缺省允许;
|
||||
6. 兼容迁移:既有 strategy_holdings 数据行(无 values)补列后为 NULL,读取正常,不报错;
|
||||
7. build + typecheck 通过;回归脚本(test-r013-custom-fields.mjs,独立数据目录)全绿。
|
||||
|
||||
## 验收方法
|
||||
|
||||
- 回归脚本跑通(隔离数据目录);
|
||||
- 老师人工验收:设置页配置网格超市 4 字段 → 策略持仓 tab 两个持仓行展开分别编辑值 → 另配长线持有 2 字段观察互不影响 → 手动做T(旧策略)确认无字段区 → 重启后确认定义与值均保留。
|
||||
|
||||
## 验收目标
|
||||
|
||||
- 7 条验收线通过,迭代 11 标记「验收通过」,R-013 更新实现状态(已实现),归档。
|
||||
@@ -0,0 +1,85 @@
|
||||
# 技术实现方案:12-持仓内存快照
|
||||
|
||||
> 迭代编号:12 | 依据:PLAN-013 + R-014(老师四问拍板)+ 技术约束-010/012/016;新增 技术约束-017
|
||||
|
||||
## 1. PositionSync(src/position/PositionSync.js,新增)
|
||||
|
||||
### 1.1 数据结构与生命周期
|
||||
|
||||
```js
|
||||
class PositionSync {
|
||||
snapshot = []; // 内存快照:Position[](mapPosition 语义化输出;只读约定)
|
||||
syncedAt = 0; // 最近成功同步毫秒时间戳(0 = 从未成功)
|
||||
_ghostMiss = new Map(); // 幽灵防抖:code → 连续消失轮数
|
||||
_lastAccountId = null; // 账户身份守卫
|
||||
stats = { syncCount, failCount, lastError, closedGhosts, lastSyncedAt };
|
||||
}
|
||||
```
|
||||
|
||||
- `start()`:立即预热一次(syncNow,失败仅 warn)+ `setInterval(10s)`;`stop()` 清定时器,**内存快照保留**(stop 后读方仍可消费最后快照);
|
||||
- 对外读:`getSnapshot()`(数组本体,只读约定)/`getSyncedAt()`/`backfill(positions)`(读穿透回填入口,仅非空回填);
|
||||
- `syncNow()` 可手动调用(**不检查 mounted**——测试与热切换后手动刷新均可用;仅 syncing 防重入)。
|
||||
|
||||
### 1.2 同步一轮(syncNow)
|
||||
|
||||
```
|
||||
mounted 不拦手动同步 → syncing 防重入
|
||||
positions = dataSource.getPositions() // 全量
|
||||
非数组 → 抛错(走失败分支)
|
||||
空数组 → 双重确认:isAvailable() && getAsset().accountId
|
||||
双通过 → 接受为「真清仓」(接受空快照)
|
||||
否则 → 视为 QMT 异常(未登录/半可用),保留旧快照,failCount++
|
||||
成功 → snapshot = positions(整体替换);syncedAt = now;stats.syncCount++
|
||||
→ _autoCloseGhosts(snapshot)(失败不影响快照)
|
||||
任何异常 → failCount++ / lastError 记录;不动内存(读方继续消费旧快照)
|
||||
```
|
||||
|
||||
### 1.3 幽灵持仓自动清仓(_autoCloseGhosts,老师拍板「同步时自动清仓」)
|
||||
|
||||
- 判定对象:本地 `getCurrentHoldings()`(全部策略、closed_at IS NULL)中 **code 不在本轮快照**的条目;
|
||||
- 防抖:连续 3 轮(`ghostRounds=3`,约 30s)消失才清仓;快照复现 → 计数复位;
|
||||
- 清仓动作:该 code **全部策略**的当前持仓 `closeHolding(strategyId, code)`(shares=0 + closed_at=now,**不物理删除**,历史保留语义不变);
|
||||
- **账户身份守卫**(防误清核心):
|
||||
- 每轮顺带 `getAsset().accountId`;身份未知 → 当轮**跳过判定**(不累计不清零,保守);
|
||||
- accountId 变化(R-004 连接热切换/换账户)→ **清空计数 + 当轮直接跳过**(旧账户快照不可信);
|
||||
- 部分减持不触发(QMT 仍有该 code)——账实差额由前端「未分配为负」暴露,属产品已知行为;
|
||||
- 单码清仓失败:保留计数,下轮重试。
|
||||
|
||||
## 2. 读路径切换(src/position/PositionManager.js)
|
||||
|
||||
```js
|
||||
constructor({ dataSource, storage, getStrategySchema, positionSync }) // positionSync 可选注入
|
||||
|
||||
async getAllPositions() {
|
||||
if (!this.positionSync) return this.dataSource.getPositions(); // 未注入 → 旧穿透(兼容)
|
||||
const cached = this.positionSync.getSnapshot();
|
||||
if (cached.length > 0) return cached; // 主路径:读快照
|
||||
const positions = await this.dataSource.getPositions(); // 读穿透:当场拉一次
|
||||
this.positionSync.backfill(positions); // 回填(仅非空)
|
||||
return positions; // QMT 也挂 → 抛错(前端 LoadState 重试)
|
||||
}
|
||||
```
|
||||
|
||||
- strategy-positions / unallocated / summary 三个接口全部经 getAllPositions,**自动切换,前端零改动**;
|
||||
- 闸门校验(addToStrategy 不超实盘)改用快照总量——与页面显示的未分配数**同源同鲜度**,自洽。
|
||||
|
||||
## 3. 装配(src/index.js)
|
||||
|
||||
```js
|
||||
const positionSync = new PositionSync({ runtime: { dataSource, storage }, logger });
|
||||
positionSync.start();
|
||||
const manager = new PositionManager({ dataSource, storage, positionSync, getStrategySchema });
|
||||
registerApi(ctx, { ..., positionSync });
|
||||
// dispose:positionSync.stop()(在 marketFeed.stop 之后、tradeSync.stop 之前)
|
||||
```
|
||||
|
||||
## 4. 回归脚本(scripts/test-position-sync.mjs,纯内存 mock)
|
||||
|
||||
- **不落库不碰真实数据目录**(内存快照方案下技术约束-011 天然满足);
|
||||
- mock:可编程 dataSource(positions/accountId/healthy 可变状态)+ 内存 storage(getCurrentHoldings/closeHolding 记录调用);
|
||||
- 34 项用例:基本流 / 失败保留快照 / 空快照四分支(health 挂、身份未知、双通过、有旧快照)/ 幽灵防抖(3 轮关闭、复现复位、账户切换重置、身份丢失跳过+恢复后关闭)/ 读路径(快照零 QMT 调用、读穿透回填、未注入兼容、QMT 挂抛错)/ getStrategyPositions 组装回归(shares/holdingId/lastTradePrice/values)/ 定时器冒烟。
|
||||
|
||||
## 5. 验证
|
||||
|
||||
- typecheck + build 通过;test-position-sync 34/34;test-r013-custom-fields 21/21(未注入 positionSync 兼容性证明);
|
||||
- 老师人工验收(见验收标准)。
|
||||
@@ -0,0 +1,46 @@
|
||||
# 迭代复盘:12-持仓内存快照(服务端 10s 定时同步,请求不再穿透 QMT)
|
||||
|
||||
> 复盘日期:2026-09-02 | 迭代状态:**已实施,待老师人工验收**
|
||||
> 关联需求:R-014(持仓内存快照,已定稿)
|
||||
> 关联计划:PLAN-013(计划-持仓内存快照)
|
||||
|
||||
## 结果
|
||||
|
||||
迭代 12 达成:服务端建全量持仓**内存快照**(PositionSync,10s 定时全量同步,**不落库**),策略持仓 / 全部持仓 / 未分配三个接口改读快照为准;同步失败保留上次快照(QMT 抖动不再白屏);空快照双重确认(health + 账户身份);快照为空读穿透兜底;**幽灵持仓自动清仓**(连续 3 轮消失 + 账户身份守卫防误清)。前端零改动,SQLite 零 schema 变更。
|
||||
|
||||
## 过程事实
|
||||
|
||||
1. **起因(架构梳理讨论)**:老师要求梳理持仓/实盘数据存储与同步机制 → 梳理结论「本地数据准确性靠打开 tab 时与实盘当场对账,无对账任务、无修正、无告警」+ 幽灵持仓盲区(QMT 卖光的票本地账本仍记着,UI 隐身)→ 老师提出优化方向(服务端缓存 + 10s 同步 + 策略持仓改读缓存);
|
||||
2. **方案反转(落库 → 内存)**:AI 初版方案建 positions_cache 表(惯性抄 market_quotes_cache / trade_fills 先例);老师质疑是否可复用 strategy_holdings 或放内存 → AI 论证修正:**判断落库的标准是「数据能否随时一次调用重拿全」**,持仓快照满足,落库反引入「过期快照冒充实时的说谎风险」;strategy_holdings 是账本不可掺对账单(holding_id 是交易归属锚点)→ 定稿纯内存方案;
|
||||
3. **四问拍板**(老师,2026-09-02):① 内存不落库;② 读穿透兜底(首启/QMT 从未连上时当场拉一次并回填);③ 幽灵持仓同步时自动清仓(加防抖护栏);④ 同步时间前端本轮不显示;
|
||||
4. **实现**:PositionSync(快照 + 定时 + 空快照双确认 + 幽灵清仓防抖 + 账户守卫 + stats)、PositionManager.getAllPositions 读快照 + 读穿透 + 未注入兼容、index.js 装配、回归脚本 34 项;
|
||||
5. **修复两处**(测试驱动发现):syncNow 的 mounted 守卫拦住了手动同步(测试直接调用返回 null)→ 移除该守卫(mounted 只管定时循环,手动同步/热切换后刷新不受限);账户切换守卫从「重置后当轮继续计数」收紧为「当轮直接跳过」(旧账户快照不可信);
|
||||
6. **验证**:typecheck + build 通过;test-position-sync 34/34;test-r013-custom-fields 21/21(未注入 positionSync 旧行为兼容证明);
|
||||
7. **文档链补全**(先实施后补档):R-014 + PLAN-013 + 迭代三件套 + 技术约束-017 + 数据存储设计.md §11;修正一处归档操作(R-014 未验收先写了 已完成/,按规范移回需求池根目录)。
|
||||
|
||||
## 经验教训(复盘沉淀)
|
||||
|
||||
### 1. 「抄先例」要过适用性检查
|
||||
- 行情缓存落库(重启首屏有价)与交易落库(QMT 只有当日数据,不存即丢)各有硬理由;持仓快照两个理由都不占。判断标准沉淀:**外部可重取的实时投影不落库**(落库换不来任何能力,只带来说谎风险);
|
||||
- 数据该放哪一层,先问三个问题:不可再生吗?重启首屏依赖吗?有读放大或复杂查询需求吗?——全否 → 内存。
|
||||
|
||||
### 2. 账本与对账单分离
|
||||
- strategy_holdings(人为分配、生命周期、holding_id 锚点)与实盘持仓快照(外部投影、易变)是两类数据;复用同一张表会污染交易归属链。合并的诱惑来自「都是持仓」,分辨的依据是「谁写、谁信、活了多久」。
|
||||
|
||||
### 3. 幽灵清仓的误清防线 = 防抖 × 身份守卫
|
||||
- 单纯「消失即清仓」在 QMT 半可用(未登录返回空)和账户热切换两个场景都会误清;连续 3 轮 + accountId 守卫(未知跳过 / 切换重置跳过)把误清窗口压到可忽略;
|
||||
- 沉淀:**对「删/清」类自动化动作,默认加两道独立护栏再上**。
|
||||
|
||||
### 4. mounted 守卫的语义边界
|
||||
- 定时器服务的 mounted 应只表达「定时循环是否运行」,不应拦截手动触发(syncNow/sync 类方法);否则测试、手动刷新、热切换后的即时同步全被误伤。
|
||||
|
||||
### 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-需求池/已完成/。
|
||||
@@ -0,0 +1,26 @@
|
||||
# 迭代目标:12-持仓内存快照(服务端 10s 定时同步,请求不再穿透 QMT)
|
||||
|
||||
> 迭代编号:12 | 创建:2026-09-02 | 状态:已实施,待验收
|
||||
> 依据计划:PLAN-013 | 需求:R-014(已定稿,2026-09-02,老师逐项拍板 4 问)
|
||||
|
||||
## 目标描述
|
||||
|
||||
消除「QMT 抖动 → 持仓页面白屏」:服务端建全量持仓**内存快照**(不落库,老师拍板),PositionSync 每 10 秒与 QMT 全量同步一次,以快照为持仓数据的唯一读取源;策略持仓 / 全部持仓 / 未分配三个接口不再在请求时穿透 QMT;同步失败保留上次快照;快照为空读穿透兜底;幽灵持仓(QMT 已卖光、本地账本仍记着的条目)在同步循环中自动清仓(带防抖护栏)。
|
||||
|
||||
## 目标分解
|
||||
|
||||
1. **PositionSync**(src/position/PositionSync.js):启动预热一次 + setInterval(10s) 全量拉 /trade/positions → 格式校验 → 整体替换内存快照;失败记 stats 不动内存;空快照双重确认(isAvailable + getAsset accountId);
|
||||
2. **读路径切换**(PositionManager.getAllPositions):有注入 positionSync → 读快照(空则读穿透回填);未注入 → 旧穿透行为(兼容既有构造点,如 test-r013);
|
||||
3. **幽灵自动清仓**(同步循环内):本地当前持仓 code ∉ QMT 快照,连续 3 轮 → 全部策略 closeHolding 转历史;护栏:accountId 未知当轮跳过、账户切换当轮重置跳过、单码清仓失败保留计数下轮重试;
|
||||
4. **装配**(index.js):start + dispose + 注入 manager/api runtime;
|
||||
5. **回归脚本**(scripts/test-position-sync.mjs,纯内存 mock 34 项)+ typecheck + build + 既有回归(r013)。
|
||||
|
||||
## 背景事实(本迭代为「先实施后补档」)
|
||||
|
||||
- 迭代代码于 2026-09-02 架构梳理讨论中当场实施(老师指令「开工吧」),讨论中老师四问拍板(落库质疑改内存 / 读穿透 / 幽灵自动清仓 / 不显示同步时间);
|
||||
- 文档链(R-014 / PLAN-013 / 本迭代三件套 / 技术约束-017 / 数据存储设计 §11)事后补全,实现与讨论结论一致。
|
||||
|
||||
## 对老师的配合需求
|
||||
|
||||
- **人工验收**:真实环境重载插件 → QMT 正常时开策略持仓/全部持仓 tab 数据正常 → 停掉 QMT Bridge 刷新页面(数据应保留上次快照而非白屏)→ 恢复 QMT → 在券商端卖出某只票全部持仓,约 30s 后确认本地持仓行自动转历史;
|
||||
- 验收通过后:R-014 移入 已完成/ 归档、迭代 12 标记验收通过。
|
||||
@@ -0,0 +1,22 @@
|
||||
# 验收标准:12-持仓内存快照
|
||||
|
||||
> 迭代编号:12 | 依据:PLAN-013 验收要点 + R-014
|
||||
|
||||
## 验收标准线
|
||||
|
||||
1. **自动化**:typecheck + build 通过;回归脚本 test-position-sync.mjs 全绿(34 项:基本流 / 失败保留快照 / 空快照双重确认四分支 / 幽灵清仓防抖与账户守卫 / 读穿透兜底 / 未注入兼容 / 组装回归 / 定时冒烟);既有回归 test-r013-custom-fields.mjs 全绿(21 项,兼容性证明);
|
||||
2. **读路径**:插件启动日志出现「PositionSync 启动」;QMT 正常时打开策略持仓 / 全部持仓 tab,数据与改造前一致(行集合 / 份额 / 成本价 / 最后一笔成交价 / 自定义字段值);连续切 tab 不产生对 QMT 的 /trade/positions 新调用(服务端日志无新增穿透请求);
|
||||
3. **抗抖**:停掉 QMT Bridge 后刷新页面,持仓 tab 仍显示**最后一次快照数据**(不再白屏);恢复 QMT 后 ≤10s 快照自动恢复最新;
|
||||
4. **幽灵自动清仓**:在券商端卖出某只票全部持仓后,约 30s(3 轮同步)内,该票在全部策略下的当前持仓自动转历史(strategy_holdings 出现 closed_at,不物理删除);服务端日志出现「PositionSync 幽灵清仓」warn;
|
||||
5. **防误清**:QMT 短暂返回空(如未登录)时本地持仓不被清空(保留旧快照);连接热切换到另一账户时旧计数重置,无误清;
|
||||
6. **零 schema 变更**:SQLite 无新表、strategy_holdings 结构不变;前端代码零改动。
|
||||
|
||||
## 验收方法
|
||||
|
||||
- 自动化项由 AI 执行并出具结果(已完成:34/34 + 21/21 + typecheck + build);
|
||||
- 2~5 项老师真实环境人工验收:重载插件 → 正常浏览持仓 tab → 停 QMT 验证抗抖 → 恢复 → 券商端清仓一票验证幽灵自动转历史;
|
||||
- 全部通过后:迭代 12 标记「验收通过」,R-014 归档至 已完成/。
|
||||
|
||||
## 验收目标
|
||||
|
||||
- 6 条验收线通过,迭代 12 标记「验收通过」,R-014 更新实现状态(已实现)并归档。
|
||||
@@ -0,0 +1,106 @@
|
||||
# 技术实现方案:13-盘口内存快照
|
||||
|
||||
> 迭代编号:13 | 依据:PLAN-014 + R-015(老师拍板:方案 A 替换 / WS 移除 / 纯内存 DROP / watch 维持现状 / 指示灯推荐逻辑)
|
||||
|
||||
## 1. 数据源扩展(QmtBridgeRestDataSource.getTicks)
|
||||
|
||||
```js
|
||||
/** 批量盘口(GET /data/tick?codes=a,b,c)→ { [code]: snapshot }(保留原始字段 + updatedAt) */
|
||||
async getTicks(codes) {
|
||||
// j.ok && j.data → data 原样返回(tick 结构已有语义字段 lastPrice/lastClose/open/high/low/... + time/timetag)
|
||||
// 每 snapshot 统一附加 updatedAt = Date.now()(服务端收到时刻,价龄判断基准)
|
||||
}
|
||||
```
|
||||
|
||||
- 首次启用 /data/tick 的适配层通道(此前 MarketDataHub 自己 fetch,绕过了适配层——本轮修正层级);
|
||||
- 涨停跌停走既有 getInstrument(code)(首次投入使用):upStopPrice/downStopPrice/preClose。
|
||||
|
||||
## 2. QuoteHub(src/market/QuoteHub.js,新)
|
||||
|
||||
```js
|
||||
class QuoteHub {
|
||||
quotes = new Map(); // code → snapshot(全量 tick 字段 + updatedAt;只读约定)
|
||||
instruments = new Map(); // code → { upStopPrice, downStopPrice, preClose, tradingDay }(交易日内存缓存)
|
||||
watchCodes = new Set(); // 关注集合(维持现状只进不出,无上限——老师拍板)
|
||||
stats = { syncCount, failCount, lastError, lastSyncedAt, restCount, watchSize };
|
||||
}
|
||||
```
|
||||
|
||||
- `getQuotes(codes)`:内存命中 + miss 读穿透(dataSource.getTicks 补拉并回填,等价旧三级命中的内存→REST 两级,磁盘层删除);
|
||||
- `getQuote(code)`:单码便捷;`watch(codes)`:关注集合登记(QuoteSync prime/刷新、api 查询共用);
|
||||
- `ingest(data, { source })`:写入统一入口(source 标签:rest|instrument——WS 将来回归时加 source 即可,入口不变);
|
||||
- 合约信息:`getInstrumentCached(code, tradingDay)`——QuoteSync 判定交易日变化后失效重拉(涨停跌停当天不变)。
|
||||
|
||||
## 3. QuoteSync(src/market/QuoteSync.js,新;与 PositionSync 同构)
|
||||
|
||||
```
|
||||
start(): prime(持仓 code 盘口,走 hub.watch + getTicks 批量)+ setInterval(5s)
|
||||
每轮:codes = [...hub.watchCodes](维持现状)
|
||||
分批 50 → dataSource.getTicks(batch) → hub.ingest(data, {source:'rest'})
|
||||
watch 中无合约缓存或 tradingDay 变化的 code → getInstrument 懒拉(失败不阻塞本轮)
|
||||
失败:stats.failCount++ / lastError;**内存不动(保留上次价)**
|
||||
setBaseUrl(url):热切换 → 清空 instruments 缓存(新账户/新市场环境)+ 立即 prime 一次
|
||||
stop(): 清定时器;内存保留
|
||||
getSyncedAt() / getStatus(): 指示灯数据源
|
||||
```
|
||||
|
||||
- 对称性:PositionSync(10s/持仓)/ QuoteSync(5s/盘口)/ TradeSync(60s/交易)三域同构,全部「失败保留、stats 可观测、手动 syncNow 可触发」。
|
||||
|
||||
## 4. 存储清理(market_quotes_cache 退役,老师拍板 DROP)
|
||||
|
||||
- SqliteStore:SCHEMA_SQL 删建表;新增 `DROP TABLE IF EXISTS market_quotes_cache`(init 内幂等执行,存量库清理);删 getMarketQuote/getMarketQuotes/setMarketQuotes/_mapQuote;migrateJson 删行情迁移段;isEmpty 只看 strategy_holdings;
|
||||
- DataStore:删 loadMarket(return this 残迹,warmup bug 根源)/getMarketQuote/getMarketQuotes/setMarketQuotes/marketLoaded;
|
||||
- 存量 store.market.json 的 .bak 不动(历史备份无碍)。
|
||||
|
||||
## 5. API(src/api/market.js + 新 sync-status)
|
||||
|
||||
```
|
||||
market-snapshot { codes } → { [code]: { lastPrice, lastClose, upStopPrice, downStopPrice, updatedAt, ...tick 字段 } }
|
||||
sync-status → {
|
||||
position: { syncedAt, ageMs, fresh: bool, snapshotSize, failCount, lastError, periodMs },
|
||||
quote: { syncedAt, ageMs, fresh: bool, snapshotSize, watchSize, failCount, lastError, periodMs },
|
||||
qmt: { ...qmtHealthMonitor.getStatus() }
|
||||
}
|
||||
sync-now { domain: 'position' | 'quote' } → 各 Sync.syncNow()(手动触发,指示灯点击用)
|
||||
market-stats 端点删除(并入 sync-status)
|
||||
```
|
||||
|
||||
- 状态判定:fresh = syncedAt 在 3×周期内(持仓 30s / 盘口 15s);前端按 ageMs 二次校准显示。
|
||||
|
||||
## 6. 前端(SyncIndicators.jsx 新 + 两处清理)
|
||||
|
||||
- **SyncIndicators**(挂 QmtConnectionChip 内 QmtHealthIndicator 右侧):两圆点「持」「行」;
|
||||
- 10s 轮询 sync-status;点击圆点 → sync-now({domain}) → 刷新;
|
||||
- 颜色:green(fresh) / yellow(stale) / gray(never);title 含同步时间、快照量、失败数、lastError;
|
||||
- 样式复用 QmtHealthIndicator 圆点(10px 圆 + glow),色值走 --dsw-* token;
|
||||
- MarketDataProvider:删 `wsInfo: null` 残留;轮询/注册逻辑不变(getByCodes 签名兼容);
|
||||
- QmtConnectionChip:挂载 SyncIndicators(健康灯右侧)。
|
||||
|
||||
## 7. 装配(src/index.js)
|
||||
|
||||
```js
|
||||
const marketHub = new QuoteHub({ logger });
|
||||
const marketFeed = new QuoteSync({ hub: marketHub, runtime: { settings, dataSource }, logger });
|
||||
marketFeed.start(startup.baseUrl ?? config?.qmtBaseUrl);
|
||||
// dispose:marketFeed.stop()(保留旧变量名,改动最小;注释标注新职责)
|
||||
```
|
||||
|
||||
- registerApi 入参 marketHub/marketFeed 对象形态不变(api/market.js 内部改用新方法);
|
||||
- QmtHealthMonitor 不动。
|
||||
|
||||
## 8. 回归脚本(scripts/test-quote-sync.mjs,纯内存 mock)
|
||||
|
||||
- mock:可编程 dataSource(ticks/instrument/positions 可变状态);
|
||||
- 用例:prime + 5s 刷新基本流 / 失败保留上次价 / watch 空不空转 / 读穿透回填 / 涨停跌停按交易日缓存与日切失效 / 热切换清合约缓存 / sync-status 状态推导(绿黄灰边界)/ 存储清理(DROP 幂等:SqliteStore 临时目录建旧结构库 → init 后表消失且持仓数据无损)。
|
||||
|
||||
## 9. 策略持仓表行情列(老师确认并入本轮)
|
||||
|
||||
- **settings.js**:COLUMN_META 追加 4 项(均 `defaultVisible: false`)——`{ key:'upStopPrice', label:'涨停价' } / { key:'downStopPrice', label:'跌停价' } / { key:'open', label:'今开' } / { key:'high', label:'最高' }`;normalizeStrategyColumns 两处适配:默认列构造带 defaultVisible,未覆盖列的缺省显隐从恒 true 改为 `defaultVisible !== false`(存量 strategyColumns 无需迁移——新列缺省即隐藏);
|
||||
- **StrategyTab.renderDataCell** switch 增 4 case:`getPrice(p.code)?.upStopPrice / downStopPrice / open / high`,复用 fmtPrice 纯价格展示(数据缺失显示 —,与现价列同行为);
|
||||
- 列设置弹层零改动(自动从 COLUMN_META 出现在列表,kind=base 标「数据」);
|
||||
- 取值来源:market-snapshot 下发的 tick 字段 + 合约信息(upStopPrice/downStopPrice 来自 QuoteSync 交易日缓存),经 MarketDataProvider prices map join 到行。
|
||||
|
||||
## 10. 验证
|
||||
|
||||
- typecheck + build;test-quote-sync 全绿;存量回归(test-position-sync 34 项 + test-r013 21 项)全绿;
|
||||
- 老师人工验收(见验收标准)。
|
||||
@@ -0,0 +1,43 @@
|
||||
# 迭代复盘:13-盘口内存快照(QuoteSync/QuoteHub + 同步指示灯)
|
||||
|
||||
> 复盘日期:2026-09-02 | 迭代状态:**已实施,待老师人工验收**
|
||||
> 关联需求:R-015 | 关联计划:PLAN-014
|
||||
|
||||
## 结果
|
||||
|
||||
迭代 13 达成:盘口数据与持仓对称收敛——QuoteSync(取数:prime + 5s REST + 涨停跌停按交易日缓存)/ QuoteHub(存查:内存快照 + watch 集合 + 读穿透 + getQuote 单一入口)替换 MarketFeed/MarketDataHub(方案 A 删除重写);WS 通路移除;market_quotes_cache 表 DROP 退役(含 loadMarket 残迹清理,warmup bug 随之消灭);新增涨停/跌停字段(/data/instrument 首次启用);会话头部新增持仓/盘口同步指示灯(绿黄灰 + 点击即同步 + sync-status 端点吸收 market-stats)。
|
||||
|
||||
## 过程事实
|
||||
|
||||
1. **讨论驱动逐项拍板**(2026-09-02,五项):① 替换 vs 包壳 → 方案 A 替换(DataStore.loadMarket 残迹为前车之鉴);② WS 移除(无生效结论 + 全市场推送被过滤 + REST 5s 已覆盖;ingest 留来源标签口子);③ **纯内存不落库**(老师纠正 AI 的「表加两列」方案:缓存表整个退役,价格单一入口 = QuoteHub;AI 复以落库三问复核通过——warmup bug 复现证明落库从未真正生效);④ watchCodes 维持现状不收缩不加限(WS 移除后膨胀仅轻微浪费,切回旧 tab 价格秒显是免费福利);⑤ 指示灯(老师提出):绿黄灰三态不引入红(红留 QMT 健康灯表达连接层)、点击即 syncNow、**PriceCell 灰点方案取消**(管道健康全局灯表达,个股停牌属数据语义另议);
|
||||
2. **warmup bug 实锤**:讨论中复现 DataStore.loadMarket() 返回 DataStore 实例自身 → Object.entries 枚举出 sqlite/loaded/marketLoaded 三个内部属性 → 「重启首屏有价」自上线起从未生效,一直靠 getByCodes 磁盘兜底路径撑着——成为「不落库」决策的关键证据;
|
||||
3. **实现**:getTicks 适配层通道(修正旧 hub 绕过适配层自 fetch 的层级破洞)、QuoteSync/QuoteHub、SqliteStore DROP + 行情方法删除、DataStore 行情方法删除、market-snapshot 扩展字段、sync-status/sync-now 端点、SyncIndicators 组件、wsInfo 清理;
|
||||
4. **验证**:typecheck + build;test-quote-sync 全绿;存量回归 test-position-sync 34/34、test-r013 21/21;
|
||||
5. **文档链**:R-015 + PLAN-014 + 迭代三件套 + 技术约束-018 + 产品约束-012 + 技术约束-010/012 变更(本轮讨论后统一落)。
|
||||
|
||||
## 经验教训(复盘沉淀)
|
||||
|
||||
### 1. 「缓存落库」的隐性成本会以 bug 形式讨债
|
||||
- market_quotes_cache 三件套(loadMarket 残迹 / warmup 假工作 / isEmpty 与 migrateJson 的行情耦合)活了三代迭代才被 root cause——落库换来的「重启首屏有价」实际从未工作,而读穿透 1 秒内就能补齐;
|
||||
- 沉淀:**给「可再生缓存」落库前,先验证它的读取路径真的被走到**(监控 stats.diskHitCount 之类),否则就是无人受益的死重 + 未来 bug 的温床。
|
||||
|
||||
### 2. 同域两个类(Feed/Hub)的「分工」挡不住职责漂移
|
||||
- MarketDataHub 本该只管存查,却自己 fetch REST(绕适配层)、管 watch 过滤、管落库防抖——「取」与「存」的边界在实践中糊掉;
|
||||
- 沉淀:类职责用「它消费谁、被谁消费」检查:QuoteHub 只消费 QuoteSync/dataSource 补拉,只被 api/前端消费;出现第三条边即重构信号。
|
||||
|
||||
### 3. 全局状态灯 > 局部灰点
|
||||
- 老师把 PriceCell 灰点讨论升级为「同步指示灯」,一并覆盖持仓+盘口两域——数据管道健康是全局属性,表达在全局位置(会话头部)比逐格标记更正确,且复用既有健康灯心智;
|
||||
- 沉淀:故障可视化先问「这是单个数据的错,还是管道的错」——后者永远放全局。
|
||||
|
||||
### 4. 交易日是行情静态数据的正确缓存键
|
||||
- 涨停/跌停/昨收「当天不变」的知识决定缓存粒度:按交易日缓存 + 日切失效,而不是跟 5s 同步周期走(省 99% 的合约请求);
|
||||
- 沉淀:为同步数据分类「每周期变(tick)/每日变(合约静态)/每次变(手动归属)」,各自匹配刷新节奏。
|
||||
|
||||
## 遗留/后续
|
||||
|
||||
1. **个股停牌标识**:QMT 正常但单票不出 tick 时价格陈旧且无提示——属数据语义,老师确认另议;
|
||||
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-需求池/已完成/。
|
||||
@@ -0,0 +1,29 @@
|
||||
# 迭代目标:13-盘口内存快照(QuoteSync/QuoteHub + 同步指示灯)
|
||||
|
||||
> 迭代编号:13 | 创建:2026-09-02 | 状态:已实施,待验收
|
||||
> 依据计划:PLAN-014 | 需求:R-015(已定稿,2026-09-02)
|
||||
|
||||
## 目标描述
|
||||
|
||||
把盘口(市场行情)数据收敛为与持仓(迭代 12)对称的「定时同步 + 内存快照」模式:新建 QuoteSync(取数)与 QuoteHub(存查)两个工具类,`market_quotes_cache` 表退役(DROP),价格单一入口 = QuoteHub;扩展涨停价/跌停价(/data/instrument 按交易日缓存);会话头部新增「持仓/盘口」同步指示灯(绿黄灰,点击即同步)。WS 数据通路本轮移除(从无生效结论,老师拍板)。
|
||||
|
||||
## 目标分解
|
||||
|
||||
1. **QuoteSync**(src/market/QuoteSync.js):启动 prime(持仓盘口)+ 5s 定时刷 watch 集合(分批 50)+ 涨停跌停按交易日懒拉缓存(watch 新 code → /data/instrument,日切失效)+ 热切换重置 + stats;失败保留内存不动;WS 不迁移;
|
||||
2. **QuoteHub**(src/market/QuoteHub.js):内存快照 Map + watchCodes Set(维持现状只进不出)+ 合约信息缓存 + 读穿透(走 dataSource.getTicks,不再自 fetch)+ 防抖 3s 写盘逻辑删除(无落库)+ 对外 getQuote/getQuotes/getByCodes/watch/stats;
|
||||
3. **数据源**:QmtBridgeRestDataSource 新增 getTicks(codes)(/data/tick 批量语义化,带 updatedAt/time 原始时间);
|
||||
4. **存储清理**:SqliteStore 删行情表方法 + `DROP TABLE IF EXISTS market_quotes_cache`(幂等)+ migrateJson 行情段/isEmpty 联动收缩;DataStore 删 loadMarket/getMarketQuote(s)/setMarketQuotes;
|
||||
5. **API**:market-snapshot 值对象扩展 {lastPrice,lastClose,upStopPrice,downStopPrice,updatedAt};新增 sync-status(持仓/盘口/QMT 三域状态,吸收 market-stats)+ sync-now {domain: position|quote};
|
||||
6. **前端**:SyncIndicators 组件(QmtHealthIndicator 旁两圆点:持●行●,10s 轮询 sync-status,绿黄灰 + 悬停详情 + 点击调 sync-now);MarketDataProvider 删 wsInfo 残留;QmtConnectionChip 挂载;
|
||||
7. **回归**:scripts/test-quote-sync.mjs(纯内存 mock)+ typecheck + build + 存量回归;
|
||||
8. **策略持仓表行情列**(老师确认并入):列设置新增 涨停价/跌停价/今开/最高 4 列(COLUMN_META 登记,kind=base,defaultVisible: false 默认隐藏);renderDataCell 取值 getPrice(code)?.{upStopPrice|downStopPrice|open|high};纯价格展示(无距离百分比);归一化缺省显隐跟随 defaultVisible;
|
||||
|
||||
## 指示灯规格(老师采纳 AI 推荐逻辑)
|
||||
|
||||
- 阈值:持仓 30s 内绿 / 超过黄、从未灰(周期 10s);盘口 15s 内绿 / 超过黄、从未灰(周期 5s)——**绿黄灰三态,不引入红**(红留给 QMT 健康灯表达连接层故障,同步灯表达数据层陈旧);
|
||||
- 悬停 title:状态 + 最近同步时间 + 快照量 + 失败次数 + lastError 摘要;
|
||||
- 点击:立即触发该域 syncNow(不等下个周期),灯自动刷新。
|
||||
|
||||
## 对老师的配合需求
|
||||
|
||||
- 人工验收(真实环境):重载插件 → 指示灯绿、悬停信息正确 → 停 QMT 后两灯渐黄且页面价格保持旧值 → 恢复 QMT ≤5s 回绿 → 点击指示灯立即同步 → 涨停/跌停列数据正确(对比券商软件)。
|
||||
@@ -0,0 +1,23 @@
|
||||
# 验收标准:13-盘口内存快照
|
||||
|
||||
> 迭代编号:13 | 依据:PLAN-014 验收要点 + R-015
|
||||
|
||||
## 验收标准线
|
||||
|
||||
1. **自动化**:typecheck + build 通过;test-quote-sync.mjs 全绿(基本流/失败保留/读穿透/交易日缓存/热切换/状态推导/DROP 幂等);存量回归 test-position-sync 34/34、test-r013 21/21;
|
||||
2. **存储退役**:插件启动后存量库中 market_quotes_cache 表被 DROP(幂等,重启不报错);持仓数据不受影响;代码中无 DataStore.loadMarket / getMarketQuote(s) / setMarketQuotes 残留;MarketFeed/MarketDataHub 文件已删除;
|
||||
3. **价格功能**:重载插件后持仓/策略 tab 现价、涨幅正常(≤5s 更新);**涨停/跌停数据**在 market-snapshot 返回中正确(与券商软件对照);QMT 挂掉时页面价格保持旧值(不空白),恢复后 ≤5s 自动追上;
|
||||
3b. **行情列**:策略持仓 tab 列设置出现 涨停价/跌停价/今开/最高 4 个新列(默认隐藏,勾选后显示且顺序可调),纯价格展示,无数据的票显示 —;
|
||||
4. **指示灯**:会话头部出现「持」「行」两圆点——正常绿(悬停:同步时间/快照量/失败数);停 QMT 后持仓灯 30s 内、盘口灯 15s 内变黄(数据仍显示旧值);恢复后回绿;**点击圆点立即触发对应域同步**;
|
||||
5. **WS 清理**:日志无「MarketFeed 连接 ws://」;market-stats 端点不再存在(sync-status 替代);前端 wsInfo 残留已删;
|
||||
6. **零 schema 破坏**:strategy_holdings / trade_orders / trade_fills 三表结构与数据完好。
|
||||
|
||||
## 验收方法
|
||||
|
||||
- 自动化项 AI 执行出具结果;
|
||||
- 2~5 老师真实环境人工验收:重载插件 → 查看指示灯与悬停信息 → 对照券商涨停跌停价 → 停/启 QMT Bridge 验证抗抖与回绿 → 点击指示灯验证即时同步;
|
||||
- 全部通过后:迭代 13 标记「验收通过」,R-015 归档 已完成/。
|
||||
|
||||
## 验收目标
|
||||
|
||||
- 6 条验收线通过,迭代 13 标记「验收通过」,R-015 更新实现状态(已实现)并归档。
|
||||
@@ -0,0 +1,79 @@
|
||||
# 技术实现方案:14-策略tab历史持仓展示
|
||||
|
||||
> 迭代编号:14 | 依据:PLAN-015 + R-016(老师拍板:Q2 显示 0 / 其余按建议)
|
||||
|
||||
## 1. 存储层(SqliteStore.getHoldingsHistory)
|
||||
|
||||
```js
|
||||
/** 某策略已清仓持仓(closed_at 非空),closed_at ≥ sinceMs,按 closed_at DESC */
|
||||
getHoldingsHistory(strategyId, { sinceMs } = {}) {
|
||||
// SELECT holding_id, strategy_id, code, shares, created_at, closed_at, "values"
|
||||
// FROM strategy_holdings
|
||||
// WHERE strategy_id=? AND closed_at IS NOT NULL [AND closed_at >= ?]
|
||||
// ORDER BY closed_at DESC
|
||||
// 返回 _mapHolding 同构(holdingId/strategyId/code/shares/createdAt/closedAt/values)
|
||||
}
|
||||
```
|
||||
|
||||
- 只读新增;**零写路径变更**(closeHolding 维持 shares=0 + closed_at,Q2 定稿);
|
||||
- sinceMs 缺省 = 不过滤(内部能力预留;API 层始终传值);
|
||||
- 与 getHoldingHistory(code) 并存:后者是全表按 code 查(R-008 遗留),不满足本需求维度,不复用不改动。
|
||||
|
||||
## 2. 门面层(DataStore)
|
||||
|
||||
```js
|
||||
async getHoldingsHistory(strategyId, opts) { await this._ensure(); return this.sqlite.getHoldingsHistory(strategyId, opts); }
|
||||
```
|
||||
|
||||
## 3. API(src/api/strategies.js 新 method)
|
||||
|
||||
- method:`strategy-holdings/history`;
|
||||
- args:`{ strategyId: string, range: 'week'|'month'|'quarter'|'halfYear'|'year' }`;
|
||||
- range → sinceMs 换算(服务端):week=7d / month=30d / quarter=90d / halfYear=182d / year=365d(自然日近似,回溯窗口展示用途足够,精确日历边界无业务含义);
|
||||
- 缺 strategyId 或 range 非法 → 参数错误(code: 'bad-request');
|
||||
- 返回行附加 name 兜底:按 holdingId 查 trade_orders 取最新一笔的 name(SQL 单查,无则 null)——前端直接展示,免二次请求。
|
||||
|
||||
## 4. 前端(StrategyTab.jsx)
|
||||
|
||||
- 状态:`showHistory`(默认 false)、`historyRange`(默认 'week')、`historyRows`、`historyLoading`;
|
||||
- 头部按钮区:「历史持仓」toggle 按钮 + 开启时显示范围下拉(近一周/近1个月/近3个月/近半年/近1年);
|
||||
- 数据流:开启或切范围 → call('one-divine-lot/strategy-holdings/history', { strategyId, range });缓存按 range 键保留,关闭仅隐藏;
|
||||
- 渲染:rowsForRender = 当前持仓行 +(showHistory ? 历史行 : []);历史行标识 `isHistory: true`;
|
||||
- 行样式:opacity/灰色调(--dsw-alias-label-tertiary)+ 徽标「已清仓 YYYY-MM-DD」;
|
||||
- 单元格:代码/名称/清仓时间(YYYY-MM-DD HH:mm)/持有天数(ceil((closedAt-createdAt)/86400000),同日=1);shares 列显示 DB 原值(=0,Q2);行情类列(lastPrice/pctChange/lastClose/avgPrice/lastTradePrice/upStop/downStop/open/high)一律 —(不注册行情、不取价);自定义字段列:p.values 只读展示,禁点击编辑(isHistory 不进入 FieldCellEditor 分支);
|
||||
- 展开:复用 toggleExpand(code, holdingId) → trades/by-holding(历史行同样有效);
|
||||
- 排序:当前行维持现状(涨幅排序仅作用于当前行集合);历史行固定清仓时间 DESC 追加在后;
|
||||
- 计数:标题(N 只)= 当前持仓数,不含历史行。
|
||||
|
||||
## 5. 回归脚本(scripts/test-r016-history.mjs)
|
||||
|
||||
- ODL_TEST_DATA_DIR 临时目录(技术约束-011,不碰真实数据);
|
||||
- 覆盖:建仓→清仓→history 可查 / 范围过滤边界(7d/30d…)/ 排序 / 策略隔离 / 未清仓行不出现 / API 参数校验 / name 兜底。
|
||||
|
||||
## 6. 验证链
|
||||
|
||||
typecheck → build → test-r016 → 存量回归(test-position-sync 34 / test-r013 21 / test-quote-sync)→ 人工验收清单。
|
||||
|
||||
## 7. 二轮补充实现:今日已清仓默认层(2026-09-07 老师拍板 Q8-Q9)
|
||||
|
||||
> 在首轮(§1-6)基础上追加;首轮实现保持可追溯,新增改动如下。
|
||||
|
||||
### 7.1 服务端(src/api/strategies.js)
|
||||
|
||||
- range 新增 today:**特判本地自然日零点**(localDayStartMs,setHours(0,0,0,0)),sinceMs = 本地今天 00:00;不落入 HISTORY_RANGE_MS 回溯毫秒档(那 5 档语义 = now − 范围,today 语义 = 自然日边界,两类不可混);
|
||||
- 参数校验错误信息扩为 today|week|month|quarter|halfYear|year;其余逻辑(strategy_id 过滤 + name 兜底 + DESC 排序)与 5 档完全复用——**SqliteStore/DataStore 零改动**(sinceMs 本就通用)。
|
||||
|
||||
### 7.2 前端(src/client/views/StrategyTab.jsx)
|
||||
|
||||
- 状态:historyRows 缓存键新增 today(与 5 档并存互不干扰);
|
||||
- **数据流**:loadHistory 定义提前到 load 之前;load() 的 finally 里调 loadHistory('today')——进 tab 即拉 + 每次持仓重载(移出/加仓/编辑后 load())都刷新今日已清仓层,当天清仓转历史后 tab 内不因此少行;
|
||||
- **渲染合成(rowsToRender)**分两层:
|
||||
- L1 = histRowsFor('today') —— **恒显示**(showHistory 关也显示);当前持仓行后追加,清仓时间 DESC;
|
||||
- L2 = showHistory 开时追加 histRowsFor(historyRange),**按 holdingId 去重**(剔除已含于 L1 的行,避免今天清仓 + 开启近一周出现两行);
|
||||
- 交互文案:开关 title 更新为「显示/隐藏今天之前已清仓的历史持仓(当天已清仓的持仓始终显示)」;文件头注释同步;
|
||||
- 标题计数(N 只)仍只算当前持仓,两层已清仓行不计入。
|
||||
|
||||
### 7.3 回归(scripts/test-r016-history.mjs)
|
||||
|
||||
- 新增 [4b] range=today:昨天 23:59:59 清仓不出现 / 今天 00:00:01 清仓出现 / 不含 40 天前行 / today 行 name 兜底同样生效 / range 非法校验不受 today 引入影响;
|
||||
- 验证链复跑:test-r016 24/24 + 存量回归全绿(见迭代复盘)。
|
||||
@@ -0,0 +1,40 @@
|
||||
# 迭代复盘:14-策略tab历史持仓展示
|
||||
|
||||
> 复盘日期:2026-09-07 | 迭代状态:**已实施,待老师人工验收**
|
||||
> 关联需求:R-016 | 关联计划:PLAN-015
|
||||
|
||||
## 结果
|
||||
|
||||
迭代 14 达成:两个策略 tab(做T / 网格超市)title 旁新增「历史持仓」开关 + 清仓时间范围筛选(近一周默认 / 近 1 个月 / 近 3 个月 / 近半年 / 近 1 年);已清仓持仓行灰显 + 「已清仓」徽标(含清仓日期)追加在当前持仓行后(closed_at DESC),展开复用 R-010 查看关联交易——补上「清仓即失联」的操作追溯缺口。当前持仓快照路径(R-014 语义)与所有写路径(closeHolding 置 shares=0,Q2 拍板)零改动。
|
||||
|
||||
## 过程事实
|
||||
|
||||
1. **讨论驱动定稿**(2026-09-07,Q1-Q7 老师逐项拍板):Q2 为本轮唯一改判——AI 建议改 closeHolding 保留清仓时份额,老师否决:「显示 0 就好了,即使显示了清仓时的 shares 也没多少意义,主要目标是追溯这次持仓相关的历史操作」——需求核心从「数据完整性」校正为「操作追溯锚点」,方案随之收敛为零写路径变更;其余六问均按 AI 建议定稿;
|
||||
2. **实现**(严格按 PLAN-015 步骤):SqliteStore.getHoldingsHistory(strategy_id + closed_at IS NOT NULL + sinceMs 过滤 + DESC,只读新增)→ DataStore 门面透传 → strategy-holdings/history 端点(range 五档服务端换算自然日近似 + name 关联委托兜底 + 参数校验 bad-request)→ StrategyTab 开关/范围下拉/历史行渲染;
|
||||
3. **实现中发现并修复一个真实隐患**:R-010 展开状态与交易缓存原以 code 为键——同码「当前行 + 历史行」并存(清仓后重新买入)时两行会同时展开、缓存串行;本轮把展开状态/缓存/行键统一收敛为唯一 rowKey(当前行 = code,历史行 = 'h-' + holdingId),toggleExpand 签名同步变更(当前调用方仅 tbody 一处);
|
||||
4. **验证**:test-r016-history.mjs 19/19(清仓转历史可查 / 5 档范围边界 / 排序 / 策略隔离 / 未清仓不出现 / 参数校验 / name 兜底 / 端点分发零破坏);typecheck 通过;存量回归 test-position-sync 35/35、test-r013 21/21、test-quote-sync 27/27;build 通过(client bundle 164KB wrapped);
|
||||
5. **文档链**:R-016 定稿 + PLAN-015 + 迭代三件套 + 技术约束-019 + 需求池索引(R-016 登记;顺带补登遗漏的 R-015 行);
|
||||
6. **部署态排查(老师首验无数据,2026-09-07)**:实锤定位 = **运行中的 DSH 服务端进程仍是旧代码**——同进程 /odl/api/summary 正常应答、新端点返回 unknown method(curl 实测);客户端 bundle 因插件 symlink 直连源码 lib/ 而被刷新(按钮已出现),服务端模块注册表却停留在进程启动时;处置 = ① loadHistory 失败从静默吞空改为 Toast 可见化(静默兜底=排障盲区,本轮教训)并重新构建(test-r016 19/19 复跑通过);② 服务端生效依赖 DSH web 进程重启(插件 symlink 方式安装,无需重装,重启即取新 lib)。
|
||||
|
||||
## 经验教训(复盘沉淀)
|
||||
|
||||
### 1. 需求的「核心目标」要听老师的定调,不要替老师拔高
|
||||
- AI 在 Q2 提议改 closeHolding 语义以「保留清仓时份额」,出发点是账本完整性;老师一句话把需求核心定调为「追溯历史操作」——份额数字本身没有追溯价值,holding_id 锚点 + 关联委托才有;
|
||||
- 沉淀:讨论中 AI 的方案建议要标明「我认定的价值假设」,让老师有机会否定假设本身,而不只是选项。
|
||||
|
||||
### 2. 复用既有交互时,键的唯一性要先审后用
|
||||
- 展开状态按 code 键控在「一码一行」时代是隐含正确的;历史行引入打破了「一码一行」不变量——同类隐含假设(缓存 Map、React key、loading 标记)都要在数据形态变化时重新过一遍;
|
||||
- 沉淀:向既有列表引入第二类行时,先列出所有「按 X 键控」的状态,逐个判断是否需要升级为行唯一键。
|
||||
|
||||
### 3. 只读需求的实现应当「零写路径」可断言
|
||||
- 本轮全程未触碰任何 INSERT/UPDATE 语句与语义(closeHolding 分毫未动),回归脚本专门加了「strategy-positions 分发不变」断言——历史查询与当前查询的隔离在端点层就已成立(独立 method),不需要靠约定;
|
||||
- 沉淀:「只读功能」的范围声明落成验收断言(分发不变 + 写路径 diff 为空),比口头承诺可靠。
|
||||
|
||||
## 遗留/后续
|
||||
|
||||
1. **归属候选不纳入清仓持仓**(R-016 关联观察,2026-09-07 大连热电实际遇到):清仓后当日委托无法再通过下拉关联到对应 holding——是否立独立需求(候选纳入近期清仓持仓)待老师定;
|
||||
2. **历史行成本价/收益推导**:本期不做(边界定稿);若后续要做,数据源 = 关联委托成交记录,属展示层推导,不动存储;
|
||||
3. **清仓方式标记**(手动 vs 幽灵自动):数据未区分,本期不做;若要做需 closeHolding 增加来源标记(涉及写路径,另立需求);
|
||||
4. **R-015 / 迭代 13 人工验收**仍待老师确认(与本迭代无依赖)。
|
||||
## 验收状态更新(2026-09-08)
|
||||
> 老师 2026-09-08 归档指令:确认验收并归档;R-016 已按归档清单移入 05-需求池/已完成/。
|
||||
@@ -0,0 +1,29 @@
|
||||
# 迭代目标:14-策略tab历史持仓展示
|
||||
|
||||
> 迭代编号:14 | 创建:2026-09-07 | 状态:已实施,待验收
|
||||
> 依据计划:PLAN-015 | 需求:R-016(已定稿,2026-09-07,Q1-Q7 老师拍板)
|
||||
|
||||
## 目标描述
|
||||
|
||||
两个策略 tab(做T / 网格超市)title 旁添加「历史持仓」功能:① 显示/隐藏已清仓历史持仓的开关;② 显示历史时默认展示近一周清仓的持仓,并提供 近 1 个月 / 近 3 个月 / 近半年 / 近 1 年 范围选项。历史持仓行复用 R-010 行展开查看关联交易记录——补上「清仓即失联」的操作追溯缺口(核心目标:追溯该持仓相关的历史操作,老师 2026-09-07 定调)。
|
||||
|
||||
## 目标分解
|
||||
|
||||
1. **存储查询**(SqliteStore.getHoldingsHistory):closed_at 非空 + closed_at ≥ sinceMs + strategy_id 过滤,按 closed_at DESC;只读新增,不动任何写路径(closeHolding 置 shares=0 语义维持,Q2 老师拍板「显示 0 就好」);
|
||||
2. **门面透传**(DataStore.getHoldingsHistory);
|
||||
3. **API 端点**(strategy-holdings/history):strategyId 必填 + range ∈ week|month|quarter|halfYear|year(服务端换算 sinceMs;week 为前端默认但服务端不设隐式默认,参数缺失报错);
|
||||
4. **前端**(StrategyTab):头部按钮区加「历史持仓」开关 + 范围下拉(开启时显示,默认 week);懒加载 + 范围切换重拉 + 关闭隐藏(缓存保留);历史行追加在当前持仓行后(清仓时间降序)、灰显 + 「已清仓」徽标、标题计数不含历史行;历史行字段 = 代码/名称(关联委托 name 兜底)/份额(0)/清仓时间/持有天数,行情列 —,自定义字段列只读;行展开复用 R-010;
|
||||
5. **回归**:scripts/test-r016-history.mjs(ODL_TEST_DATA_DIR 临时目录)+ typecheck + build + 存量回归三脚本。
|
||||
|
||||
## 二轮补充(2026-09-07 验收前追加:今日已清仓默认层,Q8-Q9 老师拍板)
|
||||
|
||||
- **补充诉求**:当天(本地自然日 00:00 起)已清仓的持仓**默认显示**;「历史持仓」开关只控制**今天之前**的历史——历史持仓关 ≠ 当天已清仓也被藏起(首轮「进 tab 默认只看当前」使当天清仓行当场不可见,补上该缺口);
|
||||
- **实现增量**:① 服务端 range='today' 特判本地自然日零点(不落回溯毫秒档);② 前端 rowsToRender 分两层——L1 今日已清仓恒显示(缓存键 'today',随每次持仓重载刷新)+ L2 历史范围层受开关控制,层间按 holdingId 去重;③ 视觉/计数维持首轮(灰显+徽标、计数不含已清仓行);
|
||||
- 影响面:只动只读查询 + 前端渲染合成;**零写路径变更**(closeHolding 语义仍不动);
|
||||
- 回归:test-r016 追加 range=today 自然日边界用例(24/24)。
|
||||
|
||||
## 对老师/主理人的配合需求
|
||||
|
||||
- 实现完成后需老师真实环境人工验收(开关/范围切换/历史行展示/展开复盘/与真实清仓数据对照);
|
||||
- 二轮补充后重点验收:**「历史持仓」关时当天清仓的持仓仍显示**、「历史持仓」开时今天之前的行按范围显示、层间无重复行;
|
||||
- 验收通过后本迭代标记「验收通过」,R-016 进入归档流程。
|
||||
@@ -0,0 +1,38 @@
|
||||
# 验收标准:14-策略tab历史持仓展示
|
||||
|
||||
> 迭代编号:14 | 依据:PLAN-015 验收要点 + R-016
|
||||
|
||||
## 验收标准线
|
||||
|
||||
1. **自动化**:typecheck + build 通过;test-r016-history.mjs 全绿(清仓转历史可查 / 5 档范围过滤与边界 / closed_at DESC 排序 / 策略隔离 / 未清仓行不出现 / 参数校验 / name 兜底);存量回归 test-position-sync、test-r013、test-quote-sync 全绿;
|
||||
2. **入口与默认态**:做T / 网格超市两个 tab title 旁出现「历史持仓」开关;默认关闭 = 与现状完全一致(进 tab 无历史行);开启后默认范围 = 近一周;**全部持仓 tab 无此功能**;
|
||||
3. **范围筛选**:5 档(近一周/近1个月/近3个月/近半年/近1年)切换即重拉;用 2026-09-07 大连热电真实数据验证——holding_id=15(做T,当日 09:32:35 幽灵清仓)在近一周内可见、切近 1 个月仍可见;
|
||||
4. **历史行展示**:灰显 + 「已清仓 YYYY-MM-DD」徽标;清仓时间/持有天数正确;份额显示 0(Q2);行情列(现价/涨幅/涨停跌停/今开/最高/成本/最后成交价)显示 —;名称有则显示、无则 —;标题计数(N 只)不含历史行;当前持仓在前、历史行按清仓时间降序在后;
|
||||
5. **展开复盘**:历史行展开可见该 holding 关联委托(大连热电 09:31:26 卖出 1000 股 @7.42 应出现);自定义字段历史行只读(无编辑入口);
|
||||
6. **零破坏**:strategy-positions / unallocated / summary 等现有端点行为不变;当前持仓行展示与现状一致;无任何写路径变更(closeHolding 语义不变)。
|
||||
|
||||
## 验收方法
|
||||
|
||||
- 自动化项 AI 执行出具结果;
|
||||
- 2~5 老师真实环境人工验收:重载插件 → 两个策略 tab 开关历史持仓 → 切换范围 → 对照大连热电当日清仓数据 → 展开历史行核对关联委托;
|
||||
- 全部通过后:迭代 14 标记「验收通过」,R-016 归档 已完成/。
|
||||
|
||||
## 验收目标
|
||||
|
||||
- 6 条验收线通过,迭代 14 标记「验收通过」,R-016 更新实现状态(已实现)并归档。
|
||||
|
||||
## 二轮补充验收线(2026-09-07 Q8-Q9 追加:今日已清仓默认层)
|
||||
|
||||
7. **今日已清仓默认显示(核心)**:「历史持仓」**关**时,策略 tab 仍显示 当前持仓 + 今天(本地自然日 00:00 起)已清仓的持仓行;当天卖出/幽灵清仓的持仓行不因开关关闭而消失;
|
||||
8. **开关只控更早历史**:「历史持仓」开时,追加显示今天之前各范围(近一周/近1个月/近3个月/近半年/近1年)已清仓行——近一周范围**不含**今天已显示的行(层间按 holdingId 去重,无重复行);
|
||||
9. **边界与展示**:昨天 23:59:59 清仓的行在开关关时不可见(属更早历史);今日行沿用灰显 + 已清仓徽标;标题计数(N 只)仍不含已清仓行;行情列 — / 份额 0 / 只读字段 / 行展开复用 R-010 均维持首轮;
|
||||
10. **自动化**:test-r016 新增 range=today 用例全绿(24/24);typecheck + build + 存量回归(test-position-sync / test-r013 / test-quote-sync)全绿。
|
||||
|
||||
## 验收方法(二轮补充)
|
||||
|
||||
- 自动化项 AI 执行出具结果(已执行:test-r016 24/24、position-sync 35、r013 21、quote-sync 27、typecheck 0、r017 12);
|
||||
- 老师真实环境人工验收:刷新页面 → 在不开启「历史持仓」的情况下核对当天已清仓持仓仍显示 → 开启开关核对今天之前的行按范围出现且与今日行无重复 → 对照大连热电当日清仓数据。
|
||||
|
||||
## 验收目标
|
||||
|
||||
- 首轮 6 条 + 二轮 4 条验收线通过后,迭代 14 标记「验收通过」,R-016 归档 已完成/。
|
||||
@@ -0,0 +1,29 @@
|
||||
# 技术实现方案:15-归属候选纳入清仓持仓
|
||||
|
||||
> 迭代编号:15 | 依据:PLAN-016 + R-017
|
||||
|
||||
## 1. 存储层(getAttributionCandidates 扩展,门面透传不变)
|
||||
|
||||
```sql
|
||||
SELECT holding_id, strategy_id, shares, closed_at FROM strategy_holdings
|
||||
WHERE code=? AND (closed_at IS NULL OR closed_at >= ?) -- ? = Date.now() - 7d
|
||||
ORDER BY (closed_at IS NULL) DESC, shares DESC, closed_at DESC
|
||||
```
|
||||
|
||||
- 返回行新增 closed: boolean / closedAt: number|null(消费方:api strategies.js 同方法直接受益——注意 attribution-candidates 端点在 trades.js,调 DataStore.getTradeAttributionCandidates 透传,零改动);
|
||||
- 窗口常量 7 * 86400000(与 R-016 默认范围一致)。
|
||||
|
||||
## 2. UI(TradeRecordsTab.jsx AttributionSelect 重构)
|
||||
|
||||
- 锚点键:currentKey = order.holdingId != null ? String(order.holdingId) : ''(holdingId 是 R-009 定稿的归属锚点;strategyId 在同策略多轮持仓时会撞值);
|
||||
- handleSelect:按 String(holdingId) 找候选 → setAttribution(orderId, code, cand.strategyId, cand.holdingId);__unassigned__ → 双 null;
|
||||
- 占位选项:已归属但候选缺失(清仓超 7 天)→ 「当前归属({strategyId})」,value=currentKey,防 select 悬空显示第一项误导;
|
||||
- 清仓候选文本后缀「(已清仓 MM-DD)」(fmtClosed 本地 helper);select maxWidth 120→150。
|
||||
|
||||
## 3. 数据修复记录(一次性,非功能)
|
||||
|
||||
2026-09-07 大连热电卖出委托 645001623 经 live set-attribution 通道回填 strategy_id=manual-t / holding_id=15(curl 实测 ok:true,orders/by-holding 回读确认)。当日归属曾被清为未关联的原因:候选下拉无做T项时老师误触「未关联」入口——本迭代修复后此路径消除。
|
||||
|
||||
## 4. 验证链
|
||||
|
||||
test-r017-candidates.mjs 12 断言 + typecheck + build + 存量回归(r016 19 / position-sync 35 / r013 21 / quote-sync 27)。
|
||||
@@ -0,0 +1,36 @@
|
||||
# 迭代复盘:15-归属候选纳入清仓持仓
|
||||
|
||||
> 复盘日期:2026-09-07 | 迭代状态:**已实施,待老师人工验收**
|
||||
> 关联需求:R-017 | 关联计划:PLAN-016
|
||||
|
||||
## 结果
|
||||
|
||||
迭代 15 达成:归属候选 = 当前持仓 + 近 7 天清仓持仓(closed 标记 + 「已清仓 MM-DD」后缀),下拉锚点改 holdingId 键控(同策略多轮持仓不撞值)+ 「当前归属」占位防悬空;清仓后当日委托可补关联。另:当日大连热电卖出委托(645001623)的归属已经 set-attribution 通道修复回 manual-t/holding 15(一次性数据修复,非功能)。
|
||||
|
||||
## 过程事实
|
||||
|
||||
1. **数据取证先行**:老师报告「无法关联」时,DB 真相 = 委托归属已被清为 null/null(非从未设置)+ 候选接口只剩网格超市——「归属丢失」与「候选缺失」两个问题一次取证分清;
|
||||
2. **当日修复走既有通道**:set-attribution 本就无候选限制(Q4 可随时改语义),curl 修复 + 回读验证,不新造工具;
|
||||
3. **实现**:SqliteStore.getAttributionCandidates WHERE/ORDER 扩展 + closed/closedAt 字段(门面/api 零改动透传);AttributionSelect 从 strategyId 键控重构为 holdingId 键控 + 占位 + 后缀;
|
||||
4. **验证**:test-r017 12/12;typecheck 通过;存量回归 r016 19/19、position-sync 35/35、r013 21/21、quote-sync 27/27;build 通过。
|
||||
|
||||
## 经验教训(复盘沉淀)
|
||||
|
||||
### 1. 「关联」的锚点键要在多轮持仓出现时重新审视
|
||||
- R-009 时代下拉 value 用 strategyId(一码一策略一行时无碍);R-016 历史行引入后同码可同时存在「当前行 + 历史行」、同策略可有多轮清仓——strategyId 键撞值成为真实 bug 面,holdingId 才是归属锚点(R-009 表结构早已如此,UI 层滞后);
|
||||
- 沉淀:外键语义(表里存什么)和 UI 键(下拉传什么)要对齐审查,表结构升级时 UI 不自动跟上。
|
||||
|
||||
### 2. 「数据问题」与「能力缺失」要分层处置
|
||||
- 老师当下要的是「这笔委托关联回去」(数据问题)——走既有 API 通道立即修复,不等迭代;「以后不再发生」(能力缺失)——立项 R-017 走流程。两层都做但互不阻塞。
|
||||
|
||||
### 3. 候选缺失时的 UI 应该显示「当前归属」而不是悬空
|
||||
- select value 不在任何 option 里时浏览器表现不可控(显示空白或第一项),第一项恰是「未关联」时极易诱导误操作(本次归属被清的可能路径);
|
||||
- 沉淀:受控 select 的 value 必须保证总有对应 option——数据缺口用占位 option 补,不让浏览器自作主张。
|
||||
|
||||
## 遗留/后续
|
||||
|
||||
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,19 @@
|
||||
# 迭代目标:15-归属候选纳入清仓持仓
|
||||
|
||||
> 迭代编号:15 | 创建:2026-09-07 | 状态:已实施,待验收
|
||||
> 依据计划:PLAN-016 | 需求:R-017(已定稿,2026-09-07)
|
||||
|
||||
## 目标描述
|
||||
|
||||
交易记录 tab 的归属候选(orders/attribution-candidates)纳入近 7 天清仓的持仓(closed 标记),下拉锚点改 holdingId 键控——修复「清仓后当日委托无法补关联」(2026-09-07 大连热电做T实际发生:盘中归属被清后下拉已无手动做T候选)。
|
||||
|
||||
## 目标分解
|
||||
|
||||
1. **存储**(SqliteStore.getAttributionCandidates):WHERE 扩展为 closed_at IS NULL OR closed_at ≥ 7 天前;返回行附 closed/closedAt;排序 当前持仓(shares DESC) 在前 → 清仓行 closed_at DESC;
|
||||
2. **UI**(AttributionSelect):value 改 String(holdingId);点选按 holdingId 找候选取 strategyId+holdingId 写入;清仓选项加「(已清仓 MM-DD)」后缀;已归属但候选缺失时显示「当前归属」占位(防 select 值悬空);title 提示更新;
|
||||
3. **回归**:scripts/test-r017-candidates.mjs(12 断言:候选构成/标记/窗口边界/排序/隔离/端点透传)。
|
||||
|
||||
## 对老师/主理人的配合需求
|
||||
|
||||
- 重启 DSH web 进程(服务端代码更新)+ 刷新页面后人工验收;
|
||||
- 验收通过后迭代 15 标记「验收通过」,R-017 归档。
|
||||
@@ -0,0 +1,20 @@
|
||||
# 验收标准:15-归属候选纳入清仓持仓
|
||||
|
||||
> 迭代编号:15 | 依据:PLAN-016 验收要点 + R-017
|
||||
|
||||
## 验收标准线
|
||||
|
||||
1. **自动化**:typecheck + build 通过;test-r017-candidates.mjs 12/12;存量回归 r016 / position-sync / r013 / quote-sync 全绿;
|
||||
2. **候选构成**(真实环境):交易记录 tab 大连热电今日卖出委托的归属下拉 = 网格超市(当前持仓)+ 手动做T(已清仓 09-07)两项;
|
||||
3. **补关联**:点选「手动做T(已清仓 09-07)」→ 保存成功提示 → 行内归属显示手动做T且下拉保持选中;做T策略 tab 历史持仓行展开可见该笔卖出(holding 15 关联);
|
||||
4. **可逆**:切回「未关联」→ 双 null 持久化;再切回做T → 恢复;
|
||||
5. **占位**:归属存在但候选缺失的场景显示「当前归属(…)」占位(可用超 7 天清仓行验证,或观察现有数据)。
|
||||
|
||||
## 验收方法
|
||||
|
||||
- 自动化项 AI 执行出具结果;2~5 老师重启 DSH web + 刷新页面人工验收;
|
||||
- 全部通过后迭代 15 标记「验收通过」,R-017 归档 已完成/。
|
||||
|
||||
## 验收目标
|
||||
|
||||
- 5 条验收线通过,迭代 15 标记「验收通过」,R-017 更新实现状态并归档。
|
||||
@@ -0,0 +1,141 @@
|
||||
# 技术实现方案:16-交易关联驱动持仓份额动态调整
|
||||
|
||||
> 迭代编号:16 | 依据:PLAN-017 + R-018(已定稿)
|
||||
|
||||
## 0. 核心不变量(数据域分界,R-018 §〇)
|
||||
|
||||
- PositionSync(含幽灵清仓)**只写对账单域**(进程内快照),永不写 strategy_holdings;
|
||||
- strategy_holdings 写操作只来自:**归属服务(交易关联)** + **手动份额操作**(add-shares/remove-shares/move-all-shares/策略删除清空,现状保留);
|
||||
- 账本行转历史(closed_at)唯一途径:卖出段关联把份额减到 0(closeHolding);**幽灵清仓代码删除**。
|
||||
|
||||
## 1. 存储地基(阶段B)
|
||||
|
||||
### 1.1 strategy_holdings 增列(幂等 ALTER,沿用 _ensureXxxColumn 模式)
|
||||
|
||||
```sql
|
||||
ALTER TABLE strategy_holdings ADD COLUMN void_at INTEGER; -- 作废时间(NULL=有效;非 NULL=建仓被撤=从未成立)
|
||||
ALTER TABLE strategy_holdings ADD COLUMN closed_shares REAL; -- 清仓前份额快照(closeHolding 写入;供撤卖出段恢复活动用)
|
||||
```
|
||||
|
||||
- 活动判定收敛:`closed_at IS NULL AND void_at IS NULL`;
|
||||
- 唯一索引重建(活动唯一收窄至两态皆空):
|
||||
```sql
|
||||
DROP INDEX IF EXISTS idx_active_holding;
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_active_holding
|
||||
ON strategy_holdings (strategy_id, code) WHERE closed_at IS NULL AND void_at IS NULL;
|
||||
```
|
||||
- closeHolding 变更:置 shares=0 + closed_at=now + **closed_shares=清仓前份额**(shares 列仍置 0,R-016 Q2 展示语义不变);
|
||||
- 新增 voidHolding(strategyId, code):置 shares=0 + void_at=now(不改 closed_at);作废行不进 R-016 历史层(getHoldingsHistory 过滤 void_at IS NULL);
|
||||
- _mapHolding 增 voidAt / closedShares 字段(读方法 select 补列)。
|
||||
|
||||
### 1.2 归属分段表 trade_order_attributions
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS trade_order_attributions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
order_id TEXT NOT NULL, -- → trade_orders.order_id
|
||||
strategy_id TEXT NOT NULL, -- 段归属策略(冗余,过滤快)
|
||||
holding_id INTEGER NOT NULL, -- 段锚点持仓(R-010 展开用)
|
||||
code TEXT NOT NULL, -- 冗余(by-holding 反查/防呆)
|
||||
direction TEXT NOT NULL, -- buy/sell 冗余(撤段判向)
|
||||
volume REAL NOT NULL, -- 该段已成交量(>0;总额 ≤ order.traded_volume)
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_attr_order_strategy ON trade_order_attributions (order_id, strategy_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_attr_holding ON trade_order_attributions (holding_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_attr_strategy ON trade_order_attributions (strategy_id);
|
||||
```
|
||||
|
||||
- **真相源 = 段表**;trade_orders.strategy_id/holding_id 两列**退役为冗余**(不再作为查询/过滤源;保留列不删,防外部脚本 break;写入不再维护);既有单组归属迁移为第一段(volume=该单 traded_volume,幂等:段表空且 order 有归属才迁);
|
||||
- 撤销语义支持 R-010 by-holding、R-009 history 策略过滤全部改经段表 join。
|
||||
|
||||
## 2. 归属服务(阶段C,新增 src/trades/AttributionService.js)
|
||||
|
||||
> 命名遵守技术约束-016(src/trades/ 交易域)。接口同步(node:sqlite DatabaseSync);归属服务持 `storage.sqlite` + DataStore 门面方法。
|
||||
|
||||
### 2.1 段级 apply(一段关联)
|
||||
|
||||
输入:{ orderId, code, direction, volume(已成交量) } + 目标 strategyId。判定动作表(R-018 §3.3):
|
||||
|
||||
| direction | strategy 活动仓(closed/void 均空) | 动作 |
|
||||
|---|---|---|
|
||||
| buy | 有 | addShares(strategyId, code, volume) → holding 取活动仓 |
|
||||
| buy | 无 | openHolding(strategyId, code, volume) → 新仓(不复活历史/作废行) |
|
||||
| sell | 有 且 shares ≥ volume | reduceShares;减后 0 → closeHolding |
|
||||
| sell | 有 且 shares < volume | **拒绝**(报错 code='segment-exceeds',由 UI 自动截断后再提交;服务端不做隐式截断——避免「半生效」状态与部分关联提示脱节) |
|
||||
| sell | 无 | 拒绝(code='no-active-holding',关联失败提示,R1) |
|
||||
|
||||
成功 → 写段表(order_id,strategy_id,holding_id,code,direction,volume,now)。买入建仓时 holding_id=openHolding 返回;买入加仓/卖出取活动仓 holding_id。
|
||||
|
||||
### 2.2 段级 revoke(撤一段)
|
||||
|
||||
输入:{ order_id, strategy_id }(唯一键)。逆操作(R3,粒度=段):
|
||||
|
||||
- **撤 buy 段**:查段 → holding。
|
||||
- holding 活动(closed/void 空):
|
||||
- shares ≥ 段量 → reduceShares(段量);**减后 shares=0** → voidHolding(作废,非 closeHolding;R3-1/R6 语义:建仓被撤=从未成立,无真实卖出→作废,不产生假清仓历史);
|
||||
- shares < 段量(说明该段之后已有卖出段作用于同一 holding)→ **拒绝**(code='segment-order-conflict',提示先撤更晚的段);
|
||||
- holding 已 closed/void → 该 buy 段本就不应撤销成功(段应随原操作撤)→ 拒绝;
|
||||
- **撤 sell 段**:查段 → holding。
|
||||
- holding 仍活动(该 sell 只是减仓)→ addShares(段量) 加回;
|
||||
- holding 已 closed 且 **closed_shares == 段量**(该段就是清仓段,且其后无新仓)→ **恢复活动**:closed_at=NULL、shares=closed_shares、closed_shares=NULL;
|
||||
- holding 已 closed 且 closed_shares > 段量(部分减仓后另段清仓)→ 拒绝(先撤更晚段);已 closed 且同 code 新活动仓已存在(close 后又建仓)→ 拒绝(无法恢复,避免撞唯一索引;R-018 §3.4 边界);
|
||||
- 成功后删段表行。
|
||||
|
||||
> 实现约束(写入 R-018 边界):**段撤销按「holding 内逆序」支持**;跨 holding 的段(拆单分给多策略)互相独立可任意撤(老师 R7 场景)。乱序撤同 holding 的段返回 segment-order-conflict,提示先撤更晚段——宁可拒绝不写错账(账本正确性 > 操作便利)。
|
||||
|
||||
### 2.3 全量替换 setSegments(改归属 = 撤旧段 + 加新段,原子)
|
||||
|
||||
输入:{ orderId, segments: [{ strategyId, volume }] }(volume>0;总额 ≤ order.traded_volume,允许 < = 部分关联)。
|
||||
流程(单事务):
|
||||
1. 读 order(trade_orders:code/direction/traded_volume);读段表现状;
|
||||
2. diff:将被删除段逐个 revoke(同 2.2 冲突规则)→ 冲突则整体回滚并报错;
|
||||
3. 新增/变更段逐个 apply(同 2.1)→ 任一失败整体回滚;
|
||||
4. COMMIT 后返回 { segments: 当前全部段 }(order 附加)。
|
||||
|
||||
> 事务:SqliteStore 暴露 `runInTransaction(fn)`(BEGIN/COMMIT/ROLLBACK 包裹;同步 API 直接 exec)。
|
||||
|
||||
### 2.4 查询
|
||||
|
||||
- getOrderAttributions(orderId):段列表(含 strategyName 由 settings 附名);
|
||||
- orders 列表附加归属:批量查段表按 order 聚合;
|
||||
- getOrdersByHolding(holdingId):改经段表 join trade_orders(R-010 展示);
|
||||
- 历史策略过滤(getOrderHistory/getFillHistory strategyId):改经段表(fill→order→段表 strategy_id 集合;`__unassigned__` = 无任何段);
|
||||
- getHoldingsHistory:过滤 void_at IS NULL(作废不进历史层)+ closed 逻辑不变。
|
||||
|
||||
## 3. 候选与 API(阶段D)
|
||||
|
||||
- **orders/attribution-candidates 退役 R-017 7 天语义** → 改 `orders/attribution-targets`:{ code } → 全部策略(settings strategies)+ 每策略该 code 当前活动份额 activityShares(0=无仓)+ 该 order 已有关联段数提示(供 UI「指向后判定」的前置展示;**不预筛**,指向后由服务端判定)。策略级(Q1),holdingId 不再作下拉键;
|
||||
- **orders/attribution-set**:{ orderId, segments } → 归属服务 setSegments(替代 set-attribution;set-attribution 端点删除或保留为单段包装——删,避免双写源混乱;既有数据迁移已覆盖);
|
||||
- **orders/attribution-segments**:{ orderId } → 读当前段(UI 展开/编辑用);
|
||||
- 今日 orders 端点:order 行附 `segments: [{strategyId,strategyName,holdingId,volume}]`(取代原单组 strategyId/holdingId 附加;兼容字段保留首段值)。
|
||||
|
||||
## 4. UI 份额分配器(阶段E,TradeRecordsTab)
|
||||
|
||||
- AttributionSelect 退役 → **AttributionAllocator**:
|
||||
- 头部:委托已成交量 / 已关联 Σ / 待分配余额(醒目色);
|
||||
- 一段 = 策略下拉(attribution-targets)+ 份额输入(默认=余额全量,可改小;sell 时 max=min(余额, activityShares) 自动钳制)+ 「添加段」;
|
||||
- 段列表(每段:策略名 + 量 + 「撤」按钮 → 调 attribution-set 去掉该段,即全量重交剩余段;或独立 revoke 端点——为原子性走 attribution-set 全量重交);
|
||||
- 已关联/部分关联/未关联三态展示 + 保存即生效(每步操作即时 setSegments);
|
||||
- 余额为 0 全关联提示;余额 >0 显「未分配剩余」提示(允许部分关联);
|
||||
- 撤销冲突(segment-order-conflict)→ toast 展示后端提示。
|
||||
- TradeRecordsTab order 行「归属」列 = 段 chips(点开进分配器)。
|
||||
|
||||
## 5. 分界 + 软提示(阶段A)
|
||||
|
||||
- PositionSync:删除 `_autoCloseGhosts`/ghostRounds/_ghostMiss/_fetchAccountId 调用链、stats.closedGhosts;类注释更新(只同步对账单快照);保留空快照双重确认与读穿透(对账单域行为不变)。test-position-sync.mjs 相应改断言(幽灵清仓→不再写账本:mock 断言 storage.closeHolding 未被调用);
|
||||
- **漏关联软提示(T3)**:新端点 `positions/orphan-hints`:账本活动行(closed/void 均空)∩ QMT 快照无此 code → 返回 [{strategyId,code,name,shares,holdingId}](**只读**,不动账本);交易记录 tab 顶部横幅提示(N 个持仓对账单已无此票,请去关联卖出单或移出)。阈值:全部孤儿都列(不设 7 天——账本无快照即提示,软提示不自动写)。
|
||||
|
||||
## 6. 涉及文件
|
||||
|
||||
- src/position/PositionSync.js(删幽灵清仓)、scripts/test-position-sync.mjs(改断言)
|
||||
- src/storage/SqliteStore.js(增列/索引重建/voidHolding/close 快照/段表 CRUD/runInTransaction/查询改段表/迁移)、src/storage/DataStore.js(门面透传)
|
||||
- src/trades/AttributionService.js(新增:apply/revoke/setSegments)
|
||||
- src/api/trades.js(attribution-targets/set/segments;orders 附加段)、src/api/strategies.js(strategy-holdings/history 过滤 void)
|
||||
- src/client/views/TradeRecordsTab.jsx(AttributionAllocator 替换 AttributionSelect)
|
||||
- scripts/test-r018-attribution.mjs(新增回归)
|
||||
- 设计约束:技术约束-017/产品约束-011/数据存储设计.md §11 变更记录 + §12 追加
|
||||
|
||||
## 7. 验证链
|
||||
|
||||
test-r018-attribution.mjs(分界/动作表/撤段逆序/迁移/软提示/API)+ test-position-sync 更新 + 存量回归(r017 改/ r016 / r013 / quote-sync)+ typecheck + build。
|
||||
@@ -0,0 +1,41 @@
|
||||
# 迭代复盘:16-交易关联驱动持仓份额动态调整
|
||||
|
||||
> 复盘日期:2026-09-08 | 迭代状态:**验收通过(2026-09-08)** —— 口径:自动化为主体 + 只读走查 + 真实使用顺带确认(老师拍板,见验收标准.md)
|
||||
> 关联需求:R-018(已定稿)| 关联计划:PLAN-017
|
||||
|
||||
## 结果
|
||||
|
||||
迭代 16 达成(R-018 全范围):**数据域分界落地**——幽灵清仓退役(PositionSync 不再写 strategy_holdings 账本);**归属升级为账本写操作**——策略级候选 + 统一份额分配器(委托拆 0..N 段 × 策略 × 量,余额默认全量可改小、允许部分关联)+ 买卖联动账本(买入加仓/建新仓不复活旧行、卖出减仓/归零清仓、仓不足拒绝、无仓拒绝)+ 撤段逆操作(撤建仓买入=作废第三态不产生假清仓历史、撤加仓=减回归零作废、撤卖出=恢复活动仓+份额加回,粒度=段、改归属原子)+ 归属分段存储(trade_order_attributions,真相源)+ R-017 7 天候选退役(holdingId 键控/占位安全逻辑由策略级 targets 承担)+ 漏关联软提示(只读)。存量归属(trade_orders 单列)自动迁移为段表第一段。
|
||||
|
||||
验证:新回归 test-r018-attribution 29/29;test-position-sync 29/29(幽灵退役断言重写);存量回归 r016 24 / r017 12(语义修订)/ r013 21 / r009 14 / r009-api 13(端点更新)/ r009-sync 8 / r009-normalize 11 / r011-api 12 / r011-tabs 23 / quote-sync 27 全绿;typecheck + build 通过。
|
||||
|
||||
## 过程事实
|
||||
|
||||
1. **文档链先行**:讨论收敛 → R-018 定稿 → PLAN-017 → 迭代 16 三件套 → 设计约束变更(技术约束-017 / 产品约束-011 / 数据存储设计 §11-13);
|
||||
2. **阶段A 分界**:PositionSync 删 `_autoCloseGhosts`/ghostRounds/账户守卫链/stats.closedGhosts,只保留对账单快照同步(含空快照双重确认与读穿透);软提示新端点 positions/orphan-hints(账本活动行 ∩ 快照缺失 code,只读);
|
||||
3. **阶段B 存储**:strategy_holdings 增 void_at/closed_shares(幂等 ALTER + 活动唯一索引重建收窄至两态皆空——索引创建移出 SCHEMA_SQL 到补列之后,防旧库无列建索引崩溃,quote-sync 回归暴露);closeHolding 记 closed_shares(shares 仍置 0,R-016 Q2 展示语义不变);voidHolding/reopenHolding/getHoldingById;新建 trade_order_attributions 段表;存量归属迁移段表第一段(幂等);
|
||||
4. **阶段C 归属服务**(src/trades/AttributionService.js 新增):apply(动作表判定)/ revoke(撤段逆操作,同 holding 逆序、跨 holding 独立)/ setSegments(全量替换、单事务原子);setOrderAttribution 兼容封装 = 替换语义(清旧段设新段);
|
||||
5. **阶段D API**:attribution-targets(策略级 + activityShares)/ attribution-set / attribution-segments;orders 与 trades/history 返回附加 segments;R-017 attribution-candidates/set-attribution 端点退役;strategy-holdings/history name 兜底改段表 join;
|
||||
6. **阶段E UI**:AttributionSelect → AttributionCell + AttributionEditor(余额头 + 段 chips 可撤 + 策略下拉/量输入/截断提示 + 全部撤除/完成);交易记录 tab 顶部漏关联软提示横幅;
|
||||
7. **阶段F 回归**:新脚本 test-r018-attribution;更新 position-sync(幽灵退役断言)、r017(7 天候选退役语义)、r009-api(新端点)、r016-history(段表造归属)。
|
||||
|
||||
## 经验教训(复盘沉淀)
|
||||
|
||||
### 1. 语义迁移要逐调用点核对 + 用旧回归脚本当哨兵
|
||||
R-018 把「归属真相源」从 trade_orders 单列迁到段表,牵动 r009/r016/r017/verify-real 一堆脚本与 strategies handler。靠跑存量回归暴露了两类坑:① quote-sync 的旧结构库建库路径在 SCHEMA_SQL 含新列索引时崩溃(索引引用尚不存在的列)——列迁移与索引重建必须同序;② 测试里「UPDATE trade_orders SET holding_id」直改冗余列的方式在真相源切换后不再生效(r016 name 兜底失败)——测试自身要改经段表造数据。沉淀:**存储层语义变更时,先跑全部存量回归锁定断裂点,再逐点核对(skill §5 语义迁移核对)**。
|
||||
|
||||
### 2. 「作废」与「清仓」是两个生命周期终态,不能用同一标记
|
||||
撤建仓买入段若走 closeHolding 会造出假的「已清仓」历史行(R-016 历史层会显示、统计会含)——老师 R3-1 拍板新增 void 第三态。沉淀:**「从未成立」与「曾经成立后卖出」在账本语义上必须区分**,UI 历史层只认 close。
|
||||
|
||||
### 3. 服务端不做隐式截断,让 UI 截断并提示余额
|
||||
卖出段 > 活动仓时服务端直接拒绝(segment-exceeds),由 UI 自动截为可容纳量并醒目提示「余额保留」——避免服务端悄悄改用户意图造成「半生效」状态与提示脱节。沉淀:**涉及钱/仓位的写接口要显式,宁可报错回滚也不要隐式修正**。
|
||||
|
||||
### 4. 数据域分界用一句话可执行规则落地
|
||||
「幽灵清仓是一个同步机制,爪子不许伸到账本」落到代码 = PositionSync 构造里不再持有 storage 写能力路径 + 回归断言 closeHolding 零调用。沉淀:**架构性拍板要落到「哪个模块能写哪张表」的显式约束,并用测试钉死**。
|
||||
|
||||
## 遗留/后续
|
||||
|
||||
1. **r013-columns 测试漂移(既有,非本迭代引入)**:断言「默认 8 列」但 COLUMN_META 自 R-015 加 4 行情列后实际 14 列(4 默认隐藏)——R-015 后未同步,建议另立小修(列显隐功能未破坏,纯测试预期过期);
|
||||
2. **R-016/R-017 文档同步**:R-017 迭代 15 复盘所述「候选窗口」语义已被 R-018 取代(7 天候选退役),R-017 需求文档待标注退役(R-018 需求文档已含 T2 决策,归档时统一);
|
||||
3. **幽灵清仓退役的旧数据影响**:存量库中过去被幽灵清仓转历史的行保持 closed 不变(不回溯);新语义下账本只随交易关联/手动操作走;
|
||||
4. **UI 待老师人工验收项**:多段拆单交互、撤段恢复活动、作废行不显历史层、软提示横幅、策略持仓 tab 不因 QMT 卖光自动消失(须关联卖出单才 close)。
|
||||
@@ -0,0 +1,29 @@
|
||||
# 迭代目标:16-交易关联驱动持仓份额动态调整
|
||||
|
||||
> 迭代编号:16 | 创建:2026-09-08 | 状态:实施中
|
||||
> 依据计划:PLAN-017 | 需求:R-018(已定稿,2026-09-08 老师逐项拍板)
|
||||
|
||||
## 目标描述
|
||||
|
||||
把「交易归属」从纯标签升级为**账本写操作**,落实 R-018 的**数据域分界**:幽灵清仓等同步机制只作用于「全部持仓」对账单域,不再写 strategy_holdings 账本;策略持仓份额只由**交易关联分配器 + 手动份额操作**驱动。
|
||||
|
||||
范围:策略级候选 + 统一份额分配器(委托已成交量拆 0..N 段 × 策略)+ 买卖联动账本(买入加仓/建新仓、卖出减仓/归零清仓、仓不足截断续分、无仓失败提示)+ 撤段逆操作(撤建仓买入=作废第三态、撤加仓=减回归零作废、撤卖出=恢复活动+份额加回,粒度=段)+ 归属分段存储(trade_order_attributions)+ R-017 7 天候选退役 + 漏关联软提示 + PositionSync 删幽灵清仓逻辑。
|
||||
|
||||
## 目标分解(阶段)
|
||||
|
||||
1. **阶段A 数据域分界**:PositionSync 删除幽灵清仓账本写(_autoCloseGhosts 不再 closeHolding),只保留对账单快照同步;漏关联软提示检测基础;
|
||||
2. **阶段B 存储地基**:strategy_holdings 增 `void_at` + `closed_shares`(幂等补列,close 时记录清仓前份额供撤段恢复);新建 `trade_order_attributions` 段表;trade_orders 原两列退役为冗余(读取改经段表);存量单组归属迁移为第一段;
|
||||
3. **阶段C 归属服务**:apply/revoke 判定动作表(买/卖 × 活动仓状态)+ 撤段逆操作 + 原子性(同事务撤旧段+加新段);
|
||||
4. **阶段D 候选与 API**:orders/attribution-candidates 回归「策略级」(R-017 7 天候选退役;holdingId 键控/占位安全逻辑保留);orders/attribution-set(全量替换式)+ attribution-segments(读);
|
||||
5. **阶段E UI 分配器**:TradeRecordsTab 归属入口改策略下拉 + 段量 + 段列表(每段可撤);
|
||||
6. **阶段F 回归**:新回归脚本(分界/分配器动作/撤段/迁移/软提示)+ 存量回归 + typecheck + build。
|
||||
|
||||
## 讨论过程(摘要)
|
||||
|
||||
2026-09-08 讨论收敛全部决策:Q1 策略级候选 / Q2 拆段 / Q3 保留建仓逻辑(份额基于 QMT 持仓创建)/ Q4 已成交量 / Q5 新买入=新仓不复活;R1 无仓卖出关联失败 / R2 分段存储 / R3 撤段逆操作(作废第三态+恢复活动)/ R4 统一分配器(不分子买卖、余额默认全量可改小、允许部分关联)/ R5 买入可拆段 / R6 撤加仓归零作废 / R7 段级取消;数据域分界(幽灵清仓不写账本);T1 关闭(无 30s 竞态);T2 R-017 退役;T3 漏关联软提示。
|
||||
|
||||
## 对老师/主理人的配合需求
|
||||
|
||||
- 重启 DSH web 进程(服务端代码更新)+ 刷新页面人工验收;
|
||||
- 验收通过后迭代 16 标记「验收通过」,R-018 归档 + 需求池索引更新;
|
||||
- 迭代 12(R-014 幽灵清仓条款)、迭代 15(R-017 退役)为既有功能变更,人工验收时一并确认新行为符合预期。
|
||||
@@ -0,0 +1,24 @@
|
||||
# 验收标准:16-交易关联驱动持仓份额动态调整
|
||||
|
||||
> 迭代编号:16 | 依据:PLAN-017 验收要点 + R-018
|
||||
> 验收状态:**已通过(2026-09-08)** —— 口径修订:自动化为主体 + 只读走查;实盘操作类验证项降为「真实使用顺带确认」(老师拍板:实盘不可为验收而操作,边用边发现问题再修)
|
||||
|
||||
## 验收标准线
|
||||
|
||||
1. **数据域分界(阶段A)**:PositionSync 不再写 strategy_holdings——幽灵清仓逻辑删除,同步只维护对账单内存快照;回归脚本断言 storage.closeHolding 不被 PositionSync 调用;
|
||||
2. **存储地基(阶段B)**:strategy_holdings 增 void_at/closed_shares 列(幂等);活动唯一索引收窄至两态皆空;closeHolding 记录 closed_shares(shares 仍置 0);voidHolding 置 void_at;段表 trade_order_attributions 建表 + 存量单组归属迁移为第一段(幂等);
|
||||
3. **份额分配器动作表(阶段C)**:买入段→加仓/建新仓(不复活旧行);卖出段→减仓/归零清仓、仓不足拒绝(segment-exceeds)、无仓拒绝(no-active-holding);撤买入段→减回/归零作废(void,不产生假清仓历史);撤卖出段→加回/恢复活动仓(closed_shares 恢复);跨 holding 段可任意撤、同 holding 乱序撤拒绝(segment-order-conflict)——全量替换 setSegments 原子(失败整体回滚);
|
||||
4. **API(阶段D)**:attribution-targets(策略级 + activityShares);attribution-set(segments 全量替换);attribution-segments(读);今日 orders / trades/history 附加 segments;R-017 7 天候选退役(attribution-candidates/set-attribution 端点退役,候选改由 targets 承担);
|
||||
5. **软提示(T3)**:positions/orphan-hints 返回账本活动行 ∩ QMT 快照缺失的 code(只读不写);
|
||||
6. **自动化**:test-r018-attribution.mjs 29/29 全绿 + test-position-sync 更新后 29/29 + 存量回归(r016 24 / r017 12 / r013 21 / r009 14 / r009-api 13 / r009-sync 8 / r009-normalize 11 / r011-api 12 / r011-tabs 23 / quote-sync 27)全绿 + typecheck + build 通过。
|
||||
|
||||
## 验收方法(2026-09-08 修订,老师拍板)
|
||||
|
||||
- **主体 = 自动化回归**:上述标准线 1-6 由回归脚本 + typecheck + build 机器验证(AI 已执行并出具结果);实盘为真实资金,不可为验收做买卖操作;
|
||||
- **人工只读走查**(老师 2026-09-08 已重启并做一次关联操作确认 OK):分配器界面(余额头/策略下拉带仓数/段 chips)显示正常;仅只读观察,不做实盘写操作;
|
||||
- **顺带确认项(不主动验收,真实使用自然发生时观察)**:卖出全清后策略持仓行不再 30s 自动消失(幽灵退役);顶部漏关联软提示出现;撤段恢复/作废行为——边用边发现问题再修。
|
||||
|
||||
## 验收目标
|
||||
|
||||
- 标准线 1-6(自动化)通过 → 迭代 16 标记「验收通过」;R-018 归档 已完成/ + 需求池索引实现状态更新;
|
||||
- 实盘行为类验证项以「真实使用顺带确认」持续跟进(老师口径:先标记成功,后面边用边发现问题再修)。
|
||||
@@ -0,0 +1,76 @@
|
||||
# 迭代 17 UI 交互设计:策略会话(讨论依据控件)
|
||||
|
||||
> 依据:R-021(合并原 R-019+R-021,已定稿)+ 产品逻辑设计(同迭代)| 日期:2026-09-08 | 状态:**设计稿(D 问题待拍板)**
|
||||
> 视觉基线:UI约束-004(--dsw-* token)/ UI约束-001(头部 chip 形态)/ 现有 QmtConnectionChip 交互模式;复用 Toast、外点关闭、RPC(useRpc) 机制。
|
||||
|
||||
## 1. 交互总览(一句话)
|
||||
|
||||
对话视图输入区上方有一枚**「讨论依据」chip**(仿 DSH workspace chip:未选=「讨论依据:请选择 ▾」;已选=「依据:做T ▾」+刷新);选择策略 → 服务端生成数据摘要 → 注入并在对话流顶部出现一条**数据依据条目**,之后即可基于该策略数据讨论。
|
||||
|
||||
## 2. 控件布局(示意)
|
||||
|
||||
```
|
||||
┌─ 对话视图 ──────────────────────────────┐
|
||||
│ …对话消息流… │
|
||||
│ [数据依据条 ▾ 做T · 快照 14:32 · 3仓/42笔] ← 选后注入显示(可展开全文)
|
||||
│ ─────────────────────────────────────── │
|
||||
│ [依据:做T ▾] [↻] │ ← accessory 行(输入框上方左侧)
|
||||
│ ┌ 输入框(composer)────────────────┐ │
|
||||
│ └────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 3. chip 状态机
|
||||
|
||||
| 状态 | 外观 | 交互 |
|
||||
|---|---|---|
|
||||
| 未加载 | 不显示(静默,与 QMT chip 一致) | — |
|
||||
| 未选择 | `讨论依据:请选择 ▾`(tertiary 色) | 点开 → 策略下拉(空态见 §5) |
|
||||
| 生成中 | `依据:做T …`(opacity .6,右侧小 spinner/省略) | 不可再点(防抖) |
|
||||
| 已选择 | `依据:做T ▾` + 右侧「↻ 刷新」小钮 | 点 chip 换策略;点 ↻ 刷新注入 |
|
||||
| 策略已删除 | `依据:做T(已删除)▾`(warning 色) | 下拉选其它/清除 |
|
||||
| 错误 | toast 错误 + chip 保留原态 | 可重试 |
|
||||
|
||||
## 4. 交互细节
|
||||
|
||||
1. **选择策略(F1 主流程)**
|
||||
- 点击 chip → 下拉菜单(与 QmtConnectionChip 同一套 Menu 视觉):
|
||||
- 标题区「以哪个策略讨论?」;
|
||||
- 策略项:每项 = 策略名 + 副行「当前 N 只持仓 · 最近交易 …」;
|
||||
- 底部「清除依据」(仅已选时出现);当前依据项前打 ✓;
|
||||
- 点击某策略 → chip 进入生成中 → 服务端取摘要 → 注入会话 + 对话流顶部出现依据条目 → toast.success(`已载入「做T」数据依据:3 持仓 / 42 笔交易(快照 14:32:05)`)。
|
||||
2. **注入条目(D-1 建议形态)**
|
||||
- 视觉:淡色卡片/引用样式(bg-layer 弱化 + 左侧竖条 accent),标题行「⛭ 神之一手数据依据」+ `做T · 快照 2026-09-08 14:32:05`,行 2 摘要「当前持仓 3 只 / 历史交易 42 笔(含已清仓)」;
|
||||
- 默认折叠,点击展开全文(markdown 摘要内容);
|
||||
- 多依据/多刷新并存时按时间堆叠,最近一次在消息流最新处;不伪装用户/助手消息(若通道仅支持文本消息,则呈现为最顶部一段普通消息文本——实现以调研为准)。
|
||||
3. **换策略**:chip 下拉选另一策略 → 同"生成中→注入"流程;chip 文案与已注入条目更新。
|
||||
4. **刷新依据**:点 chip 右侧 ↻ → 生成新快照并注入新条目(含新快照时间);toast「已更新依据(快照 14:35:10)」。不会删除旧条目。
|
||||
5. **清除依据**:下拉「清除依据」→ chip 复位「请选择」;不清除历史注入条目。
|
||||
6. **键盘/可及**:Esc 或外点关闭下拉;chip 提供 title 提示「以某个策略的数据为依据开始讨论(只读)」。
|
||||
7. **只读表达**:选择器下拉仅含策略与其状态信息,无任何写/操作入口;文案提示"会话只读,不执行交易操作"(依赖尾注与依据条目样式即可,不打断输入)。
|
||||
|
||||
## 5. 空态与边界 UI
|
||||
|
||||
| 场景 | UI |
|
||||
|---|---|
|
||||
| 无策略可配置 | 下拉空态:「暂无策略,请到 设置 → 神之一手 → 策略分组 添加」+「去设置」快捷键位 |
|
||||
| 策略持仓为空 | 摘要条目持仓区显示「当前无持仓」;交易摘要照常 |
|
||||
| 行情缺失 | 摘要中现价列显示 —,条目副行提示「部分行情缺失(--)」,不阻塞 |
|
||||
| 生成失败/服务不可用 | toast.error(原因);chip 回原态可重试 |
|
||||
| 已在数据 tab(.odl-root)| 不显示该控件(现状 CSS 隐藏 composer 的区域一致,仅聊天视图展示) |
|
||||
|
||||
## 6. 组件落点(初拟,技术方案细化)
|
||||
|
||||
- 客户端新组件 `StrategyBasisChip.jsx`(chip + 下拉 + 刷新),复刻 QmtConnectionChip 的结构(rootRef/外点关闭/useRpc/useToast);
|
||||
- 注入条目组件 `DataBasisCard.jsx`(折叠/展开 markdown 展示);
|
||||
- 服务端新只读端点(`one-divine-lot/strategies/basis-summary`)生成摘要(产品逻辑 §4 规格);
|
||||
- 挂载位置:D-4(对话 accessory vs 会话头部)拍板后确定 slot 与可见性策略。
|
||||
|
||||
## 7. D 问题(AI 建议项,与产品逻辑设计共享,待老师拍板)
|
||||
|
||||
| # | 问题 | AI 建议 |
|
||||
|---|---|---|
|
||||
| D-1 | 注入条目呈现形态 | 折叠「数据依据」卡片,点开全文(见 §4.2) |
|
||||
| D-2 | 交易摘要聚合粒度 | 按持仓聚合 + 已清仓标注;如需近期逐笔再加近 N 笔 |
|
||||
| D-3 | 依据跨刷新持久化 | 会话级不持久化(重开重选) |
|
||||
| D-4 | chip 位置 | 对话视图输入框上方 accessory 行左侧(仿 workspace chip 位置);备选=会话头部与 QMT chip 并排 |
|
||||
@@ -0,0 +1,131 @@
|
||||
# 迭代 17 产品逻辑设计:策略会话
|
||||
|
||||
> 依据:R-021(合并原 R-019+R-021,已定稿)| 日期:2026-09-08 | 状态:**设计稿(D 问题待老师拍板后定稿)**
|
||||
|
||||
## 0. 一句话逻辑
|
||||
|
||||
> 会话 = 用户选定「讨论策略」→ 系统把该策略**当前数据**(当前持仓 + 全历史关联交易摘要)作为会话依据注入 → 之后人机围绕该数据复盘/分析;依据可随时更换/刷新;只读。
|
||||
|
||||
## 1. 概念与数据域
|
||||
|
||||
| 概念 | 定义 | 数据来源 |
|
||||
|---|---|---|
|
||||
| 策略 | settings 中的策略定义(id/name,做T、网格超市…) | settings.strategies(现有) |
|
||||
| 策略当前持仓(活动行) | 该策略账本中未清仓的 holding(shares>0 活动) | SqliteStore strategy_holdings(R-018 数据域:账本为唯一权威) |
|
||||
| 行情现价/涨跌 | holding code 的实时价格 | QuoteHub 内存快照(读穿透;不可得=—) |
|
||||
| 关联交易记录 | 归属该策略/持仓的委托(含已清仓历史,trade_orders 归属列 + 历史库) | SqliteStore trade_orders/trade_fills(R-009/R-018) |
|
||||
| 数据依据(basis) | 一次注入到会话的"策略当前数据摘要快照" | 服务端按需生成(本设计) |
|
||||
|
||||
**关键语义(沿用 R-018 数据域分界)**:持仓展示 = 账本(活动行);交易 = 归属账本(历史全量);行情 = 内存快照。摘要生成**只读**这些域,不写账本。
|
||||
|
||||
## 2. 会话级状态
|
||||
|
||||
策略会话不需要跨会话持久存储(设计建议,见 D-3);会话内 UI 状态:
|
||||
|
||||
- `basis: none | { strategyId, strategyName, injectedAt }`
|
||||
- `basisState: idle | generating | done | error`
|
||||
- 注入后的摘要内容随会话消息流存在(可折叠"数据依据"条目),不单独存储。
|
||||
|
||||
## 3. 核心流程
|
||||
|
||||
### 3.1 选择策略 → 注入依据(主流程)
|
||||
1. 对话视图(聊天)中,用户在「讨论依据」控件里选一个策略;
|
||||
2. 客户端调服务端 `strategies/:id/basis-summary`(只读)生成摘要文本(规格见 §4);
|
||||
3. 摘要通过**宿主注入通道**进入当前会话上下文(技术通道调研待定,见 §6);同屏在对话流顶部呈现「数据依据」条目;
|
||||
4. toast 反馈:`已载入「做T」数据依据(3 持仓 / 42 笔交易,快照 14:32:05)`;
|
||||
5. 用户开始提问复盘/分析;模型以摘要(及会话中后续补充)为依据回答。
|
||||
|
||||
### 3.2 换策略
|
||||
- 重开选择器选另一策略 → 再次执行 3.1(新摘要作为新的依据条目追加;chip 切换为新策略)。旧依据条目保留在历史中(模型自然以最近/明确引用的为准)。
|
||||
|
||||
### 3.3 刷新依据
|
||||
- 已选状态下 chip 提供「刷新」:重新生成**当前时刻**摘要并注入(盘中数据变化后想拿最新)。
|
||||
- 语义:每次刷新=新快照依据条目(含新快照时间),不覆盖旧条目。
|
||||
|
||||
### 3.4 清除依据
|
||||
- 选择器提供「清除依据」:回到无依据普通会话(chip 复位为「讨论依据:未选择」);已注入的历史条目仍在消息流中,后续对话不再视为策略依据会话。
|
||||
|
||||
### 3.5 边界
|
||||
| 场景 | 行为 |
|
||||
|---|---|
|
||||
| 无可选策略(未配置) | 选择器显示空态「暂无策略,请到 设置→神之一手→策略分组 添加」 |
|
||||
| 会话进行中该策略被删除/改名 | chip 下次加载策略列表时按 id 对齐:找不到=显示「策略已删除」+ 可清除;改名后按 id 正常(显示新名);历史依据条目保留 |
|
||||
| 策略当前持仓为空 / 无行情 | 摘要照常生成:持仓区显示"当前无持仓";现价列显示 —;交易摘要照常(历史) |
|
||||
| 账本只读 | 摘要生成与注入路径无任何写操作;UI 无操作按钮(区别于数据 tab 的手动操作) |
|
||||
| 未选策略的普通聊天 | 与现状完全一致,不受影响(策略会话是可选增强,不锁定会话) |
|
||||
|
||||
## 4. 摘要规格(服务端 `basis-summary` 输出,markdown 文本)
|
||||
|
||||
结构(自上而下):
|
||||
1. **快照头**:`【神之一手数据依据】策略:做T | 快照:2026-09-08 14:32:05 | 数据为只读快照,非实时`
|
||||
2. **当前持仓**(活动行,逐行):code 名称|份额|可用|成本价|现价(— 缺失)|市值|浮动盈亏|持仓天数;尾部合计(N 只 / 总市值 / 总盈亏)。现价/市值缺行情时给—,不阻塞。
|
||||
3. **交易摘要(该策略全历史关联委托,聚合)**:总笔数/买入累计/卖出累计(按归属 holding 聚合):
|
||||
- 每 holding 一段:code 名称|状态(持有中/已清仓 清仓日)|买卖笔数|累计买入(量/额)|累计卖出(量/额)|当前份额;
|
||||
- 已清仓 holding 标注清仓日期与最后动作摘要;
|
||||
- 排序:持有中在前(按成本日/名称),已清仓在后(按清仓日倒序);
|
||||
- **规模上限**:holding 超过 20 行时按最新清仓倒序截断并注明「更多历史见 交易记录 tab」。
|
||||
4. **尾注**:只读提示「以上为策略数据快照;本会话只读,不执行任何交易/账本操作」。
|
||||
|
||||
字段精确取数与空值规则在技术实现方案细化。
|
||||
|
||||
## 5. 非目标(本期明确不做)
|
||||
|
||||
- 宿主工作区/目录注册(原 R-019 R3,已并入 R-021);
|
||||
- 账本写/交易执行、任何操作按钮;
|
||||
- 按需取数工具(AI 自行调用业务接口);
|
||||
- 复盘产物落盘 / 复盘管理(含 DB)——未来 T-002 方向;
|
||||
- 通用视图(全部持仓/交易记录)作依据;
|
||||
- 关注列表作为依据源。
|
||||
|
||||
## 6. 技术通道(设计假设,待调研确认)
|
||||
|
||||
注入的可行通道候选:① 宿主 injected-context/上下文节点(可折叠条目、非用户消息)→ 首选;② 会话 send(文本消息形态进会话)→ 兜底(呈现为一条前置依据消息)。**渲染与动作方案以调研结果为准**(本期设计保留两种形态的表达,UI 交互设计按"数据依据条目"统一描述)。
|
||||
|
||||
## 0.5 产品逻辑总图(2026-09-08 老师确认:画像 / 模型 / 主线 / 分期草案)
|
||||
|
||||
### 目标交易者画像(Q-画像,2026-09-08 确认)
|
||||
- **混合交易模式的多策略分仓管理**:同一账户下并存多种交易风格的策略分组(当前:手动做T + 程序化网格超市;未来:情绪流交易风格的管理逻辑);
|
||||
- 即"策略 = 分仓 + 交易风格/规则的容器";系统须能承载 手动主观 + 自动化规则 + 未来情绪流 三类玩法(彼此数据/规则/复盘口径不同)。
|
||||
|
||||
### 交易员工作模型(知识库归纳,作为产品逻辑基准)
|
||||
- **日循环**:盘前计划 → 盘中执行/监控 → 盘后记账核对 → 复盘总结 → 明日计划;
|
||||
- **策略循环**:规则制定 → 按规则执行 → 规则校验(复盘)→ 规则修订;
|
||||
- **复盘闭环**:重建事实 → 归因 → 对照规则 → 提炼改进;**复盘原料 = 决策时的依据记录**(知识库共识:没有决策留痕的复盘只是猜)。
|
||||
|
||||
### AI 分工定位(人机合一,只读)
|
||||
- **盘中 = AI 参谋台**(状态聚合 / 规则触发提醒 / 轻问答 / 决策留痕 / 人扣扳机执行),快、准、不打断;
|
||||
- **盘后 = AI 复盘主持人**(自动重建事实线 → 引导人过结论 → 结论留痕),人不被 AI 下"对错"裁决;
|
||||
- 决策依据记录(盘中一键留痕:理由 + 当时快照)是产品逻辑主线之一(2026-09-08 老师确认)——复盘有据的前提。
|
||||
|
||||
### 能力地图(现状 → 缺口)
|
||||
- 已具备"事实记录层":对账单快照 / 策略账本(唯一权威)/ 交易记录+归属 / 行情快照 / 策略 configSchema(可承载规则参数);
|
||||
- 缺口(按承接排序):①复盘事实线入口+可讨论会话(R-021 合并需求,本迭代)②决策依据留痕(盘中)③规则触发提醒 ④盘后自动日结/统计 ⑤复盘管理库(决策日志+复盘记录结构化存储检索)。
|
||||
|
||||
### 分期草案(2026-09-08 老师认可分步实现;**各 Phase 具体内容待讨论细化**)
|
||||
| 阶段 | 主题 | 交付设想(草案) |
|
||||
|---|---|---|
|
||||
| Phase 0(迭代 17) | 复盘会话闭环 | R-021 策略会话复盘(原 R-019+R-021 合并;策略数据依据 + 粒度复盘上下文,事实线 → 会话讨论 → 结论在会话) |
|
||||
| Phase 1 | 盘中辅助(雏形) | 决策依据一键留痕(理由+快照落库)+ 规则触发/偏离提醒雏形(configSchema × 行情)+ 会话内盘中快查 |
|
||||
| Phase 2 | 盘后日结与统计 | 自动日结(当日各策略事实线+盈亏+异常)→ 一键入会话复盘;周期统计(轮次收益/胜率/策略健康) |
|
||||
| Phase 3 | 复盘管理库 | 决策日志+复盘结论结构化沉淀/检索/关联(DB),对接目标-006;情绪流风格管理逻辑另立项 |
|
||||
|
||||
## 0.6 Phase 0 范围确认(2026-09-08 老师确认:就这样)
|
||||
|
||||
- **Phase 0 = 复盘会话闭环,范围 = R-021 单一合并需求(原 R-019 + R-021,2026-09-08 老师指令合并、逻辑整合入 R-021)**;
|
||||
- **复盘单元建模(A 拍板)**:引入"轮次 round"概念(holding 之下的交易周期聚合层):
|
||||
- 网格超市:一次买入触发 + 对应卖出配对 = 一轮(每格触发记一轮);
|
||||
- 手动做T:一次开仓 → 清仓 = 一轮;
|
||||
- 支持人工合并/拆分修正(先人工可调);
|
||||
- round 承接"复盘对象"的最小粒度,委托/段为 round 内明细;
|
||||
- **行情口径(C 拍板)**:本期事实线 = 交易记录 + 持仓史 + 现价对照(无历史 K 线/分时);历史行情背景 Phase 3 复盘库时再评估;
|
||||
- **决策日志预留(拍板)**:Phase 0 设计为"决策依据记录"留数据模型位置(时刻/理由/类别/当时快照/关联 round 或 holding),Phase 1 实现,不返工;
|
||||
- **风格差异**:复盘口径按策略类型可扩展(手动/网格,未来情绪流预留策略类型维度)。
|
||||
|
||||
## 7. D 问题(AI 建议项,待老师拍板)
|
||||
|
||||
| # | 问题 | AI 建议 |
|
||||
|---|---|---|
|
||||
| D-1 | 注入条目的呈现形态 | 对话流顶部「数据依据」条目:默认折叠为一行(图标+策略+快照时间+行数摘要),点开看全文;不伪装成用户消息 |
|
||||
| D-2 | 交易摘要聚合粒度 | 按持仓聚合(每持仓一段汇总 + 已清仓标注),不逐笔铺开(避免注入过长);如老师要"近期逐笔"可加近 N 笔小表 |
|
||||
| D-3 | 依据选择是否跨刷新持久化 | 本期**会话级不持久化**(刷新需重选;历史注入条目仍在消息流),与 R-016 会话级偏好一致、不加存储;复盘管理未来再考虑 |
|
||||
| D-4 | 选择控件挂载位置 | 对话视图输入框上方 accessory 行左侧(仿 DSH workspace chip 位置),仅聊天视图可见;替代方案=会话头部(与 QMT chip 并排)待老师定 |
|
||||
@@ -0,0 +1,39 @@
|
||||
# 迭代 17:策略会话(以策略当前数据为依据的会话讨论)
|
||||
|
||||
## 目标(一句话)
|
||||
|
||||
让 AI 会话可"以策略为 Workspace":对话开始前选「讨论策略」,预注入该策略**当前数据摘要**作为会话依据,AI 与老师围绕该策略持仓/交易做复盘与分析讨论;只读、会话中可换/刷新依据。
|
||||
|
||||
## 目标描述
|
||||
|
||||
- 老师原始诉求「新需求,让 AI 会话可以以策略为 Workspace」经四轮讨论定稿(R-019,2026-09-08;同日老师指令 R-019 并入 R-021 合并为单一需求):
|
||||
- **改造现有 DSH 对话窗口**,不引入宿主工作区/目录;
|
||||
- 会话的数据 = ①开始前/过程中注入的策略当前数据摘要 + ②会话过程本身产生的对话;复盘管理(可能带 DB 沉淀)为神之一手未来模块,本期不做;
|
||||
- 注入口径:当前持仓明细 + 该策略全历史关联交易聚合摘要(含已清仓);
|
||||
- 只读(Q4=A);预注入不挂工具(R2-Q1=A);产物不落盘(R2-Q4)。
|
||||
- **本迭代分阶段推进**:先设计(产品逻辑 + UI 交互逻辑,老师 2026-09-08 指令)→ 宿主注入通道调研 → 技术实现方案 → 实现与验收。
|
||||
|
||||
## 讨论过程
|
||||
|
||||
| 日期 | 轮次 | 要点 |
|
||||
|---|---|---|
|
||||
| 2026-09-08 | 需求讨论 Q1-Q4 | 语义收敛:非物理目录;数据为依据;会话内只读 |
|
||||
| 2026-09-08 | 需求讨论 R2 | 预注入;范围 R2-Q3=A;产物暂不落 |
|
||||
| 2026-09-08 | 需求讨论 R3 | 参考 DSH 工作区模式的"开始前选策略"体验,但**不用宿主工作区** |
|
||||
| 2026-09-08 | 需求定稿 F1/F2 | 首条消息前「讨论策略」选择 + 会话中可换/刷新;注入口径维持 R2-Q3=A |
|
||||
| 2026-09-08 | 老师指令 | 立项迭代:先做产品逻辑、UI 交互逻辑设计 |
|
||||
|
||||
## 本期(设计阶段)交付
|
||||
|
||||
- `产品逻辑设计.md`(数据与行为逻辑、摘要规格、状态与边界)
|
||||
- `UI交互设计.md`(交互流程、控件、视觉与文案、状态机)
|
||||
- D 系列设计问题(AI 建议项)→ 老师拍板 → 设计定稿
|
||||
|
||||
## 后续阶段(设计定稿后)
|
||||
|
||||
- 宿主注入通道技术调研 → 技术实现方案.md → 实现 → 验收标准.md + 验收(是否拆分独立迭代由老师定)。
|
||||
|
||||
## 对老师配合的请求
|
||||
|
||||
1. 拍板 D1-D4(产品/UI 设计中的 AI 建议项,见设计文档末);
|
||||
2. 实现验收需真实会话环境人工目视。
|
||||
@@ -0,0 +1,68 @@
|
||||
# 迭代 18 技术实现方案:QMT Bridge MCP 能力内建
|
||||
|
||||
> 依据:R-020(2026-09-08 定稿,Q1-Q6 + 老师澄清:移除三方 dsh-skill-mcp-panel、官方 dsh-mcp-client 可依赖)| PLAN-018
|
||||
> 日期:2026-09-08
|
||||
|
||||
## 目标架构
|
||||
|
||||
神之一手在 apply 内**动态挂载 DSH 官方 @deepseek-ai/dsh-mcp-client**(依赖官方库、管理在插件内),把 QMT Bridge 的 MCP 工具注册给模型;MCP 实例生命周期(挂/切/卸)随 QMT 连接配置的增删改与激活切换联动;状态出口 = 设置页 + 会话头部指示灯。REST 直连架构不变(技术约束-003)。
|
||||
|
||||
```
|
||||
神之一手 apply(ctx)
|
||||
├─ settings(QMT 连接配置:list + activeId + defaultId) [已有 R-004]
|
||||
├─ dataSource(REST 直连,baseUrl 随激活热切换) [已有]
|
||||
└─ QmtMcpManager(新增 src/mcp/QmtMcpManager.js)
|
||||
├─ 动态 import(@deepseek-ai/dsh-mcp-client) → ctx.plugin(module, config)
|
||||
│ config = { serverName: QMT_Bridge_MCP, transport: streamable-http,
|
||||
│ url: 激活 baseUrl + /mcp, failOnStartupError:false,
|
||||
│ reconnect: {enabled, 500ms→30s, 10次} } ← 与宿主现受管块同参(Q5)
|
||||
├─ 生命周期:start(启动挂载激活连接)/ resync(url)(激活·编辑地址·删除联动)
|
||||
├─ 状态:probe()(SDK listTools 握手:connected + 工具数 + 延迟 / error)
|
||||
│ getStatus() → { state, serverName, url, mounted, toolCount, error, latencyMs, checkedAt }
|
||||
└─ dispose:卸载 fiber(ctx.effect 释放)
|
||||
```
|
||||
|
||||
## 关键事实(已实证)
|
||||
|
||||
- `@deepseek-ai/dsh-mcp-client@0.1.1-rc.2` 已加入 dependencies;从插件 lib 可 import,模块导出 {name:mcp-client, inject:[tools], apply, Config},可 ctx.plugin() 动态挂载(mcp_router 同款实证);
|
||||
- serverName 存活实例内必须唯一 → 移三方受管块前插件实例与宿主实例不可并存(实施时序见 PLAN-018 注意事项);
|
||||
- dsh-mcp-client 不暴露连接状态查询 API → 状态用**独立 SDK 探测**(@modelcontextprotocol/sdk Client:initialize + tools/list → 工具数/延迟),与 dsh-mcp-client 自管连接互不干扰;
|
||||
- 宿主现 QMT_Bridge_MCP 受管块 = 三方 dsh-skill-mcp-panel 写入,连接与注册实为官方 dsh-mcp-client。
|
||||
|
||||
## 改动清单
|
||||
|
||||
| 文件 | 改动 |
|
||||
|---|---|
|
||||
| package.json | dependencies + @deepseek-ai/dsh-mcp-client@0.1.1-rc.2 + @modelcontextprotocol/sdk@1.30.0(已加) |
|
||||
| src/mcp/QmtMcpManager.js(新增) | MCP 实例管理:动态挂载/卸载 + resync + probe/getStatus + dispose |
|
||||
| src/index.js | 实例化 QmtMcpManager、start、ctx.effect 释放、传入 registerApi runtime |
|
||||
| src/api/index.js | runtime 增 mcpManager(分发 handleQmt/handleMarket) |
|
||||
| src/api/qmt-connections.js | METHODS + mcp-status;activate/update(地址变)/remove 热切换点 + mcpManager.resync(url) |
|
||||
| src/api/market.js | sync-status 增加 mcp 域(读 mcpManager.getStatus) |
|
||||
| src/client/views/SyncIndicators.jsx | 三灯 → 四灯:+「MCP」(绿=已连接 / 黄=重连中 / 红=断开 / 灰=未启用;点击刷新) |
|
||||
| src/client/views/SettingsSection.jsx | QMT 连接配置子 tab 顶部 MCP 状态条 + 「检查 MCP」按钮(手动触发 mcp-status refresh) |
|
||||
|
||||
## MCP 状态语义(对齐产品约束-012 指示灯体系)
|
||||
|
||||
| state | 灯色 | 说明 |
|
||||
|---|---|---|
|
||||
| connected | 绿 | 已连接 + N 工具(probe listTools 成功) |
|
||||
| connecting | 黄 | 挂载中 / 重连中 |
|
||||
| error | 红 | 不可达 / 失败(含原因) |
|
||||
| disabled | 灰 | 无激活连接(未启用) |
|
||||
|
||||
## 实现步骤
|
||||
|
||||
1. QmtMcpManager(挂/卸/resync/probe/getStatus/dispose);
|
||||
2. index.js 接线 + registerApi runtime;
|
||||
3. api:mcp-status + sync-status.mcp + 热切换联动;
|
||||
4. 客户端:SyncIndicators 四灯 + 设置页 MCP 状态条;
|
||||
5. 构建(pnpm build)验证;
|
||||
6. 宿主迁移(独立步骤,需老师确认 + editing-cordis-compositions 流程):卸载 dsh-skill-mcp-panel + 移除 cordis.patch.yml 受管块 → 重载 → 验证工具来自神之一手;
|
||||
7. 验收。
|
||||
|
||||
## 风险与注意
|
||||
|
||||
- 同 serverName 冲突:插件实例与宿主受管块不可并存(先插件后移除,或先移除后插件接管,二选一窗口);
|
||||
- ctx.plugin 子 fiber 需要 tools 服务:one-divine-lot inject 列表评估(对齐 mcp_router inject [webServer, tools]);
|
||||
- dsh-mcp-client 初始连接失败不抛(failOnStartupError:false),但**重复 serverName 会抛** → 挂载错误必须 catch,插件其余功能不受影响。
|
||||
@@ -0,0 +1,53 @@
|
||||
# 迭代复盘:18-QMTBridgeMCP内建
|
||||
|
||||
> 复盘日期:2026-09-08 | 迭代状态:**验收通过(2026-09-08)** —— 口径:服务端自动化验收全绿 + MCP 工具端到端实测 + UI 接线核验(构建产物含新控件)+ 真实调用顺带确认(老师参与验收:重启宿主 / 指令修正 / MCP 实测)
|
||||
> 关联需求:R-020(已定稿,2026-09-08)| 关联计划:PLAN-018
|
||||
|
||||
## 结果
|
||||
|
||||
迭代 18 达成(R-020 全范围):**QMT Bridge MCP 能力内建**——神之一手在 apply 内动态挂载 DSH 官方 @deepseek-ai/dsh-mcp-client(serverName 固定 QMT_Bridge_MCP,url 自动派生自激活连接 baseUrl+/mcp;挂载/切换/卸载/状态全由神之一手插件内管理);**三方依赖移除**(dsh-skill-mcp-panel 卸载 + 宿主 cordis.patch.yml 受管块清空);MCP 生命周期随 QMT 连接配置增删改与激活切换联动;状态出口 ×2(设置页「QMT 连接配置」MCP 状态条 + 会话头部指示灯区新增「MCP」灯);失败不崩(failOnStartupError:false + reconnect + 挂载 catch)。REST 直连架构不变(技术约束-003)。
|
||||
|
||||
## 验收证据(2026-09-08 实测)
|
||||
|
||||
| # | 验收项 | 结果 |
|
||||
|---|---|---|
|
||||
| 1 | 移除宿主受管块后工具仍可用 | ✅ cordis.patch.yml 清空 []、panel 已卸载;mcp__QMT_Bridge_MCP__qmt_list_apis 实际调用成功(QMT Bridge v2.0.0 返回) |
|
||||
| 2 | MCP url 随激活自动派生 | ✅ 100.110.38.78:8610/mcp(激活 baseUrl+/mcp,Q3) |
|
||||
| 3 | 激活切换联动 | ✅ 切 QMT_CYY → 192.168.3.43:8610/mcp connected;切回 GEMWIN 恢复 |
|
||||
| 4 | 删除激活自动切默认 + MCP 重挂 | ✅ 删死地址连接 → 自动切默认 GEMWIN → MCP 恢复 connected + 10 工具 |
|
||||
| 5 | 失败不崩(Q5) | ✅ 激活死地址 → state=error(fetch failed),插件其余功能正常 |
|
||||
| 6 | mcp-status(refresh=检测按钮语义) | ✅ refresh:true 即时探测 connected + 10 |
|
||||
| 7 | sync-status.mcp 域(头部灯数据源) | ✅ state/url/mounted/toolCount/latencyMs/checkedAt 齐全 |
|
||||
| 8 | 客户端接线 | ✅ build 产物含「MCP」指示灯 + 设置页「检测 MCP」按钮 + header.actions/settings.section 注册 |
|
||||
| 9 | MCP 工具端到端实测 | ✅ qmt_kline 取 000333.SZ 近一个月日 K(30 根 OHLCV,区间 -4.27%) |
|
||||
| 10 | 构建 | ✅ typecheck + pnpm build 通过(lib/mcp/QmtMcpManager.js 产出) |
|
||||
|
||||
## 过程事实
|
||||
|
||||
1. **需求讨论定稿**:老师指令(2026-09-08)→ R-020 登记 → Q1-Q6 逐项拍板(只挂激活连接 / 移除三方 / url 自动派生 / 双状态出口 / 失败重连采纳 / 不做通用 MCP 管理);
|
||||
2. **老师指令修正(关键)**:AI 一度把「动态挂载官方 dsh-mcp-client」误读为要自研协议,先撤依赖后澄清——老师明确:不要的是**三方 dsh-skill-mcp-panel**,**官方 @deepseek-ai/dsh-mcp-client 可以依赖**;文档同步更正(R-020 / PLAN-018 / 迭代目标 / 索引);
|
||||
3. **依赖可行性实证(PLAN-018 步骤 1)**:@deepseek-ai/dsh-mcp-client@0.1.1-rc.2(registry 有)加为 dependencies,从插件 lib 实证可 import,模块导出 {name:mcp-client, inject:[tools], apply, Config} → ctx.plugin() 动态挂载可行(mcp_router 同款路径);@modelcontextprotocol/sdk@1.30.0 供独立状态探测(dsh-mcp-client 不暴露状态 API);
|
||||
4. **实现**:src/mcp/QmtMcpManager.js(挂/卸/resync/probe/getStatus/dispose);index.js 接线(start + dispose + registerApi runtime);api/qmt-connections.js(mcp-status + activate/update/remove 三热切换点 resync);api/market.js(sync-status.mcp 域);客户端 SyncIndicators(四灯 + MCP)+ SettingsSection(MCP 状态条 + 检测按钮);tsdown 增 mcp 域入口;
|
||||
5. **宿主迁移(老师执行,验收方确认)**:cordis.patch.yml 受管块清空 + dsh-skill-mcp-panel 卸载 + 宿主重启 → 神之一手实例接管;
|
||||
6. **验收**:服务端 8 项 + MCP 实测 + 构建全绿(上表);
|
||||
7. **提交推送**:c700f2f(迭代18,16 files)+ 9382ace(迭代17 遗留文档补提交)→ origin/main。
|
||||
|
||||
## 经验教训(复盘沉淀)
|
||||
|
||||
### 1. 「去掉对三方插件的依赖」要先问清:依赖边界到底指哪一层
|
||||
AI 把老师「不希望依赖三方插件来加入 MCP 能力」误推到「也不能依赖 DSH 官方 mcp-client → 要自研 MCP 协议」,先动手撤了官方依赖、准备重造轮子;老师澄清后回撤。沉淀:**需求里「不依赖 X」先精确定位 X 是谁(管理面板 vs 执行插件),并确认官方能力是否允许复用;架构岔路口先问一句,别先动手拆**。
|
||||
|
||||
### 2. 动态挂载子插件 = ctx.plugin(module, config),前提是模块在插件依赖树内可解析
|
||||
mcp_router 实证了「import 官方模块 → ctx.plugin()」路径;one-divine-lot 照做时关键前置是把官方模块加进自己的 dependencies(而非 peer 裸声明),并用 createRequire 从插件 lib 路径实证解析成功再动代码。沉淀:**动态挂载依赖模块时,先验证「插件运行路径能解析到模块」这个前置条件(技术约束-003 语义外的新增实践)**。
|
||||
|
||||
### 3. 状态查询与连接管理分离:dsh-mcp-client 不暴露状态 API
|
||||
官方插件只管连接与工具注册,无状态查询口 → 用 @modelcontextprotocol/sdk 独立只读探测(initialize + tools/list 拿工具数/延迟),与官方自管连接互不干扰。沉淀:**外部能力库若不开状态口,用协议级只读探测补状态出口,不试图扒内部字段**。
|
||||
|
||||
### 4. 验收期间临时造数要小心「先清后补」
|
||||
用死地址测失败语义时两次 add 同名 TEST-DEAD 未清理(返回结构读错导致 id 取空),留下两条残留连接;发现后立即清理、用正确返回结构重测。沉淀:**验收脚本对临时数据的增删要成对且校验返回值;测试前后核对连接列表与激活态**。
|
||||
|
||||
## 关联
|
||||
|
||||
- R-020(已定稿 → 已实现 → 本迭代验收通过);宿主三方 dsh-skill-mcp-panel 已移除(其「技能/MCP」设置菜单与受管块职责移交完成)
|
||||
- 新增依赖:@deepseek-ai/dsh-mcp-client@0.1.1-rc.2、@modelcontextprotocol/sdk@1.30.0
|
||||
- 若需新增设计约束条目(如「插件内建 MCP 客户端注册规范」)——本迭代未新增,留待后续讨论(R-020 定稿时标注"如需…于定稿时补充"未触发)
|
||||
@@ -0,0 +1,19 @@
|
||||
# 迭代 18:QMT Bridge MCP 能力内建(去三方插件依赖)
|
||||
|
||||
## 目标(一句话)
|
||||
|
||||
神之一手插件在 apply 内动态挂载 DSH 官方 dsh-mcp-client(serverName 固定 QMT_Bridge_MCP,url 自动派生自激活连接 baseUrl+/mcp,挂载/切换/卸载/状态均由神之一手插件内管理),MCP 能力不再依赖三方 dsh-skill-mcp-panel(卸载其插件 + 移除宿主 cordis.patch.yml 受管块)。
|
||||
|
||||
## 目标描述
|
||||
|
||||
- 依据 R-020(2026-09-08 定稿,Q1-Q6)+ PLAN-018;
|
||||
- Q1 只挂激活连接;Q2 移除三方块由神之一手管理;Q3 url 自动派生;Q4 状态出口=设置页卡片检查 + 会话头部 MCP 指示灯;Q5 失败不崩+重连;Q6 不做通用 MCP 管理;
|
||||
- REST 直连架构不变(技术约束-003)。
|
||||
|
||||
## 讨论过程
|
||||
|
||||
| 日期 | 轮次 | 要点 |
|
||||
|---|---|---|
|
||||
| 2026-09-08 | 需求讨论 Q1-Q6 | R-020 定稿(见 R-020.md) |
|
||||
| 2026-09-08 | 立项 | 老师:「启动,这个不是一个大需求」→ 快速迭代 |
|
||||
| 2026-09-08 | 指令澄清 | 老师修正:不要的是三方 dsh-skill-mcp-panel;依赖官方 @deepseek-ai/dsh-mcp-client 没问题 → 方案定为官方库动态挂载 + 移除三方面板 |
|
||||
@@ -0,0 +1,10 @@
|
||||
# 迭代 18 验收标准:QMT Bridge MCP 能力内建
|
||||
|
||||
> 验收方法/验收目标见 PLAN-018「验收标准 / 验收方法」逐条。
|
||||
|
||||
1. 移除宿主 cordis.patch.yml QMT_Bridge_MCP 受管块后,模型仍能用 mcp__QMT_Bridge_MCP__qmt_* 工具(来源=神之一手动态挂载,工具集一致);
|
||||
2. 激活连接切换 → MCP 实例 url 跟随;编辑/删除激活连接 → 同步重挂无残留;
|
||||
3. 连接列表为空 → 不挂实例(mcp-status=disabled),其余功能不受影响;
|
||||
4. QMT 不可达 → 插件不崩、MCP 灯红、恢复后自动重连绿灯;
|
||||
5. 设置页连接卡片 MCP 状态 + 检查按钮;会话头部 MCP 指示灯四灯齐全;
|
||||
6. 现有功能无回归;typecheck + build 通过。
|
||||
@@ -0,0 +1,45 @@
|
||||
# 技术实现方案:19-指示灯状态自适应探测
|
||||
|
||||
> 迭代编号:19 | 依据:R-022 + R-023 | 涉及约束:产品约束-012(指示灯体系)、技术约束-016、技术约束-020/021(本迭代新增)
|
||||
|
||||
## 方案总览
|
||||
|
||||
两只连接类状态缓存统一改「**结果驱动 setTimeout 自适应链**」:每次探测完成(成功/失败/异常)后按结果排下一轮。
|
||||
|
||||
| 维度 | QmtHealthMonitor(R-022) | QmtMcpManager(R-023) |
|
||||
|---|---|---|
|
||||
| 健康/已连接 | intervalMs 默认 5 分钟(稳态,原设计) | intervalMs 默认 5 分钟 |
|
||||
| 异常/未知/未连接 | retryMs 默认 10 秒快速重试 | retryMs 默认 10 秒快速重试 |
|
||||
| 探测目标 | 激活 baseUrl + /health(GET) | 激活 baseUrl + /mcp(SDK Client 握手,只读) |
|
||||
| 停止调度 | stop()(mounted 守卫) | 无激活连接 / dispose() / unmount() |
|
||||
| 并发防护 | _inFlight 去重 | _probing 去重 |
|
||||
| 前端/API | 零改动(读缓存秒回) | 零改动(getStatus 读缓存秒回) |
|
||||
|
||||
## 实现步骤
|
||||
|
||||
### R-022 QmtHealthMonitor(src/data-source/QmtHealthMonitor.js)
|
||||
|
||||
1. 常量 HEALTH_RETRY_MS=10s;构造注入 { intervalMs, retryMs }(测试用短间隔,对齐 QuoteSync 先例);
|
||||
2. 固定 setInterval(5min) → _nextDelay()/_scheduleNext()(setTimeout 链,探测完成重排);
|
||||
3. resolveActiveBaseUrl 移入 try:解析失败也落 healthy:false 缓存走快速重试
|
||||
(原实现解析在 try 外,抛错场景灯会灰且不自动重试——顺带修复的隐性缺陷);
|
||||
4. start() 立即探测不变;stop() 用 clearTimeout + mounted 守卫。
|
||||
|
||||
### R-023 QmtMcpManager(src/mcp/QmtMcpManager.js)
|
||||
|
||||
1. 常量 PROBE_RETRY_MS=10s;构造注入 { intervalMs, retryMs };
|
||||
2. probe() 结果写缓存后 _scheduleNext();target 为空早退置 disabled 并取消定时器;
|
||||
3. unmount() 取消定时器;dispose() 置 _disposed + 取消 + 卸载;_probing 防并发;
|
||||
4. 探测本体(SDK Client connect/initialize/tools/list)与挂载/重连逻辑零改动。
|
||||
|
||||
### 回归
|
||||
|
||||
- scripts/test-health-monitor.mjs(10 项:启动即探测/异常快速重试自动恢复/健康稳态不空转/stop 停止调度…)
|
||||
- scripts/test-mcp-status.mjs(12 项:无激活连接不调度/异常快速重试自动恢复/已连接稳态不空转/dispose 停止调度/节奏选择;
|
||||
SDK 握手不可纯内存 mock,守护调度机制:stub probe 翻转状态 + 触达真实 _scheduleNext)
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- src/data-source/QmtHealthMonitor.js、src/mcp/QmtMcpManager.js(核心改动)
|
||||
- scripts/test-health-monitor.mjs、scripts/test-mcp-status.mjs(新增回归)
|
||||
- lib/*(build 再产物,部署用)
|
||||
@@ -0,0 +1,45 @@
|
||||
# 迭代复盘:19-指示灯状态自适应探测
|
||||
|
||||
## 结论
|
||||
|
||||
达成(R-022 + R-023 全范围):会话头部两只「连接类」指示灯(QMT连接 / MCP)统一改为
|
||||
**结果驱动自适应探测**——健康/已连接维持 5 分钟稳态(原设计不变),异常/未知每 10 秒快速重试;
|
||||
QMT Bridge 与其 MCP 服务启动/恢复后两灯 ≤20s 自动回绿,不再等满 5 分钟 / 不再永不恢复。
|
||||
前端指示灯与 sync-status / qmt-health / mcp-status 端点零改动(读缓存秒回语义不变)。
|
||||
|
||||
## 事实记录
|
||||
|
||||
### R-022 QMT 健康灯(src/data-source/QmtHealthMonitor.js)
|
||||
|
||||
- 根因(已证实):健康缓存固定 5 分钟探测一次激活 /health;其余三灯有高频自愈回路
|
||||
(持仓 10s / 行情 5s / MCP 客户端重连)。Bridge 在两次探测间启动/恢复时缓存停留在上次失败结果。
|
||||
- 变更:setInterval(5min) → setTimeout 自适应链(_nextDelay/_scheduleNext,结果驱动);
|
||||
HEALTH_RETRY_MS=10s;构造注入 { intervalMs, retryMs };resolveActiveBaseUrl 移入 try
|
||||
(解析失败也落 healthy:false 走快速重试,原实现该场景灯灰且不自动重试);_inFlight 防并发保留。
|
||||
|
||||
### R-023 MCP 状态灯(src/mcp/QmtMcpManager.js)
|
||||
|
||||
- 根因(已证实):状态缓存只在 挂载/切换/手动探测 时刷新,无定时回路;dsh-mcp-client 自带 reconnect
|
||||
只恢复真实连接(fiber 层),不刷新插件状态缓存。
|
||||
- 变更:probe() 结果写缓存后 _scheduleNext();PROBE_RETRY_MS=10s;构造注入 { intervalMs, retryMs };
|
||||
无激活连接早退置 disabled 并取消定时器;unmount()/dispose() 停止调度;_probing 防并发;
|
||||
探测本体与挂载/重连逻辑零改动。
|
||||
|
||||
### 验证
|
||||
|
||||
- scripts/test-health-monitor.mjs 10/10 绿;scripts/test-mcp-status.mjs 12/12 绿(调度机制守护);
|
||||
- pnpm typecheck 0 错;pnpm build 通过(lib 两文件均含自适应逻辑);
|
||||
- 存量回归 test-quote-sync / test-position-sync 均 0 退出。
|
||||
|
||||
## 待老师确认事项
|
||||
|
||||
1. R-022 / R-023 定稿确认(边界:健康/已连接稳态 5 分钟不变 / 异常 10s 重试 / 不新增配置 / API 前端零改动);
|
||||
2. 真实环境人工验收(验收标准.md 第 8 条):停/启 QMT Bridge → QMT 灯与 MCP 灯 ≤20s 自动回绿、
|
||||
点击仍即时探测、插件重载后缓存秒回。
|
||||
|
||||
## 经验沉淀(候选)
|
||||
|
||||
- 定时轮询类功能的恢复感知要与故障探测解耦:固定低频定时器做稳态,异常态必须用快速重试把「恢复」
|
||||
也纳入自愈回路——灯类状态"看起来没生效"的根因通常是恢复路径的响应时延,而不是探测本身失效。
|
||||
- 「外层有自管重连」不等于「状态缓存会自愈」:重连只恢复真实连接,展示层缓存若无自己的刷新回路,
|
||||
必须单独加状态自适应探测(R-023 教训)。
|
||||
@@ -0,0 +1,32 @@
|
||||
# 迭代目标:19-指示灯状态自适应探测(QMT 健康灯 + MCP 灯)
|
||||
|
||||
## 目标
|
||||
|
||||
会话头部两只「连接类」指示灯(QMT连接 / MCP)改为**结果驱动自适应探测**:
|
||||
健康/已连接时维持 5 分钟稳态探测(低开销),异常/未知时每 10 秒快速重试,
|
||||
使 QMT Bridge 与其 MCP 服务启动/恢复后指示灯 ≤20s 自动回绿,不再等满 5 分钟。
|
||||
|
||||
## 目标描述
|
||||
|
||||
- **背景**:两只灯的数据分别来自 QmtHealthMonitor / QmtMcpManager 的内存状态缓存,刷新机制均为
|
||||
固定/缺失低频定时回路——QMT 健康缓存固定每 5 分钟探测一次(R-022),MCP 状态缓存只在
|
||||
挂载/切换/手动探测时刷新、无定时回路(R-023)。持仓(10s)/ 行情(5s)灯靠各自定时同步秒级自愈;
|
||||
唯独两只连接灯恢复感知滞后,老师实测反馈「QMT连接灯一直没亮」「MCP 灯也不自动恢复」(2026-09-09)。
|
||||
- **范围**:只改 QmtHealthMonitor(R-022)与 QmtMcpManager(R-023)的探测调度;前端指示灯组件、
|
||||
sync-status / qmt-health / mcp-status 端点零改动;健康稳态间隔 5 分钟不变;不新增配置项。
|
||||
- **验收线**:两灯异常/未知态 ≤10s 重试;健康/已连接态维持 5 分钟不空转;服务恢复后缓存回正常态
|
||||
沿快速重试被感知;无激活连接与 dispose 后停止调度。对应需求 R-022 + R-023。
|
||||
|
||||
## 目标讨论过程
|
||||
|
||||
1. 老师报告症状(2026-09-09):Bridge 启动后其他三灯自动点亮,QMT 灯不亮 → 定位为健康缓存固定 5 分钟探测。
|
||||
2. AI 提议自适应节奏(健康 5min / 异常 10s),老师确认方向,迭代 19 实施(R-022)。
|
||||
3. 老师复查发现 MCP 灯同样不自动恢复(R-023)→ 确认 QmtMcpManager 状态缓存无定时回路,
|
||||
dsh-mcp-client 重连只恢复真实连接、不刷新本类缓存 → 同模式加自适应探测,并入本次迭代。
|
||||
4. 决策点见 R-022 / R-023 决策记录。
|
||||
|
||||
## 对老师(项目主理人)的配合需求
|
||||
|
||||
- 确认 R-022 / R-023 定稿与迭代 19 验收(核实标准见验收标准.md);
|
||||
- 真实环境人工验收:停/启 QMT Bridge → QMT 灯 ≤20s 回绿、MCP 灯 ≤20s 回绿;点击灯/「检测 MCP」仍即时探测;
|
||||
插件重载后灯按缓存秒回并正常。
|
||||
@@ -0,0 +1,29 @@
|
||||
# 验收标准:19-指示灯状态自适应探测
|
||||
|
||||
## 验收标准线
|
||||
|
||||
| # | 标准 | 判定 |
|
||||
|---|---|---|
|
||||
| 1 | QMT 健康:异常/未知态按 retryMs(默认 10s)快速重试 | test-health-monitor [2]:≥2 次快速重试且缓存 healthy=false |
|
||||
| 2 | QMT 健康:Bridge 恢复后 ≤ 一个重试周期缓存回 healthy=true(灯回绿),无需等 5 分钟 | 同脚本 [2] |
|
||||
| 3 | QMT 健康:健康态维持稳态间隔(默认 5 分钟)不空转 | 同脚本 [3] |
|
||||
| 4 | QMT 健康:stop() 后停止自动调度 | 同脚本 [4] |
|
||||
| 5 | MCP:无激活连接置 disabled 且不调度(真实 probe 早退不触 SDK) | test-mcp-status [1] |
|
||||
| 6 | MCP:异常态按 retryMs 快速重试;MCP 服务恢复后自动探测到 connected(灯回绿) | 同脚本 [2] |
|
||||
| 7 | MCP:已连接态按 intervalMs 稳态不空转;dispose() 后停止调度;_nextDelay 节奏正确 | 同脚本 [3][4][5] |
|
||||
| 8 | 启动即探测 + 读缓存秒回语义不变(QMT 健康 / MCP 状态) | 两脚本 [1];sync-status/qmt-health/mcp-status 端点零改动 |
|
||||
| 9 | 构建产物(lib/)含两处自适应逻辑;typecheck 0 错;存量不回归 | pnpm typecheck + pnpm build;test-quote-sync / test-position-sync 通过 |
|
||||
|
||||
## 验收方法
|
||||
|
||||
- 自动化:node scripts/test-health-monitor.mjs(10/10)+ node scripts/test-mcp-status.mjs(12/12)全绿;
|
||||
pnpm typecheck;pnpm build;存量回归脚本 0 退出。
|
||||
- 人工(老师真实环境):重启插件后四灯状态秒回;停 QMT Bridge → QMT 灯 ≤10s 转红、MCP 灯 ≤10s 转红、
|
||||
持仓/行情灯按 30s/15s 阈值转黄;**重新启动 QMT Bridge → QMT 灯与 MCP 灯 ≤20s 自动回绿**(本次修复核心);
|
||||
点击 QMT 灯 /「检测 MCP」仍即时探测并刷新悬停详情。
|
||||
|
||||
## 验收目标
|
||||
|
||||
修复确认:QMT Bridge 及其 MCP 服务启动/恢复后,会话头部 QMT连接灯与 MCP 灯自动回绿(≤20s),
|
||||
与其余两盏数据灯恢复节奏一致;健康/已连接稳态无额外探测开销;前端与 API 行为不变。
|
||||
验收通过后 R-022 / R-023 定稿并归档(老师确认)。
|
||||
@@ -0,0 +1,88 @@
|
||||
# 技术实现方案:20-策略列设置拖拽排序
|
||||
|
||||
## 总体思路
|
||||
|
||||
在 ColumnSettingsPopover.jsx 内复用 Tab 设置(SettingsSection.jsx TabSettings)已验证的
|
||||
**原生 HTML5 Drag & Drop** 模式:行 draggable + onDragStart/onDragOver/onDrop + 落点高亮 + drop 重排,
|
||||
零第三方依赖。持久化沿用现有 strategy-columns/update 整表覆盖 RPC,把「保存」语义从手动按钮改为
|
||||
每次变更(drop / 勾选 / ↑↓)立即提交。
|
||||
|
||||
## 改动点(单文件:src/client/views/ColumnSettingsPopover.jsx)
|
||||
|
||||
### 1. 状态
|
||||
|
||||
- 已有 `dragKey`(未被使用,正好用于 DnD):当前被拖行的 column.key;
|
||||
- 新增 `overKey`:当前悬停落点的 column.key(用于高亮);
|
||||
- `saving` 保持:正在写库时禁止再次拖动/勾选。
|
||||
|
||||
### 2. 持久化函数(替换原 save)
|
||||
|
||||
- 新增 `persist(nextList, msg)`:提交整表 `{key, visible}` → strategy-columns/update;
|
||||
成功 Toast 提示 + onSaved() 刷新;失败 Toast 错误 + 本地回滚(还原为服务端最新列配置)。
|
||||
- 勾选显隐:切完立即 persist(不再等「保存」按钮);
|
||||
- ↑↓ 箭头:移动完立即 persist;
|
||||
- drop 重排:重排完立即 persist(对齐 Tab 设置)。
|
||||
|
||||
### 3. 拖拽事件(照搬 TabSettings 模式)
|
||||
|
||||
```jsx
|
||||
<div key={c.key} draggable={!saving}
|
||||
onDragStart={(e) => { setDragKey(c.key); e.dataTransfer.effectAllowed = 'move'; }}
|
||||
onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; setOverKey(c.key); }}
|
||||
onDragLeave={() => { if (overKey === c.key) setOverKey(null); }}
|
||||
onDrop={(e) => { e.preventDefault(); handleDrop(c.key); }}
|
||||
style={{ cursor: saving ? 'default' : 'grab',
|
||||
background: overKey === c.key ? 'color-mix(in srgb, var(--dsw-alias-state-business-primary, #1565c0) 12%, transparent)' : 'transparent' }}>
|
||||
<span>≡</span>
|
||||
<button>↑</button><button>↓</button>
|
||||
<input type="checkbox" ... />
|
||||
...
|
||||
</div>
|
||||
```
|
||||
|
||||
### 4. handleDrop(照搬 TabSettings 逻辑,key 定位)
|
||||
|
||||
```js
|
||||
const handleDrop = (targetKey) => {
|
||||
if (!dragKey || dragKey === targetKey) { setDragKey(null); setOverKey(null); return; }
|
||||
const from = list.findIndex((c) => c.key === dragKey);
|
||||
const to = list.findIndex((c) => c.key === targetKey);
|
||||
if (from < 0 || to < 0) { setDragKey(null); setOverKey(null); return; }
|
||||
const next = list.slice();
|
||||
const [moved] = next.splice(from, 1);
|
||||
next.splice(to, 0, moved);
|
||||
setDragKey(null); setOverKey(null);
|
||||
persist(next, `「${moved.label}」已移动`, next);
|
||||
};
|
||||
```
|
||||
|
||||
### 5. UI 文案
|
||||
|
||||
- 标题下提示改为:「勾选显示列,按住 ≡ 拖动调整顺序;代码/名称/操作固定」;
|
||||
- 底部按钮区:去掉「保存」,保留「关闭」;
|
||||
- 每行最前加 ≡ 手柄(配色沿用 Tab 设置 `#999`/label-tertiary)。
|
||||
|
||||
## 边界与不做
|
||||
|
||||
- 服务端、API、表格渲染零改动(strategy-columns/update 已是整表覆盖语义);
|
||||
- 固定列不参与排序,不进列表(现状维持);
|
||||
- 不做方案 C(表格表头直接拖列头),不引入第三方 DnD 库;
|
||||
- `saving` 期间禁用拖拽与勾选,防并发写。
|
||||
|
||||
## 回归
|
||||
|
||||
- scripts/test-r013-columns.mjs(列配置归一化/读写的服务端逻辑,未改动应全绿);
|
||||
- npm run typecheck + build(tsdown 产物,弹层 JSX 变更需重编 web bundle);
|
||||
- 页面人工验收:拖拽重排 / 落点高亮 / drop 即存 / ↑↓ 微调 / 刷新保持。
|
||||
## 实现纪要(2026-09-10,老师验收反馈后修订)
|
||||
|
||||
> 老师反馈:初版按原方案做的是「整行高亮」(overKey → 目标行淡蓝底),拖动时无法判断会插入目标列的**前面还是后面**,
|
||||
> 观感不符合习惯。定稿修订为 **间隙高亮线**:
|
||||
|
||||
- 落点判定:拖动悬停时读鼠标在目标行内的 **Y 坐标**(getBoundingClientRect),上半 → 插入该行**之前**(线画行上缘),
|
||||
下半 → 插入该行**之后**(线画行下缘);两行之间显示 3px 主色圆角高亮线(absolute 定位在行自身 padding 内,left/right 6,
|
||||
无负偏移无裁剪风险),指示精确插入位置。
|
||||
- drop 判定:drop 事件内直接按坐标重算 before(不依赖 state 闭包,避免滞后);无操作原地(insertAt 等于原槽位)
|
||||
跳过写库;移除后目标下标左移(to > from 时 to-1)。
|
||||
- 状态:overKey(整行)→ overPos { idx, half }(间隙线锚点行 + 上下缘);保留 onDragEnd 清理防拖出弹层残留。
|
||||
- 边界维持:固定列不参与、服务端零改动、↑↓ 兜底保留、立即持久化不变。
|
||||
@@ -0,0 +1,47 @@
|
||||
# 迭代复盘:20-策略列设置拖拽排序
|
||||
|
||||
## 结论
|
||||
|
||||
达成(R-024):策略 tab「列设置」弹层的列排序从 ↑↓ 箭头点按升级为**鼠标选中后拖动排序**,
|
||||
复用 R-011 Tab 设置已验证的原生 HTML5 DnD 模式;老师 2026-09-10 页面确认交互直观,验收通过。
|
||||
|
||||
## 事实记录
|
||||
|
||||
### src/client/views/ColumnSettingsPopover.jsx(单文件改动,服务端零改动)
|
||||
|
||||
- 排序交互升级:每行新增 ≡ 拖拽手柄(整行 draggable),拖到目标位置松手即重排;↑↓ 箭头保留作兜底微调。
|
||||
- **落点立即持久化**:勾选显隐 / 拖拽 / ↑↓ 任一操作立即调 strategy-columns/update 整表提交;弹层去掉「保存」
|
||||
按钮只留「关闭」,Toast 反馈;saving 期间禁用拖拽与勾选防并发写;失败回滚到服务端最新配置。
|
||||
- **落点指示修订(老师验收反馈)**:初版为整行高亮(overKey → 目标行淡蓝底),老师反馈无法判断插入目标列的
|
||||
前/后;改为**行间间隙高亮线**——按鼠标在目标行内的 Y 坐标取上半/下半,决定线画在该行上缘(插其前)/下缘(插其后),
|
||||
明确指示精确插入位置;drop 事件内按坐标重算(不依赖 state 闭包),拖回原位置不重复写库,onDragEnd 清理残留。
|
||||
- 修复:间隙线渲染曾误将 style 对象直接作 React child(`{gapLineStyle(i)}`),改为条件渲染 `<div style={gapLineStyle(i)}/>`。
|
||||
|
||||
### 验证
|
||||
|
||||
- pnpm typecheck 0 错;pnpm build 通过(client bundle 185KB 含间隙线逻辑,grep 确认编译产物);
|
||||
- test-r013-columns.mjs 回归:11 过 / 5 败 —— **与本次改动无关**(git stash 基线验证同样 11/5);
|
||||
失败根因:脚本仍按 R-013 时期期望「6 个基础列」,而 COLUMN_META 已有 10 列(R-015 新增涨停/跌停/今开/最高 4 个默认隐藏列),
|
||||
测试期望未随迭代 13 更新的存量债务,不在本迭代范围,列为遗留事项。
|
||||
|
||||
### 文档
|
||||
|
||||
- docs/05-需求池/R-024.md:定稿 + 变更记录(整行高亮 → 间隙高亮线);
|
||||
- docs/03-设计约束/UI交互约束.md:新增 UI约束-007(列表/弹层排序交互规范:间隙高亮线标准,Tab 设置是否统一待老师评估);
|
||||
- docs/04-迭代记录/20-策略列设置拖拽排序/:迭代目标 + 技术实现方案(含实现纪要)+ 验收标准(10 项)。
|
||||
|
||||
## 遗留事项(待老师决策)
|
||||
|
||||
1. **test-r013-columns.mjs 期望过期**:COLUMN_META 现 10 列(R-015 新增 4 行情列默认隐藏),脚本期望仍为 6 列,
|
||||
默认列数相关 5 项断言失败。建议后续小迭代同步更新脚本期望(或重构为从 COLUMN_META 推导期望),本次未动。
|
||||
2. **Tab 设置整行高亮是否统一为间隙高亮线**:UI约束-007 列设置弹层已用间隙线,Tab 设置(UI约束-003)仍为整行高亮;
|
||||
老师反馈列设置「直观多了」——若 Tab 设置同样已存在「无法判断插入前/后」的观感问题,可同模式调整(独立小任务,本次未动)。
|
||||
|
||||
## 经验沉淀(候选)
|
||||
|
||||
- 排序类拖拽的落点指示,**间隙线(两行之间)比整行高亮更符合用户观感**:整行高亮只能表达「落在哪一行」,
|
||||
无法表达「插入该行前还是后」;主流列表/表格拖拽(如 Notion、表格列排序)均用间隙指示。
|
||||
- 服务端整表覆盖语义(updateStrategyColumns 整表提交)天然适配前端即时持久化,无需逐条增量接口;
|
||||
「保存按钮」在每次变更即写库后失去存在意义(本迭代据此移除)。
|
||||
- 回归脚本的期望值要随数据模型演进同步更新(test-r013-columns 教训:R-015 扩展 COLUMN_META 后脚本未跟随,
|
||||
留下 5 项静默失败,直到本迭代回归才暴露)。
|
||||
@@ -0,0 +1,30 @@
|
||||
# 迭代目标:20-策略列设置拖拽排序
|
||||
|
||||
## 目标
|
||||
|
||||
策略 tab「列设置」弹层(ColumnSettingsPopover)的列排序交互从 **↑↓ 箭头点按** 升级为
|
||||
**鼠标选中后拖动排序**(原生 HTML5 DnD),并对齐 Tab 设置的即时持久化体验。
|
||||
|
||||
## 目标描述
|
||||
|
||||
- **背景**:策略 tab 的可配置列(基础数据列 + 自定义字段列)在「列设置」弹层中管理,排序目前依赖
|
||||
↑↓ 箭头(R-013 迭代 11 产物,当时刻意简化,代码注释自述「用箭头替代 DnD」)。老师反馈希望
|
||||
改成鼠标选中后拖动排序(2026-09-10)。
|
||||
- **结论**:复用 R-011 Tab 设置已验证的原生 HTML5 DnD 模式(draggable 行 + 落点高亮 + drop 重排),
|
||||
零第三方依赖;落点立即持久化(strategy-columns/update 整表覆盖),弹层去掉「保存」按钮只留「关闭」。
|
||||
- **范围**:仅改 src/client/views/ColumnSettingsPopover.jsx 单文件;服务端/API/表格渲染零改动;
|
||||
固定列(代码/名称/操作/展开箭头)不参与排序,维持现状。
|
||||
- **验收线**:拖拽可重排列顺序且落点高亮可见;drop 后立即写库(无需点保存);↑↓ 箭头仍可微调;
|
||||
勾选显隐立即生效;刷新后列顺序保持。对应需求 R-024(已定稿)。
|
||||
|
||||
## 目标讨论过程
|
||||
|
||||
1. 老师反馈(2026-09-10):策略 tab 列设计可否做成鼠标选中后拖动排序。
|
||||
2. AI 调研:Tab 设置(R-011)已实现同款原生 HTML5 DnD(SettingsSection.jsx),可零依赖复用;
|
||||
列设置弹层存在未使用的 dragKey 状态(原计划 DnD 的残留)。
|
||||
3. 登记 R-024(讨论中)→ 方案草案 A/B/C → 老师确认方案 A(弹层内拖拽);
|
||||
保存时机 / 箭头去留按 AI 推荐默认(立即持久化 + ↑↓ 保留兜底)。
|
||||
|
||||
## 对老师(项目主理人)的配合需求
|
||||
|
||||
- 无(纯前端交互优化,服务端零改动,验收走页面人工确认)。
|
||||
@@ -0,0 +1,32 @@
|
||||
# 验收标准:20-策略列设置拖拽排序
|
||||
|
||||
## 验收线(在哪里验收)
|
||||
|
||||
DSH Web GUI → 神之一手 → 任一策略 tab → 工具条「列设置」按钮 → 弹层。
|
||||
页面侧人工验收;服务端逻辑零改动,跑 scripts/test-r013-columns.mjs 回归确认无破坏。
|
||||
|
||||
## 验收方法(逐项操作)
|
||||
|
||||
| # | 操作 | 预期 |
|
||||
|---|---|---|
|
||||
| 1 | 打开「列设置」弹层,鼠标按住某行 ≡ 手柄(含整行)拖动到另一行附近移动鼠标 | 目标行上缘/下缘显示**主色间隙高亮线**(插入线),明确指示落点在目标列之前还是之后,非整行高亮 |
|
||||
| 2 | drop 后直接关闭弹层(不点任何保存) | 策略表格表头列顺序已按新顺序展示(立即持久化生效) |
|
||||
| 3 | 刷新页面 / 重新打开弹层 | 列顺序保持拖拽后的结果(写库成功) |
|
||||
| 4 | 点 ↑ / ↓ 箭头 | 仍可逐格微调,且立即持久化(关闭弹层后表格列顺序同步变化) |
|
||||
| 5 | 勾选/取消勾选某一列 | 显隐立即生效(关闭弹层后,表格对应列隐藏/出现) |
|
||||
| 6 | 拖动到目标行上半区 vs 下半区各松手一次 | 上半区 = 插入该目标列**之前**;下半区 = 插入该目标列**之后**(以落点线位置为准) |
|
||||
| 7 | 拖动期间把行拖回原位置或原地松手 | 无异常,列顺序不变(不重复写库) |
|
||||
| 8 | 固定列(代码/名称/操作) | 弹层列表不含固定列,不参与拖拽,恒显示(维持现状) |
|
||||
| 9 | 自定义字段列(某策略配了 configSchema) | 与基础列一样可拖拽排序、可勾选显隐 |
|
||||
| 10 | 保存中(快速连续拖动) | 无并发写异常;saving 期间拖拽/勾选禁用,Toast 提示保存结果 |
|
||||
|
||||
## 验收目标
|
||||
|
||||
- 功能正交:拖拽排序 / ↑↓ 微调 / 显隐勾选 三者并存且各自立即持久化;
|
||||
- 零回归:scripts/test-r013-columns.mjs 全绿、typecheck + build 通过;
|
||||
- 视觉一致性:≡ 手柄沿用 Tab 设置(UI约束-003)同套样式;落点为**间隙高亮线**(主色 token,UI约束-004 无硬编码色值);
|
||||
- 服务端零改动确认:git diff 无 src/api / src/settings.js 变更。
|
||||
|
||||
## 判定
|
||||
|
||||
- 老师页面人工确认 10 项全过 + 服务端回归全绿 ⇒ 迭代 20 验收通过,R-024 归档至 已完成/ 并更新索引实现状态。
|
||||
@@ -0,0 +1,84 @@
|
||||
# 技术实现方案:21-Tab设置排序间隙高亮统一
|
||||
|
||||
## 总体思路
|
||||
|
||||
把 SettingsSection.jsx 的 TabSettings 组件落点指示从「整行高亮(overId → 行背景 color-mix 12%)」
|
||||
改为「行间间隙高亮线(overPos { idx, half } → 目标行上缘/下缘 3px 主色线)」,与 ColumnSettingsPopover.jsx
|
||||
(R-024)逐点对齐;其余交互(≡ 手柄、开关、立即持久化、徽标、saving 禁用)不动。
|
||||
|
||||
## 改动点(单文件:src/client/views/SettingsSection.jsx,仅 TabSettings 组件)
|
||||
|
||||
### 1. 状态
|
||||
|
||||
- `overId`(整行高亮锚点)→ `overPos { idx, half }`(间隙线锚点行下标 + 上/下缘);
|
||||
- `dragId` 保留。
|
||||
|
||||
### 2. 悬停判定(与 R-024 全同)
|
||||
|
||||
```jsx
|
||||
const handleOver = (e, index) => {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
const half = e.clientY - rect.top < rect.height / 2 ? 'top' : 'bottom';
|
||||
setOverPos({ idx: index, half });
|
||||
};
|
||||
```
|
||||
|
||||
### 3. drop(事件内按坐标重算,不依赖 state 闭包)
|
||||
|
||||
```jsx
|
||||
const handleDrop = async (e, index) => {
|
||||
e.preventDefault();
|
||||
if (!tabs || !dragId) { setDragId(null); setOverPos(null); return; }
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
const before = e.clientY - rect.top < rect.height / 2;
|
||||
let to = before ? index : index + 1;
|
||||
const from = tabs.findIndex((x) => x.id === dragId);
|
||||
if (from < 0 || to < 0 || to > tabs.length) { setDragId(null); setOverPos(null); return; }
|
||||
if (to === from || to === from + 1) { setDragId(null); setOverPos(null); return; } // 原地跳过写库
|
||||
const next = tabs.slice();
|
||||
const [moved] = next.splice(from, 1);
|
||||
const insertAt = to > from ? to - 1 : to;
|
||||
next.splice(insertAt, 0, moved);
|
||||
const ordered = next.map((x, i) => ({ ...x, order: i }));
|
||||
setDragId(null);
|
||||
setOverPos(null);
|
||||
await persist(ordered, '「' + moved.name + '」已移动');
|
||||
};
|
||||
```
|
||||
|
||||
### 4. 行渲染(tr 上挂间隙线样式)
|
||||
|
||||
tr 为表格行,直接内嵌 div 会破坏表格结构(tr 只允许 td/th)——**间隙线以 td 为定位锚点绘制**:
|
||||
首选在每个 td 内各画一段 3px 高亮线(结构合规、视觉连续),实现纪要记录最终选型。
|
||||
|
||||
```jsx
|
||||
<tr
|
||||
key={t.id}
|
||||
draggable={!saving}
|
||||
onDragStart={(e) => { setDragId(t.id); setOverPos(null); e.dataTransfer.effectAllowed = 'move'; }}
|
||||
onDragOver={(e) => handleOver(e, i)}
|
||||
onDrop={(e) => handleDrop(e, i)}
|
||||
onDragEnd={() => { setDragId(null); setOverPos(null); }}
|
||||
style={{ cursor: saving ? 'default' : 'grab' }}
|
||||
>
|
||||
<td style={{ position: 'relative', padding: '8px 6px' }}> {gapLine(i, 'td1')} ≡</td>
|
||||
...
|
||||
</tr>
|
||||
```
|
||||
|
||||
间隙线样式(与 R-024 全同):absolute、left/right 内缩 6、height 3、borderRadius 2、
|
||||
背景 primary token;top:0(插前)/ bottom:0(插后)。
|
||||
|
||||
## 边界与不做
|
||||
|
||||
- 服务端/API/表格列结构/持久化逻辑零改动;
|
||||
- 不动「策略分组」子 tab(该处无排序交互);
|
||||
- 不引入第三方 DnD 库。
|
||||
|
||||
## 回归
|
||||
|
||||
- pnpm typecheck + build(SettingsSection 变更需重编 web bundle);
|
||||
- 页面人工验收:Tab 设置拖动 → 间隙线位置正确(上=前/下=后)、整行高亮消失、drop 即存、
|
||||
拖回原位不重复写库、刷新页面后顺序保持。
|
||||
@@ -0,0 +1,26 @@
|
||||
# 迭代目标:21-Tab设置排序间隙高亮统一
|
||||
|
||||
## 目标
|
||||
|
||||
设置页「Tab 设置」的分组 tab 拖拽排序落点指示,从**整行高亮**统一为**行间间隙高亮线**,
|
||||
与策略 tab 列设置弹层(R-024/UI约束-007)完全一致——明确指示插入目标行的前/后。
|
||||
|
||||
## 目标描述
|
||||
|
||||
- **背景**:迭代 20 复盘遗留第 2 项——R-024 将列设置弹层落点改为间隙高亮线后老师确认「直观多了」,
|
||||
但设置页 Tab 设置(R-011/UI约束-003)仍是整行高亮(overId → 目标行淡蓝底),无法判断插入前/后,
|
||||
两处观感不一致。2026-09-10 老师指令:Tab 设置也统一为间隙高亮线形式。
|
||||
- **范围**:仅改 src/client/views/SettingsSection.jsx 的 TabSettings 组件落点指示;表格结构、≡ 手柄、
|
||||
显隐开关、立即持久化(tabs/update)、徽标、saving 禁用全部维持不变;服务端/API 零改动。
|
||||
- **验收线**:拖动时目标行上缘/下缘显示主色间隙线(上半区=插前、下半区=插后);整行高亮消失;
|
||||
拖回原位不重复写库;drop 后立即持久化。对应需求 R-025(已定稿)。
|
||||
|
||||
## 目标讨论过程
|
||||
|
||||
1. 迭代 20 复盘(2026-09-10)列遗留第 2 项:Tab 设置整行高亮是否统一为间隙线,留待老师评估。
|
||||
2. 老师指令(2026-09-10):设置页分组 tab 排序也改上面(列设置弹层)的拖动高亮形式 → 登记 R-025 已定稿,
|
||||
迭代 21 实施。
|
||||
|
||||
## 对老师(项目主理人)的配合需求
|
||||
|
||||
- 无(纯前端交互统一,服务端零改动,验收走页面人工确认)。
|
||||
@@ -0,0 +1,27 @@
|
||||
# 验收标准:21-Tab设置排序间隙高亮统一
|
||||
|
||||
## 验收线(在哪里验收)
|
||||
|
||||
DSH Web GUI → 设置页 → 「Tab 设置」子 tab → 拖动任一标签行排序。
|
||||
页面侧人工验收;服务端逻辑零改动。
|
||||
|
||||
## 验收方法(逐项操作)
|
||||
|
||||
| # | 操作 | 预期 |
|
||||
|---|---|---|
|
||||
| 1 | 打开「Tab 设置」,按住某行 ≡ 手柄(含整行)拖动到另一行附近移动鼠标 | 目标行上缘/下缘显示**主色间隙高亮线**(插入线),明确指示落点在目标 tab 之前还是之后;原整行淡蓝高亮消失 |
|
||||
| 2 | 拖动到目标行上半区 vs 下半区各松手一次 | 上半区 = 插入该 tab **之前**;下半区 = 插入该 tab **之后**(以落点线位置为准) |
|
||||
| 3 | drop 后落点立即生效 | Toast「已移动」,刷新页面后 tab 顺序保持(tabs/update 已写库) |
|
||||
| 4 | 拖动期间把行拖回原位置或原地松手 | 无异常,顺序不变,且不触发重复写库 |
|
||||
| 5 | 既有交互回归 | ≡ 手柄、显示/隐藏开关、内置/策略徽标、saving 期间拖拽禁用——全部维持原行为 |
|
||||
| 6 | 策略分组子 tab | 无排序交互,不受影响(仍为 新增/重命名/删除) |
|
||||
|
||||
## 验收目标
|
||||
|
||||
- 功能对准:Tab 设置落点与列设置弹层(R-024)**同一套间隙线交互**,观感一致;
|
||||
- 零回归:既有 tabs 持久化(整表 order + visible)、显隐开关、策略改名联动 name join 不受影响;
|
||||
- typecheck + build 通过;无新增硬编码色值(UI约束-004)。
|
||||
|
||||
## 判定
|
||||
|
||||
- 老师页面人工确认 6 项全过 + build 通过 ⇒ 迭代 21 验收通过,R-025 归档至 已完成/ 并更新索引实现状态。
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user