迭代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:
2026-09-03 13:26:25 +08:00
parent 245fcad95c
commit d793d2cf35
26 changed files with 1075 additions and 589 deletions
+4 -2
View File
@@ -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 addbundle 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**:存储引擎为 **SQLitenode:sqlite**——strategy_holdings + market_quotes_cache + **trade_orders + trade_fills(交易记录两表,R-009**;存储层单票生命周期操作;交易记录两表零冗余 strategy_id/holding_id(外键链推导策略归属);策略定义仍存 DSH settings;旧 JSONstore.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**:存储引擎为 **SQLitenode:sqlite**——strategy_holdings + ~~market_quotes_cache~~**R-015 退役 DROP2026-09-02**;行情改内存快照)+ **trade_orders + trade_fills(交易记录两表,R-009**;存储层单票生命周期操作;交易记录两表零冗余 strategy_id/holding_id(外键链推导策略归属);策略定义仍存 DSH settings;旧 JSONstore.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 交易记录两表**变更 32026-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**变更 22026-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 归一化;委托交易日 = insertDatetradeDate 兜底) | 2026-09-01 | 生效 | - | 新增(2026-09-01 R-009 定稿 + 迭代 07 实施):两表 + 定时同步 + 本地历史查询;变更 12026-09-01):+code 归一化 + tradeDate 兜底;变更 22026-09-01 老师二次定稿):归属改**手动设置**trade_orders 冗余 strategy_id+holding_idUPSERT 不覆盖归属列),弃算法推导 |
| 技术约束-015 | 策略自定义字段存储(R-013,2026-09-02):字段定义随策略定义存 settings.strategies 扩展 configSchema[{key,label,type,enum?,def}]type ∈ text|number|boolean|enum,旧项缺省空数组);字段值落 strategy_holdings 新增 values TEXTJSON 键值对,key 对齐 configSchema.key,允许额外键=可扩展,NULL=未配置);补列用幂等 ALTER(沿用 _ensureTradeAttributionColumns 模式,只读连接容忍);APIstrategy-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/ 仅放 UIviews/ 组件 + market/ provider);文件命名 = 类名(PascalCase+ .js/.jsx;新增服务端模块必须先落对应域目录,无合适域时先讨论补域,不得回退平铺 | 2026-09-02 | 生效 | - | 新增(2026-09-02 结构审查 + 优化落地):component/ 平铺还原为语义分域,删除死代码 AllocationStorage、DataStore.setDataset/removeDataset |
| 技术约束-017 | 持仓内存快照(R-0142026-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-0152026-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**(幂等),价格单一入口 = QuoteHubDataStore 行情方法(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 既有能力,新框架需论证 |
-->