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:
@@ -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 工作空间)。
|
||||
|
||||
Reference in New Issue
Block a user