Files
one_divine_lot/docs/04-迭代记录/13-盘口内存快照/技术实现方案.md
T
kyugao d793d2cf35 迭代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现状/指示灯)
2026-09-03 13:26:25 +08:00

107 lines
6.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 技术实现方案: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. QuoteHubsrc/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. QuoteSyncsrc/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(): 指示灯数据源
```
- 对称性:PositionSync10s/持仓)/ QuoteSync5s/盘口)/ TradeSync(60s/交易)三域同构,全部「失败保留、stats 可观测、手动 syncNow 可触发」。
## 4. 存储清理(market_quotes_cache 退役,老师拍板 DROP
- SqliteStoreSCHEMA_SQL 删建表;新增 `DROP TABLE IF EXISTS market_quotes_cache`(init 内幂等执行,存量库清理);删 getMarketQuote/getMarketQuotes/setMarketQuotes/_mapQuotemigrateJson 删行情迁移段;isEmpty 只看 strategy_holdings
- DataStore:删 loadMarketreturn this 残迹,warmup bug 根源)/getMarketQuote/getMarketQuotes/setMarketQuotes/marketLoaded
- 存量 store.market.json 的 .bak 不动(历史备份无碍)。
## 5. APIsrc/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);
// disposemarketFeed.stop()(保留旧变量名,改动最小;注释标注新职责)
```
- registerApi 入参 marketHub/marketFeed 对象形态不变(api/market.js 内部改用新方法);
- QmtHealthMonitor 不动。
## 8. 回归脚本(scripts/test-quote-sync.mjs,纯内存 mock
- mock:可编程 dataSourceticks/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 + buildtest-quote-sync 全绿;存量回归(test-position-sync 34 项 + test-r013 21 项)全绿;
- 老师人工验收(见验收标准)。