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
This commit is contained in:
2026-09-01 13:21:49 +08:00
parent 5a723d514e
commit bb3f8bd28d
57 changed files with 5244 additions and 397 deletions
@@ -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.jsxWS 客户端(连接/订阅/价格 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.js377 行)按领域拆分 → 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=wholecodes=SH/SZ),前端按关注列表过滤;
3. 关注列表来源:**3 表格自动汇总**;
4. 现价展示:**现价列 + 红涨绿跌 + 变化高亮**,刷新机制为**价格叠加层**(不整表重拉);
- 2026-08-31 协议实测(GEMWIN 100.110.38.78:8610):订阅/退订/推送/快照格式全部确认。
## 对老师的配合需求
- 验收需要 GEMWINTailscale 100.110.38.78:8610)可达(已确认可达);
- 构建安装后重载页面配合验证;
- 验收通过后确认归档。
@@ -0,0 +1,482 @@
# 迭代说明: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 工作空间)。
@@ -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 个表格出现的所有股票;
- 交互(着色/高亮/重连提示)符合预期。