迭代13: 盘口内存快照(QuoteSync/QuoteHub)+ 同步指示灯 + 行情列
- QuoteSync/QuoteHub 替换 MarketFeed/MarketDataHub(方案A 删除重写): 5s REST 同步 watch 集合 + 涨停/跌停经 /data/instrument 按交易日内存缓存 + 读穿透走适配层 getTicks(修正旧 hub 绕适配层的层级破洞) - WS 数据通路移除(从无生效结论;ingest 留 source 标签回归口子) - market_quotes_cache 表退役 DROP(幂等);价格单一入口 = QuoteHub - 数据同步指示灯组: QMT连接|持仓数据|行情数据(绿/黄/灰,点击即同步/即时探测) + sync-status 端点(吸收 market-stats)+ sync-now;修复 runtime 漏传 positionSync 致持灯恒灰 - 策略持仓表行情列: 涨停价/跌停价/今开/最高(COLUMN_META defaultVisible=false,纯价格) - 回归 test-quote-sync.mjs 27 项(含 DROP 幂等真实 SQLite 验证) - 文档链: R-015 + PLAN-014 + 迭代三件套 + 技术约束-010/012变更、018新增 + 产品约束-012 需求: R-015(老师五拍板: 替换/WS移除/纯内存DROP/watch现状/指示灯)
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
# 计划:盘口内存快照(QuoteSync/QuoteHub + 同步指示灯)(阶段航点)
|
||||
|
||||
> 编号:PLAN-014 | 粒度:阶段航点 | 创建:2026-09-02 | 状态:**已实施(待验收)**
|
||||
> 派生自终极目标:目标-003(按策略监控市场)、目标-008(真实交易系统接入)
|
||||
> 依据需求:**R-015(已定稿,2026-09-02,老师逐项拍板)** —— 符合入范围门槛
|
||||
> 设计约束:技术约束-010 变更(行情服务端中转改纯内存)、技术约束-012 变更(market_quotes_cache 退役)、技术约束-016(分域沿用);本次新增 技术约束-018、产品约束-012
|
||||
|
||||
## 目标
|
||||
|
||||
盘口数据与持仓数据对称收敛:QuoteSync/QuoteHub 纯内存管理(5s REST 同步 + 涨停跌停合约信息),market_quotes_cache 退役(DROP),价格单一入口;会话头部新增持仓/盘口同步指示灯(绿黄灰、点击即同步)。
|
||||
|
||||
## 范围
|
||||
|
||||
**做**:
|
||||
1. QuoteSync(新):启动 prime + 5s REST 刷新 watch 集合 + 涨停跌停按交易日拉取缓存 + 热切换重置;WS 通路不迁移(删除);
|
||||
2. QuoteHub(新):内存快照 + 合约信息缓存 + watch 集合 + 读穿透(走 dataSource.getTicks)+ getQuote(s) 对外;对外方法签名与旧 hub 兼容(getByCodes/watch/stats/getQuote);
|
||||
3. 数据源适配:QmtBridgeRestDataSource 新增 getTicks(codes)(/data/tick 语义化批量);
|
||||
4. 存储清理:SqliteStore 删行情表读写 + DROP TABLE market_quotes_cache(幂等)+ migrateJson/isEmpty 联动;DataStore 删 loadMarket/getMarketQuote(s)/setMarketQuotes;
|
||||
5. API:market-snapshot 返回加涨停/跌停/昨收/updatedAt;新增 sync-status(吸收 market-stats)+ sync-now {domain};
|
||||
6. 前端:QmtConnectionChip 加 SyncIndicators(两圆点,10s 轮询,点击即同步);MarketDataProvider 清理 wsInfo 残留;
|
||||
7. 策略持仓表行情列(老师确认并入):COLUMN_META + 涨停价/跌停价/今开/最高(defaultVisible: false,纯价格);StrategyTab.renderDataCell 对应 case;normalizeStrategyColumns 缺省显隐跟随 defaultVisible;
|
||||
7. 回归脚本 test-quote-sync.mjs(纯内存 mock)。
|
||||
|
||||
**不做**:watch 收缩/降频;个股停牌标识;WS 相关需求推进;PriceCell 改动;最低/成交量/成交额列;距离涨停百分比展示。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- 新增:src/market/QuoteSync.js、src/market/QuoteHub.js、src/client/views/SyncIndicators.jsx、scripts/test-quote-sync.mjs
|
||||
- 删除:src/market/MarketFeed.js、src/market/MarketDataHub.js
|
||||
- 修改:src/data-source/QmtBridgeRestDataSource.js(getTicks)、src/storage/SqliteStore.js、src/storage/DataStore.js、src/api/market.js、src/api/qmt-connections.js(热切换联动)、src/index.js、src/client/views/QmtConnectionChip.jsx、src/client/market/MarketDataProvider.jsx
|
||||
|
||||
## 实现步骤
|
||||
|
||||
1. 文档链(本计划 + 迭代三件套 + 约束条目);2. 数据源 getTicks;3. QuoteHub + QuoteSync;4. 存储清理 + DROP;5. API 改造;6. 前端指示灯 + 清理;7. 回归脚本 + typecheck + build + 存量回归;8. 复盘。
|
||||
|
||||
## 验收要点
|
||||
|
||||
见 `docs/04-迭代记录/13-盘口内存快照/验收标准.md`。
|
||||
@@ -20,6 +20,8 @@
|
||||
| 产品约束-008 | 交易记录支持**按策略过滤**:交易记录 tab 提供策略过滤下拉(全部 / 各策略 / 未关联),对今日(QMT 实时)与历史(本地 SQLite)均生效;策略归属 = 委托时间 join 持仓生命周期窗口推导(一码多策略取份额最大,未命中=未关联);历史范围展示本地积累数据(不再「接口开发中」占位) | 2026-09-01 | 生效 | - | 新增(2026-09-01 R-009 定稿 + 迭代 07 实施):策略过滤 + 历史本地展示 |
|
||||
| 产品约束-010 | 策略自定义字段配置:每个策略可在设置页「策略分组」子 tab 配置自定义字段定义(字段名 / 类型文本·数字·布尔·枚举 / 枚举选项 / 默认值),定义随策略存 settings(不落库);该策略下每个持仓(strategy_holdings 行)按所属策略的定义存取一份字段值(values JSON 列,持仓级键值对,key 对齐定义、允许扩展额外键);旧策略无定义时行为与现状一致(不渲染字段区、不迁移历史值) | 2026-09-02 | 生效 | - | 新增(2026-09-02 R-013 定稿 Q1-Q4 + D6):定义随策略走、值随持仓行,类型化(含枚举),旧策略兼容 |
|
||||
| 产品约束-009 | 会话 tab 统一由「Tab 设置」管理:设置页「Tab 设置」子 tab 是**所有会话 tab(系统内置 + 策略分组)的唯一顺序与显隐入口**,两类 tab 混排;每行 = 拖动排序 + 显示/隐藏开关;**任何 tab 均不支持重命名与删除**(内置 tab 名称只读,策略命名/删除仍在「策略分组」子 tab);策略改名后 Tab 设置中的名称自动跟随(只存引用);新增策略默认追加到列表末尾;删除策略联动删除 Tab 设置中对应条目;顺序与显隐唯一数据源 = settings.tabs 有序数组 | 2026-09-02 | 生效 | - | R-011 定稿(2026-09-02 Q1-Q5 确认):Tab 设置 = 显示/隐藏 + 拖动排序(落点立即持久化),全 tab 禁重命名/删除 |
|
||||
| 产品约束-011 | 持仓页数据(策略持仓 / 全部持仓 / 未分配)以服务端 10s 内存快照为准,**接受最多 10s 滞后**(同步时间前端不显示,老师拍板);QMT 抖动/掉线时持仓页面显示**最后一次快照**而非空白;QMT 已卖光的票(连续 3 轮同步确认消失)本地持仓自动转历史(幽灵清仓,不物理删除);部分减持仅表现为「未分配为负」,不做自动修正 | 2026-09-02 | 生效 | - | 新增(2026-09-02 R-014 定稿):老师四问拍板(内存不落库 / 读穿透兜底 / 幽灵自动清仓 / 不显示同步时间) |
|
||||
| 产品约束-012 | 行情数据服务形态(R-015):现价/昨收/涨停/跌停统一由盘口内存快照提供(≤5s 更新),价格单一入口;QMT 抖动时页面价格保持旧值不空白;会话头部(QMT 健康灯旁)提供**持仓/盘口同步指示灯**——绿=同步正常(持仓 30s/盘口 15s 内)、黄=同步失败中快照陈旧、灰=从未同步;悬停显示同步时间/快照量/失败数/错误摘要;点击灯 = 立即触发该域同步;不做个股停牌标识(另议) | 2026-09-02 | 生效 | - | 新增(2026-09-02 R-015 定稿):老师提出指示灯,采纳 AI 推荐三态/点击即同步/取消 PriceCell 灰点 |
|
||||
<!-- 示例条目(确认格式后删除):
|
||||
| 产品约束-001 | 示例:求签功能必须保证抽取结果的不可预测性 | 2026-08-26 | 生效 | - | 讨论确认:为保证公平性,抽取必须不可预测 |
|
||||
-->
|
||||
@@ -17,15 +17,17 @@
|
||||
| 技术约束-006 | 客户端 slots.register 的 component 必须是第二参数(register({...}, Component));settings schema 必须用 schemastery z.object() 函数式定义 | 2026-08-28 | 生效 | - | 迭代 01 复盘沉淀:component 位置错误致 React #130;普通对象 schema 报 schema is not a function |
|
||||
| 技术约束-007 | 插件安装用 dsh plugin add(自动 reconcile bundles),不直接用 pnpm add;bundle patch 顶层必须是 insert 操作 | 2026-08-28 | 生效 | - | 迭代 01 复盘沉淀:pnpm add 不会更新 dsh.profile.bundles |
|
||||
| 技术约束-008 | QMT 连接配置存储复用 one-divine-lot settings namespace(新增 qmtConnections 字段:list[{id,name,baseUrl,order}] + activeId + defaultId),与策略配置同机制持久化;激活切换 = 更新数据源实例的 baseUrl(数据源按请求读取地址,已核实),立即生效无需重启 DSH;插件启动时激活默认配置(列表为空时回退 cordis 注入的 qmtBaseUrl 兜底,不做自动迁移);测试连接由服务端代理请求 {baseUrl}/health(避免浏览器跨域) | 2026-08-29 | 生效 | - | R-004 定稿(2026-08-29):Q1/Q2/Q4/Q5/Q8 确认(启动自动激活默认、立即切换、不迁移、超时不配置化、复用 settings) |
|
||||
| 技术约束-012 | 插件数据存储遵循 **docs/03-设计约束/数据存储设计.md**:存储引擎为 **SQLite(node:sqlite)**——strategy_holdings + market_quotes_cache + **trade_orders + trade_fills(交易记录两表,R-009)**;存储层单票生命周期操作;交易记录两表零冗余 strategy_id/holding_id(外键链推导策略归属);策略定义仍存 DSH settings;旧 JSON(store.json / store.market.json / allocations.json)经一次性迁移脚本 + 启动自动迁移(幂等、迁移前自动备份)后废弃;仅存储引擎替换,对外行为不变 | 2026-09-01 | 生效 | - | 变更(2026-09-01 R-008 定稿 + 迭代 06 实施):JSON data store → SQLite;变更(2026-09-01 R-009 定稿 + 迭代 07 实施):+trade_orders/trade_fills 交易记录两表(零冗余,外键链推导策略归属) |
|
||||
| 技术约束-012 | 插件数据存储遵循 **docs/03-设计约束/数据存储设计.md**:存储引擎为 **SQLite(node:sqlite)**——strategy_holdings + ~~market_quotes_cache~~(**R-015 退役 DROP,2026-09-02**;行情改内存快照)+ **trade_orders + trade_fills(交易记录两表,R-009)**;存储层单票生命周期操作;交易记录两表零冗余 strategy_id/holding_id(外键链推导策略归属);策略定义仍存 DSH settings;旧 JSON(store.json / store.market.json / allocations.json)经一次性迁移脚本 + 启动自动迁移(幂等、迁移前自动备份)后废弃;仅存储引擎替换,对外行为不变 | 2026-09-01 | 生效 | - | 变更(2026-09-01 R-008 定稿 + 迭代 06 实施):JSON data store → SQLite;变更(2026-09-01 R-009 定稿 + 迭代 07 实施):+trade_orders/trade_fills 交易记录两表;**变更 3(2026-09-02 R-015/迭代 13)**:market_quotes_cache 退役(DROP),行情移出 SQLite |
|
||||
| 技术约束-011 | 测试/回归脚本**禁止在真实数据上执行写操作**:份额写操作(add/remove/move/clear)必须使用独立数据目录(AllocationStorage 支持 ODL_TEST_DATA_DIR 环境变量或 dataDir 参数指向临时目录),只读端点(positions/summary/strategies/market-snapshot)可直连生产 API | 2026-08-31 | 生效 | - | 2026-08-31 数据误删事故沉淀:回归测试误删大连热电/万顺新材份额分配,老师定「测试用独立数据目录」 |
|
||||
| 技术约束-010 | 行情实时数据由**服务端中转 + 缓存**提供(R-005 演进,2026-08-31 老师改):DSH 服务端做「WS 订阅 + REST 轮询 + 行情缓存」,前端统一轮询 /odl/api/market-snapshot(不做前端直连,无跨域);行情持久化到 store.market.json(重启不丢价,首屏快速展现);QMT Bridge WS 推送是会话级/有状态行为(归属 QMT Bridge 工作空间) | 2026-08-31 | 生效 | - | 变更(2026-08-31):老师由「前端直连 WS」改为「服务端中转 + 缓存」——解决跨域与 WS 语义不稳定问题;2026-09-01 加行情持久化与启动 prime |
|
||||
| 技术约束-010 | 行情实时数据由**服务端中转 + 内存缓存**提供(R-005 演进,2026-08-31 老师改;R-015 二改,2026-09-02):DSH 服务端做「REST 轮询 + 行情内存缓存」,前端统一轮询 /odl/api/market-snapshot(不做前端直连,无跨域);~~行情持久化到 store.market.json~~(**R-015 失效:改纯内存,QuoteSync 启动 prime + 读穿透保证秒级有价**);~~QMT Bridge WS 推送~~(**R-015 移除 WS 通路**,见技术约束-018) | 2026-08-31 | 生效 | - | 变更(2026-08-31):老师由「前端直连 WS」改为「服务端中转 + 缓存」;2026-09-01 加行情持久化与启动 prime;**变更 2(2026-09-02 R-015/迭代 13)**:持久化与 WS 均失效——行情纯内存(QuoteSync/QuoteHub),落库与 WS 通路删除 |
|
||||
| 技术约束-009 | 会话头部快捷切换控件挂载 DSH 开放 slot `conversation.session.header.actions`(多实例挂载点,按 order 排序多插件共存):客户端插件以独立 id 并排注册(DSH 内置 PTC 标签 order=-10,本控件 order=-9),不改动 DSH 宿主;控件经 ConnectionProvider 包装复用现有 RPC 通道与 /odl/api/* 端点 | 2026-08-29 | 生效 | - | R-004 定稿(2026-08-29):Q9 确认;宿主代码审查核实 slot 机制与内置插件注册方式 |
|
||||
|
||||
| 技术约束-013 | 交易记录本地存储(R-009):QMT 当日交易数据(委托/成交)由服务端 TradeSync 定时同步落 SQLite(启动预热 + 60s 定时 + UPSERT 幂等,只同步当日);trade_orders(委托主行,order_id 主键 + insert_ts 派生时间列 + **strategy_id/holding_id 手动归属列**)+ trade_fills(成交明细,trade_id 主键、order_id 外键)两表;**委托归属由用户在交易记录 tab 手动设置**(候选 = 该 code 当前持仓策略 + 未关联,全手动选、可随时改、以最终为准);**UPSERT 不覆盖归属列**(手动指定为插件逻辑);本地历史查询走 trades/history 端点(策略过滤 = 用户设置的归属);今日实时仍走 QMT Bridge;QMT 委托/成交 code 无后缀、持仓带后缀 —— 数据源映射层统一 normalizeInstrumentCode 归一化;委托交易日 = insertDate(tradeDate 兜底) | 2026-09-01 | 生效 | - | 新增(2026-09-01 R-009 定稿 + 迭代 07 实施):两表 + 定时同步 + 本地历史查询;变更 1(2026-09-01):+code 归一化 + tradeDate 兜底;变更 2(2026-09-01 老师二次定稿):归属改**手动设置**(trade_orders 冗余 strategy_id+holding_id,UPSERT 不覆盖归属列),弃算法推导 |
|
||||
| 技术约束-015 | 策略自定义字段存储(R-013,2026-09-02):字段定义随策略定义存 settings.strategies 扩展 configSchema([{key,label,type,enum?,def}],type ∈ text|number|boolean|enum,旧项缺省空数组);字段值落 strategy_holdings 新增 values TEXT(JSON 键值对,key 对齐 configSchema.key,允许额外键=可扩展,NULL=未配置);补列用幂等 ALTER(沿用 _ensureTradeAttributionColumns 模式,只读连接容忍);API:strategy-positions 每行附 values,新增 holdings/values-update {holdingId, values} 写回,服务端按 configSchema 校验(number=有限数、enum=在选项内、boolean=布尔),空值/缺省可写入;持仓生命周期操作(openHolding/addShares/reduceShares/closeHolding)不碰 values 列 | 2026-09-02 | 生效 | - | 新增(2026-09-02 R-013 定稿 + PLAN-012):定义 settings + 值 SQLite 列 + 幂等补列 + 类型校验 |
|
||||
| 技术约束-014 | 会话 tab 注册与顺序显隐(R-011):客户端注册统一读 **settings.tabs 有序数组**(唯一顺序与显隐来源,内置条目 refKey + 策略条目 refId),按 order 排序、过滤 visible 后注册(builtin 走内置 render、strategy 走 StrategyTab),移除硬编码 order 间隔(原内置 10/11/12、策略 13+);settings.tabs 从布尔对象升级为有序数组,读取时对旧格式(布尔对象 + 策略自带 order/visible)静默归一化迁移(旧隐藏策略迁移后显示),写入即落库;策略定义表收窄为 {id,name}(去除 visible/order);tabs/update 语义改为整表更新(顺序 + 显隐),strategies/add 联动追加 tab 条目(末尾),strategies/remove 联动删除对应 tab 条目,废弃 strategies/move | 2026-09-02 | 生效 | - | R-011 定稿(2026-09-02 Q1-Q5 确认):统一 tabs 有序数组 + 自动迁移 + 联动增删 |
|
||||
| 技术约束-016 | src 目录按功能域归类(2026-09-02 结构优化):服务端代码**禁止平铺**,按职责域分目录 —— src/data-source/(QmtBridgeRestDataSource + data-source-types + QmtHealthMonitor,数据源与连接健康)、src/storage/(SqliteStore + DataStore,存储层)、src/position/(PositionManager,分仓逻辑)、src/market/(MarketDataHub + MarketFeed,行情)、src/trades/(TradeSync,交易同步);api/ 按领域拆分子文件(positions/strategies/qmt-connections/market/trades),client/ 仅放 UI(views/ 组件 + market/ provider);文件命名 = 类名(PascalCase)+ .js/.jsx;新增服务端模块必须先落对应域目录,无合适域时先讨论补域,不得回退平铺 | 2026-09-02 | 生效 | - | 新增(2026-09-02 结构审查 + 优化落地):component/ 平铺还原为语义分域,删除死代码 AllocationStorage、DataStore.setDataset/removeDataset |
|
||||
| 技术约束-017 | 持仓内存快照(R-014,2026-09-02):全量实盘持仓由服务端 PositionSync **进程内内存快照**管理(启动预热 + 10s 定时全量拉 /trade/positions → 校验 → 整体替换),**不落库**(无缓存表、不复用 strategy_holdings——账本与对账单分离,holding_id 交易锚点不掺易变快照;判断标准:外部可一次调用重取全的实时投影不落库);PositionManager.getAllPositions 以快照为准(strategy-positions / unallocated / summary 三接口不再请求时穿透 QMT),快照为空读穿透兜底(当场拉一次并回填);同步失败保留上次快照(不清空不报错);空快照双重确认(/health 可用 + getAsset 账户身份可识别)才接受为真清仓;幽灵清仓:快照连续 3 轮(约 30s)消失的 code 该码全部策略当前持仓自动 closeHolding 转历史(不物理删除),账户身份守卫防误清(accountId 未知当轮跳过、切换当轮重置跳过);syncNow 允许手动调用(mounted 只管定时循环,不拦手动同步) | 2026-09-02 | 生效 | - | 新增(2026-09-02 R-014 定稿 + 迭代 12 实施):AI 初版建缓存表方案经老师质疑反转为内存方案(落库三问:不可再生?重启首屏依赖?读放大/复杂查询?全否 → 内存) |
|
||||
| 技术约束-018 | 盘口内存快照(R-015,2026-09-02):行情数据由 QuoteSync(取数:启动 prime + 5s REST 定时刷 watch 集合 + 涨停跌停经 /data/instrument 按**交易日**内存缓存)与 QuoteHub(存查:内存快照 Map + watchCodes Set + 读穿透走 dataSource.getTicks + getQuote/getByCodes 对外)管理,**替换并删除 MarketFeed/MarketDataHub**(方案 A,不留兼容壳);**WS 数据通路移除**(ingest 入口带 source 标签留回归口子);**market_quotes_cache 表退役 DROP**(幂等),价格单一入口 = QuoteHub,DataStore 行情方法(loadMarket/getMarketQuote(s)/setMarketQuotes)删除;同步失败保留内存旧值;watchCodes 维持只进不出无上限;涨停/跌停/昨收不落库 | 2026-09-02 | 生效 | - | 新增(2026-09-02 R-015 定稿 + 迭代 13 实施):老师五拍板(替换/WS 移除/纯内存 DROP/watch 现状/指示灯);warmup bug 复现(loadMarket return this 残迹)为不落库关键证据 |
|
||||
<!-- 示例条目(确认格式后删除):
|
||||
| 技术约束-001 | 示例:技术栈以 Node.js / TypeScript 为准,不引入未讨论的新框架 | 2026-08-26 | 生效 | - | 讨论确认:优先复用 DSH 既有能力,新框架需论证 |
|
||||
-->
|
||||
@@ -0,0 +1,106 @@
|
||||
# 技术实现方案:13-盘口内存快照
|
||||
|
||||
> 迭代编号:13 | 依据:PLAN-014 + R-015(老师拍板:方案 A 替换 / WS 移除 / 纯内存 DROP / watch 维持现状 / 指示灯推荐逻辑)
|
||||
|
||||
## 1. 数据源扩展(QmtBridgeRestDataSource.getTicks)
|
||||
|
||||
```js
|
||||
/** 批量盘口(GET /data/tick?codes=a,b,c)→ { [code]: snapshot }(保留原始字段 + updatedAt) */
|
||||
async getTicks(codes) {
|
||||
// j.ok && j.data → data 原样返回(tick 结构已有语义字段 lastPrice/lastClose/open/high/low/... + time/timetag)
|
||||
// 每 snapshot 统一附加 updatedAt = Date.now()(服务端收到时刻,价龄判断基准)
|
||||
}
|
||||
```
|
||||
|
||||
- 首次启用 /data/tick 的适配层通道(此前 MarketDataHub 自己 fetch,绕过了适配层——本轮修正层级);
|
||||
- 涨停跌停走既有 getInstrument(code)(首次投入使用):upStopPrice/downStopPrice/preClose。
|
||||
|
||||
## 2. QuoteHub(src/market/QuoteHub.js,新)
|
||||
|
||||
```js
|
||||
class QuoteHub {
|
||||
quotes = new Map(); // code → snapshot(全量 tick 字段 + updatedAt;只读约定)
|
||||
instruments = new Map(); // code → { upStopPrice, downStopPrice, preClose, tradingDay }(交易日内存缓存)
|
||||
watchCodes = new Set(); // 关注集合(维持现状只进不出,无上限——老师拍板)
|
||||
stats = { syncCount, failCount, lastError, lastSyncedAt, restCount, watchSize };
|
||||
}
|
||||
```
|
||||
|
||||
- `getQuotes(codes)`:内存命中 + miss 读穿透(dataSource.getTicks 补拉并回填,等价旧三级命中的内存→REST 两级,磁盘层删除);
|
||||
- `getQuote(code)`:单码便捷;`watch(codes)`:关注集合登记(QuoteSync prime/刷新、api 查询共用);
|
||||
- `ingest(data, { source })`:写入统一入口(source 标签:rest|instrument——WS 将来回归时加 source 即可,入口不变);
|
||||
- 合约信息:`getInstrumentCached(code, tradingDay)`——QuoteSync 判定交易日变化后失效重拉(涨停跌停当天不变)。
|
||||
|
||||
## 3. QuoteSync(src/market/QuoteSync.js,新;与 PositionSync 同构)
|
||||
|
||||
```
|
||||
start(): prime(持仓 code 盘口,走 hub.watch + getTicks 批量)+ setInterval(5s)
|
||||
每轮:codes = [...hub.watchCodes](维持现状)
|
||||
分批 50 → dataSource.getTicks(batch) → hub.ingest(data, {source:'rest'})
|
||||
watch 中无合约缓存或 tradingDay 变化的 code → getInstrument 懒拉(失败不阻塞本轮)
|
||||
失败:stats.failCount++ / lastError;**内存不动(保留上次价)**
|
||||
setBaseUrl(url):热切换 → 清空 instruments 缓存(新账户/新市场环境)+ 立即 prime 一次
|
||||
stop(): 清定时器;内存保留
|
||||
getSyncedAt() / getStatus(): 指示灯数据源
|
||||
```
|
||||
|
||||
- 对称性:PositionSync(10s/持仓)/ QuoteSync(5s/盘口)/ TradeSync(60s/交易)三域同构,全部「失败保留、stats 可观测、手动 syncNow 可触发」。
|
||||
|
||||
## 4. 存储清理(market_quotes_cache 退役,老师拍板 DROP)
|
||||
|
||||
- SqliteStore:SCHEMA_SQL 删建表;新增 `DROP TABLE IF EXISTS market_quotes_cache`(init 内幂等执行,存量库清理);删 getMarketQuote/getMarketQuotes/setMarketQuotes/_mapQuote;migrateJson 删行情迁移段;isEmpty 只看 strategy_holdings;
|
||||
- DataStore:删 loadMarket(return this 残迹,warmup bug 根源)/getMarketQuote/getMarketQuotes/setMarketQuotes/marketLoaded;
|
||||
- 存量 store.market.json 的 .bak 不动(历史备份无碍)。
|
||||
|
||||
## 5. API(src/api/market.js + 新 sync-status)
|
||||
|
||||
```
|
||||
market-snapshot { codes } → { [code]: { lastPrice, lastClose, upStopPrice, downStopPrice, updatedAt, ...tick 字段 } }
|
||||
sync-status → {
|
||||
position: { syncedAt, ageMs, fresh: bool, snapshotSize, failCount, lastError, periodMs },
|
||||
quote: { syncedAt, ageMs, fresh: bool, snapshotSize, watchSize, failCount, lastError, periodMs },
|
||||
qmt: { ...qmtHealthMonitor.getStatus() }
|
||||
}
|
||||
sync-now { domain: 'position' | 'quote' } → 各 Sync.syncNow()(手动触发,指示灯点击用)
|
||||
market-stats 端点删除(并入 sync-status)
|
||||
```
|
||||
|
||||
- 状态判定:fresh = syncedAt 在 3×周期内(持仓 30s / 盘口 15s);前端按 ageMs 二次校准显示。
|
||||
|
||||
## 6. 前端(SyncIndicators.jsx 新 + 两处清理)
|
||||
|
||||
- **SyncIndicators**(挂 QmtConnectionChip 内 QmtHealthIndicator 右侧):两圆点「持」「行」;
|
||||
- 10s 轮询 sync-status;点击圆点 → sync-now({domain}) → 刷新;
|
||||
- 颜色:green(fresh) / yellow(stale) / gray(never);title 含同步时间、快照量、失败数、lastError;
|
||||
- 样式复用 QmtHealthIndicator 圆点(10px 圆 + glow),色值走 --dsw-* token;
|
||||
- MarketDataProvider:删 `wsInfo: null` 残留;轮询/注册逻辑不变(getByCodes 签名兼容);
|
||||
- QmtConnectionChip:挂载 SyncIndicators(健康灯右侧)。
|
||||
|
||||
## 7. 装配(src/index.js)
|
||||
|
||||
```js
|
||||
const marketHub = new QuoteHub({ logger });
|
||||
const marketFeed = new QuoteSync({ hub: marketHub, runtime: { settings, dataSource }, logger });
|
||||
marketFeed.start(startup.baseUrl ?? config?.qmtBaseUrl);
|
||||
// dispose:marketFeed.stop()(保留旧变量名,改动最小;注释标注新职责)
|
||||
```
|
||||
|
||||
- registerApi 入参 marketHub/marketFeed 对象形态不变(api/market.js 内部改用新方法);
|
||||
- QmtHealthMonitor 不动。
|
||||
|
||||
## 8. 回归脚本(scripts/test-quote-sync.mjs,纯内存 mock)
|
||||
|
||||
- mock:可编程 dataSource(ticks/instrument/positions 可变状态);
|
||||
- 用例:prime + 5s 刷新基本流 / 失败保留上次价 / watch 空不空转 / 读穿透回填 / 涨停跌停按交易日缓存与日切失效 / 热切换清合约缓存 / sync-status 状态推导(绿黄灰边界)/ 存储清理(DROP 幂等:SqliteStore 临时目录建旧结构库 → init 后表消失且持仓数据无损)。
|
||||
|
||||
## 9. 策略持仓表行情列(老师确认并入本轮)
|
||||
|
||||
- **settings.js**:COLUMN_META 追加 4 项(均 `defaultVisible: false`)——`{ key:'upStopPrice', label:'涨停价' } / { key:'downStopPrice', label:'跌停价' } / { key:'open', label:'今开' } / { key:'high', label:'最高' }`;normalizeStrategyColumns 两处适配:默认列构造带 defaultVisible,未覆盖列的缺省显隐从恒 true 改为 `defaultVisible !== false`(存量 strategyColumns 无需迁移——新列缺省即隐藏);
|
||||
- **StrategyTab.renderDataCell** switch 增 4 case:`getPrice(p.code)?.upStopPrice / downStopPrice / open / high`,复用 fmtPrice 纯价格展示(数据缺失显示 —,与现价列同行为);
|
||||
- 列设置弹层零改动(自动从 COLUMN_META 出现在列表,kind=base 标「数据」);
|
||||
- 取值来源:market-snapshot 下发的 tick 字段 + 合约信息(upStopPrice/downStopPrice 来自 QuoteSync 交易日缓存),经 MarketDataProvider prices map join 到行。
|
||||
|
||||
## 10. 验证
|
||||
|
||||
- typecheck + build;test-quote-sync 全绿;存量回归(test-position-sync 34 项 + test-r013 21 项)全绿;
|
||||
- 老师人工验收(见验收标准)。
|
||||
@@ -0,0 +1,41 @@
|
||||
# 迭代复盘:13-盘口内存快照(QuoteSync/QuoteHub + 同步指示灯)
|
||||
|
||||
> 复盘日期:2026-09-02 | 迭代状态:**已实施,待老师人工验收**
|
||||
> 关联需求:R-015 | 关联计划:PLAN-014
|
||||
|
||||
## 结果
|
||||
|
||||
迭代 13 达成:盘口数据与持仓对称收敛——QuoteSync(取数:prime + 5s REST + 涨停跌停按交易日缓存)/ QuoteHub(存查:内存快照 + watch 集合 + 读穿透 + getQuote 单一入口)替换 MarketFeed/MarketDataHub(方案 A 删除重写);WS 通路移除;market_quotes_cache 表 DROP 退役(含 loadMarket 残迹清理,warmup bug 随之消灭);新增涨停/跌停字段(/data/instrument 首次启用);会话头部新增持仓/盘口同步指示灯(绿黄灰 + 点击即同步 + sync-status 端点吸收 market-stats)。
|
||||
|
||||
## 过程事实
|
||||
|
||||
1. **讨论驱动逐项拍板**(2026-09-02,五项):① 替换 vs 包壳 → 方案 A 替换(DataStore.loadMarket 残迹为前车之鉴);② WS 移除(无生效结论 + 全市场推送被过滤 + REST 5s 已覆盖;ingest 留来源标签口子);③ **纯内存不落库**(老师纠正 AI 的「表加两列」方案:缓存表整个退役,价格单一入口 = QuoteHub;AI 复以落库三问复核通过——warmup bug 复现证明落库从未真正生效);④ watchCodes 维持现状不收缩不加限(WS 移除后膨胀仅轻微浪费,切回旧 tab 价格秒显是免费福利);⑤ 指示灯(老师提出):绿黄灰三态不引入红(红留 QMT 健康灯表达连接层)、点击即 syncNow、**PriceCell 灰点方案取消**(管道健康全局灯表达,个股停牌属数据语义另议);
|
||||
2. **warmup bug 实锤**:讨论中复现 DataStore.loadMarket() 返回 DataStore 实例自身 → Object.entries 枚举出 sqlite/loaded/marketLoaded 三个内部属性 → 「重启首屏有价」自上线起从未生效,一直靠 getByCodes 磁盘兜底路径撑着——成为「不落库」决策的关键证据;
|
||||
3. **实现**:getTicks 适配层通道(修正旧 hub 绕过适配层自 fetch 的层级破洞)、QuoteSync/QuoteHub、SqliteStore DROP + 行情方法删除、DataStore 行情方法删除、market-snapshot 扩展字段、sync-status/sync-now 端点、SyncIndicators 组件、wsInfo 清理;
|
||||
4. **验证**:typecheck + build;test-quote-sync 全绿;存量回归 test-position-sync 34/34、test-r013 21/21;
|
||||
5. **文档链**:R-015 + PLAN-014 + 迭代三件套 + 技术约束-018 + 产品约束-012 + 技术约束-010/012 变更(本轮讨论后统一落)。
|
||||
|
||||
## 经验教训(复盘沉淀)
|
||||
|
||||
### 1. 「缓存落库」的隐性成本会以 bug 形式讨债
|
||||
- market_quotes_cache 三件套(loadMarket 残迹 / warmup 假工作 / isEmpty 与 migrateJson 的行情耦合)活了三代迭代才被 root cause——落库换来的「重启首屏有价」实际从未工作,而读穿透 1 秒内就能补齐;
|
||||
- 沉淀:**给「可再生缓存」落库前,先验证它的读取路径真的被走到**(监控 stats.diskHitCount 之类),否则就是无人受益的死重 + 未来 bug 的温床。
|
||||
|
||||
### 2. 同域两个类(Feed/Hub)的「分工」挡不住职责漂移
|
||||
- MarketDataHub 本该只管存查,却自己 fetch REST(绕适配层)、管 watch 过滤、管落库防抖——「取」与「存」的边界在实践中糊掉;
|
||||
- 沉淀:类职责用「它消费谁、被谁消费」检查:QuoteHub 只消费 QuoteSync/dataSource 补拉,只被 api/前端消费;出现第三条边即重构信号。
|
||||
|
||||
### 3. 全局状态灯 > 局部灰点
|
||||
- 老师把 PriceCell 灰点讨论升级为「同步指示灯」,一并覆盖持仓+盘口两域——数据管道健康是全局属性,表达在全局位置(会话头部)比逐格标记更正确,且复用既有健康灯心智;
|
||||
- 沉淀:故障可视化先问「这是单个数据的错,还是管道的错」——后者永远放全局。
|
||||
|
||||
### 4. 交易日是行情静态数据的正确缓存键
|
||||
- 涨停/跌停/昨收「当天不变」的知识决定缓存粒度:按交易日缓存 + 日切失效,而不是跟 5s 同步周期走(省 99% 的合约请求);
|
||||
- 沉淀:为同步数据分类「每周期变(tick)/每日变(合约静态)/每次变(手动归属)」,各自匹配刷新节奏。
|
||||
|
||||
## 遗留/后续
|
||||
|
||||
1. **个股停牌标识**:QMT 正常但单票不出 tick 时价格陈旧且无提示——属数据语义,老师确认另议;
|
||||
2. **WS 回归路径**:ingest 已留 source 标签;T-005 草稿维持起草,需要亚秒级行情时再议订阅契约;
|
||||
3. **指示灯扩展位**:sync-status 已按域结构化(position/quote/qmt),未来交易域(TradeSync 60s)可加第三盏灯,前端加一行渲染;
|
||||
4. **前端 getQuote 消费扩展**:涨停/跌停已随 market-snapshot 下发,UI 尚无展示列(如需「接近涨停提示」另立需求)。
|
||||
@@ -0,0 +1,29 @@
|
||||
# 迭代目标:13-盘口内存快照(QuoteSync/QuoteHub + 同步指示灯)
|
||||
|
||||
> 迭代编号:13 | 创建:2026-09-02 | 状态:已实施,待验收
|
||||
> 依据计划:PLAN-014 | 需求:R-015(已定稿,2026-09-02)
|
||||
|
||||
## 目标描述
|
||||
|
||||
把盘口(市场行情)数据收敛为与持仓(迭代 12)对称的「定时同步 + 内存快照」模式:新建 QuoteSync(取数)与 QuoteHub(存查)两个工具类,`market_quotes_cache` 表退役(DROP),价格单一入口 = QuoteHub;扩展涨停价/跌停价(/data/instrument 按交易日缓存);会话头部新增「持仓/盘口」同步指示灯(绿黄灰,点击即同步)。WS 数据通路本轮移除(从无生效结论,老师拍板)。
|
||||
|
||||
## 目标分解
|
||||
|
||||
1. **QuoteSync**(src/market/QuoteSync.js):启动 prime(持仓盘口)+ 5s 定时刷 watch 集合(分批 50)+ 涨停跌停按交易日懒拉缓存(watch 新 code → /data/instrument,日切失效)+ 热切换重置 + stats;失败保留内存不动;WS 不迁移;
|
||||
2. **QuoteHub**(src/market/QuoteHub.js):内存快照 Map + watchCodes Set(维持现状只进不出)+ 合约信息缓存 + 读穿透(走 dataSource.getTicks,不再自 fetch)+ 防抖 3s 写盘逻辑删除(无落库)+ 对外 getQuote/getQuotes/getByCodes/watch/stats;
|
||||
3. **数据源**:QmtBridgeRestDataSource 新增 getTicks(codes)(/data/tick 批量语义化,带 updatedAt/time 原始时间);
|
||||
4. **存储清理**:SqliteStore 删行情表方法 + `DROP TABLE IF EXISTS market_quotes_cache`(幂等)+ migrateJson 行情段/isEmpty 联动收缩;DataStore 删 loadMarket/getMarketQuote(s)/setMarketQuotes;
|
||||
5. **API**:market-snapshot 值对象扩展 {lastPrice,lastClose,upStopPrice,downStopPrice,updatedAt};新增 sync-status(持仓/盘口/QMT 三域状态,吸收 market-stats)+ sync-now {domain: position|quote};
|
||||
6. **前端**:SyncIndicators 组件(QmtHealthIndicator 旁两圆点:持●行●,10s 轮询 sync-status,绿黄灰 + 悬停详情 + 点击调 sync-now);MarketDataProvider 删 wsInfo 残留;QmtConnectionChip 挂载;
|
||||
7. **回归**:scripts/test-quote-sync.mjs(纯内存 mock)+ typecheck + build + 存量回归;
|
||||
8. **策略持仓表行情列**(老师确认并入):列设置新增 涨停价/跌停价/今开/最高 4 列(COLUMN_META 登记,kind=base,defaultVisible: false 默认隐藏);renderDataCell 取值 getPrice(code)?.{upStopPrice|downStopPrice|open|high};纯价格展示(无距离百分比);归一化缺省显隐跟随 defaultVisible;
|
||||
|
||||
## 指示灯规格(老师采纳 AI 推荐逻辑)
|
||||
|
||||
- 阈值:持仓 30s 内绿 / 超过黄、从未灰(周期 10s);盘口 15s 内绿 / 超过黄、从未灰(周期 5s)——**绿黄灰三态,不引入红**(红留给 QMT 健康灯表达连接层故障,同步灯表达数据层陈旧);
|
||||
- 悬停 title:状态 + 最近同步时间 + 快照量 + 失败次数 + lastError 摘要;
|
||||
- 点击:立即触发该域 syncNow(不等下个周期),灯自动刷新。
|
||||
|
||||
## 对老师的配合需求
|
||||
|
||||
- 人工验收(真实环境):重载插件 → 指示灯绿、悬停信息正确 → 停 QMT 后两灯渐黄且页面价格保持旧值 → 恢复 QMT ≤5s 回绿 → 点击指示灯立即同步 → 涨停/跌停列数据正确(对比券商软件)。
|
||||
@@ -0,0 +1,23 @@
|
||||
# 验收标准:13-盘口内存快照
|
||||
|
||||
> 迭代编号:13 | 依据:PLAN-014 验收要点 + R-015
|
||||
|
||||
## 验收标准线
|
||||
|
||||
1. **自动化**:typecheck + build 通过;test-quote-sync.mjs 全绿(基本流/失败保留/读穿透/交易日缓存/热切换/状态推导/DROP 幂等);存量回归 test-position-sync 34/34、test-r013 21/21;
|
||||
2. **存储退役**:插件启动后存量库中 market_quotes_cache 表被 DROP(幂等,重启不报错);持仓数据不受影响;代码中无 DataStore.loadMarket / getMarketQuote(s) / setMarketQuotes 残留;MarketFeed/MarketDataHub 文件已删除;
|
||||
3. **价格功能**:重载插件后持仓/策略 tab 现价、涨幅正常(≤5s 更新);**涨停/跌停数据**在 market-snapshot 返回中正确(与券商软件对照);QMT 挂掉时页面价格保持旧值(不空白),恢复后 ≤5s 自动追上;
|
||||
3b. **行情列**:策略持仓 tab 列设置出现 涨停价/跌停价/今开/最高 4 个新列(默认隐藏,勾选后显示且顺序可调),纯价格展示,无数据的票显示 —;
|
||||
4. **指示灯**:会话头部出现「持」「行」两圆点——正常绿(悬停:同步时间/快照量/失败数);停 QMT 后持仓灯 30s 内、盘口灯 15s 内变黄(数据仍显示旧值);恢复后回绿;**点击圆点立即触发对应域同步**;
|
||||
5. **WS 清理**:日志无「MarketFeed 连接 ws://」;market-stats 端点不再存在(sync-status 替代);前端 wsInfo 残留已删;
|
||||
6. **零 schema 破坏**:strategy_holdings / trade_orders / trade_fills 三表结构与数据完好。
|
||||
|
||||
## 验收方法
|
||||
|
||||
- 自动化项 AI 执行出具结果;
|
||||
- 2~5 老师真实环境人工验收:重载插件 → 查看指示灯与悬停信息 → 对照券商涨停跌停价 → 停/启 QMT Bridge 验证抗抖与回绿 → 点击指示灯验证即时同步;
|
||||
- 全部通过后:迭代 13 标记「验收通过」,R-015 归档 已完成/。
|
||||
|
||||
## 验收目标
|
||||
|
||||
- 6 条验收线通过,迭代 13 标记「验收通过」,R-015 更新实现状态(已实现)并归档。
|
||||
@@ -0,0 +1,28 @@
|
||||
# 需求:R-015 盘口数据内存化(QuoteSync/QuoteHub 统一管理)+ 数据同步指示灯
|
||||
|
||||
> 登记:2026-09-02 | 来源:架构演进讨论(老师逐项拍板)| 状态:**已定稿**
|
||||
> 归属:迭代 13 | 计划:PLAN-014
|
||||
|
||||
## 需求描述
|
||||
|
||||
持仓数据已完成「内存快照统一管理」(R-014/迭代 12)。本轮把**盘口(市场行情)数据**收敛为同一模式:
|
||||
|
||||
1. **QuoteSync + QuoteHub 工具类**(替换 MarketFeed/MarketDataHub,方案 A 替换不包壳):
|
||||
- QuoteSync 管取数:启动 prime(持仓盘口)+ 5s REST 定时刷新(watch 集合);
|
||||
- QuoteHub 管存查:内存快照 + watch 集合 + 读穿透兜底 + 对外 getQuote(s);
|
||||
2. **纯内存,market_quotes_cache 表退役**(老师拍板 DROP):行情随时可重取、warmup bug 证明落库从未生效、重启 prime 1 秒内有价——三问全否不落库;价格单一入口 = QuoteHub;
|
||||
3. **WS 通路去掉**(老师拍板):从无生效结论(market-stats 诊断无结论文档)、全市场推送被 watch 过滤成本高、REST 5s 已覆盖;ingest 入口来源无关留再接入口子;
|
||||
4. **字段扩展**:新增涨停价/跌停价(来源 /data/instrument,按交易日内存缓存,不落库);昨收 tick 自带;getInstrument 首次投入使用;
|
||||
5. **数据同步指示灯**(老师提出):会话头部 QMT 健康灯旁加「持仓/盘口」两个同步状态圆点——绿(正常)/黄(同步失败中、快照陈旧)/灰(从未同步),悬停详情(同步时间/快照量/错误),**点击 = 立即触发该域 syncNow**;新增 sync-status 端点(吸收临时诊断 market-stats);
|
||||
6. **策略持仓表行情列扩展**(老师确认并入):列设置新增 涨停价/跌停价/今开/最高 4 个基础列(纯价格展示,默认隐藏,列设置勾选开启);最低/成交量/成交额暂不加(单位口径另议);距涨停百分比类衍生展示另立需求;
|
||||
6. **同步失败保留上次价**(AI 定,与持仓对称);**前端不加价龄灰点**(老师采纳预判:管道健康由全局灯表达,个股停牌属数据语义另议)。
|
||||
|
||||
## 边界
|
||||
|
||||
**做**:上述 1-6;watchCodes 维持现状(只进不出、无上限——去掉 WS 后膨胀仅轻微浪费,收缩不值成本)。
|
||||
|
||||
**不做**:不落库任何行情字段(涨停跌停也不落);不做 watch 收缩/降频;不做个股停牌标识;WS 不删除需求草稿(T-005 维持起草);PriceCell 不改动;最低/成交量/成交额列不做;距离涨停百分比衍生展示不做。
|
||||
|
||||
## 验收
|
||||
|
||||
见 `docs/04-迭代记录/13-盘口内存快照/验收标准.md`。
|
||||
Reference in New Issue
Block a user