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

14 KiB
Raw Blame History

技术方案约束(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_cacheR-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/迭代 13market_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.jsonR-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 残迹)为不落库关键证据 |