Files
one_divine_lot/docs/04-迭代记录/04-WS盘中价格实时更新/迭代说明.md
T
kyugao bb3f8bd28d feat: 迭代03-05 交易记录接入、行情实时、QMT连接配置、data store 标准化
- R-004 QMT连接配置:多配置管理 + 会话头部快捷切换(03-QMT连接配置)
- R-005 WS盘中价格实时更新:服务端中转+缓存+前端轮询(04-WS盘中价格实时更新)
- R-006 策略数据 JSON data store 标准化(store.json/market.json/schema.json)
- R-007 交易记录接入:当日订单+成交单表合并(委托主行+展开成交明细)、时间段查询(今日/本周/本月+手动起止)、费用计算(佣金费率万m_dCommission+最低5元、印花税卖出万5、过户费双向万0.1)
- 架构治理:api 按领域拆分、component 目录、QmtHealthMonitor
2026-09-01 13:21:49 +08:00

483 lines
37 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 迭代说明:04-WS盘中价格实时更新
> 本文件为本次迭代的**修改记录**(追加式,只记事实):每完成一处修改,追加一条记录(改了什么 / 为什么 / 涉及文件)。
> 创建:2026-08-31
## 修改记录
### 2026-08-31 · 建立迭代 04 文档骨架
- **做了什么**:需求 T-005 草稿转正为 R-005docs/05-需求池/R-005.md);创建计划 PLAN-004docs/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 · 客户端:MarketDataProviderWS 行情客户端)
- **做了什么**:新增 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 } 价格 MapuseReducer);
- 事件驱动: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.jswithConnection 内包一层 MarketDataProvider3 个表格共享同一行情实例;
- 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.18lastClose=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 快照轮询走服务端代理)→ 一次返回全部关注 code600719.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_idtick-snapshot 返回真实持仓现价);前端 bundle 确认加载新代码(rev=ae40cadd056a,与本地 lib 一致)。
- **端到端链路验证**(Node 模拟前端调宿主 /odl/api):
- 激活配置 GEMWIN → 订阅 oksub_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.jsMarketCache 类(服务端连 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 直连 GEMWIN2026-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.jsMarketCache 加 REST tick 快照兜底(每 5s 对缓存已有 code 用 /data/tick 刷新);
- src/api.jsmarket-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.jsQMT 连接域(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 全部 OK600719.SH 100 股往返,清理干净);
- 核心端点:market-snapshot 13/13、active-host GEMWIN、QMT CRUD、subscribe-ws/unsubscribe-ws、tick-snapshotlastPrice=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.getByCodesHTTP 编排纯净);
- src/api/qmt-connections.js:热切换 marketCache?.setBaseUrl → marketFeed?.setBaseUrl
- src/api/index.jsruntime 命名 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/13miss 补拉正常,600719.SH → 7.18);二次查询命中缓存 13 条;
- summary 13 只;qmt-connections / active-hostGEMWIN/ activate 热切换 / subscribe-ws+unsubscribe-ws / tick-snapshot7.18/ strategies2 个)全部 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.jsposition/manager.js → component/PositionManager.jsdata-source/qmt-bridge-rest.js → component/QmtBridgeRestDataSource.jsdata-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 1000300057 = 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.jsSTORE_SCHEMA 定义 + 读写 store.json(原子写)+ 迁移 + dataset CRUDgetDataset/setDataset/removeDataset/getAllDatasets);
- 重写 src/component/PositionManager.js:存储改接 DataStore(策略为中心),对外方法签名不变(addToStrategy/removeFromStrategy/getSummary 等);
- src/index.jsDataStore 替代 AllocationStorage 装配。
**验证**(测试隔离目录):
- 迁移:allocations.jsoncode 为中心)→ 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.jsoncode → snapshot,原子写);
- MarketDataHub 接入:启动 warmup 读盘 → 内存缓存首屏即有价;ingest 增量更新后防抖写盘(3s);查询命中顺序:内存 → 磁盘 → REST 补拉;
- index.jsdataStore 注入 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 创建时引用 storagedataStore: 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:移除「行情实时」状态条**
- 前端表格彻底移除 MarketStatusBarPriceCell 删除该组件;AllPositionsTab / StrategyTab 移除引用与 status/wsInfo 解构)。
- 原则:行情刷新是服务端职责,前端只拿结果,对行情状态彻底无感。
- 涉及文件:src/client/views/PriceCell.jsx、AllPositionsTab.jsx、StrategyTab.jsx。
**需求 3QMT 连接健康检查**
- 新增 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-statusQMT 健康 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=trueWS 稳定)、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.jsqmt-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 膨胀到 38MB50910 只股票行情)。
- **根因**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 · 修复 v2warmup 认可全市场导致 watchCodes 过滤失效
- **问题**:重启后 store.market.json 又膨胀到 38MB50910 键)——watchCodes 过滤未生效。
- **根因**MarketDataHub.warmup 从磁盘加载旧数据时,把**所有 code 都加入 watchCodes**this.watch(code))——磁盘历史认可了全市场,后续 WS 推送全部保留。
- **修复**warmup 只加载数据到内存 cache**不加入 watchCodes**watchCodes 只由「持仓 + 前端查询」驱动)。
- **清理**:再次删除膨胀文件(重建后只含持仓 13 条)。
- **涉及文件**src/component/MarketDataHub.js。
### 2026-09-01 · 修复 v3setMarketQuotes 整体替换(清除旧键残留)
- **问题**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 从 38MB50910 键)恢复为 **11KB13 键 = 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 工作空间)。