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

37 KiB
Raw Blame History

迭代说明: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 typecheck0 错误)+ 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/subscribebody { 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 都加入 watchCodesthis.watch(code))——磁盘历史认可了全市场,后续 WS 推送全部保留。
  • 修复:warmup 只加载数据到内存 cache不加入 watchCodeswatchCodes 只由「持仓 + 前端查询」驱动)。
  • 清理:再次删除膨胀文件(重建后只含持仓 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 工作空间)。