Files
one_divine_lot/docs/03-设计约束/技术方案约束.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

33 lines
14 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.
# 技术方案约束(Technical Constraints
> 技术方案层面的约束:技术栈、架构、实现规范、依赖原则。后续迭代进行技术实现时以此为依据。
>
> 记录格式:每条约束包含「编号 / 约束说明 / 添加日期 / 生效状态 / 失效日期 / 最后一次变更描述」。新增追加、变更更新、失效标记状态(不删除记录)。引用用编号,如 `技术约束-001`。
> **最后一次变更描述**:概括该约束最近一次变更(新增 / 变更 / 失效)讨论的最后一层结论;新增时写新增结论,变更/失效时更新为当次结论。
## 约束列表
| 编号 | 约束说明 | 添加日期 | 生效状态 | 失效日期 | 最后一次变更描述 |
|---|---|---|---|---|---|
| 技术约束-002 | 分仓管理采用「全量持仓 + 标签」模型:持仓数据为账户真实持仓(来自数据源适配器),标签为逻辑分组元数据(独立于持仓存储,可增删改),聚合视图按标签汇总仓位 | 2026-08-27 | 生效 | - | 讨论确认(R-002):全量持仓为基础,标签体系做逻辑分仓 |
| 技术约束-001 | 行情/交易数据源必须通过统一接口抽象(定义统一的数据模型与操作接口),实现层为具体数据源适配器;当前限定实现 QMT Bridge 适配器,未来可新增其他行情/交易接口适配器,业务层不感知具体数据源 | 2026-08-27 | 生效 | 2026-08-27 | 变更(2026-08-27 R-002 讨论):原为「QMT Bridge MCP 适配器」,修正为**直接调用 QMT Bridge RESTful 接口**http://192.168.3.43:8610),MCP 仅作为 Agent 侧封装层,插件侧直连 REST |
| 技术约束-003 | QMT Bridge 数据源通过其 RESTful HTTP 接口接入(基础地址 http://192.168.3.43:8610OpenAPI v3 规范),不经过 MCP 层;MCP 是给 Agent 用的封装,插件内部直连 REST | 2026-08-27 | 生效 | - | 讨论确认(R-002 第 7 轮):老师指出 MCP 是给智能体用的,插件应直连同一服务端口的 RESTful 接口;变更标注(2026-08-29 R-004 定稿):基础地址由固定单一地址改为多配置动态管理(由激活配置决定,见技术约束-008),直连 REST 原则不变 |
| 技术约束-004 | 插件服务端 API 用 webServer 自开路由(如 /odl/api/*),**不直接使用 connection.rpc.intercept('/api')** —— DSH 的 /api 通道只能一个 interceptorapi-gateway 已占用),重复 intercept 会抛错导致插件 apply 失败 | 2026-08-28 | 生效 | - | 迭代 01 复盘沉淀:RPC 冲突导致服务端插件 apply 失败、RPC 404 |
| 技术约束-005 | 客户端插件 bundle 必须是 CJS + window.__ModuleLoader__.load({id, factory}) 包装(tsdown 构建 + wrap 脚本),裸 ESM 无法被 DSH 客户端模块系统加载 | 2026-08-28 | 生效 | - | 迭代 01 复盘沉淀:客户端 bundle 未包装导致 loaded without registering via __ModuleLoader__.load |
| 技术约束-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~~**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 老师改;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 既有能力,新框架需论证 |
-->