- R-027 静态化:策略恒为内置两项(不可增删改名,保留显隐/排序);设置页移除「策略分组」; 字段配置入口迁到策略 tab 内「列设置」旁(FieldConfigDialog + schema-update 单策略写入) - R-026 计算字段:自写公式引擎(中文变量、四则/括号/round·abs·min·max、缺值短路→—)+ 变量目录(行情/合约/持仓/自定义字段)+ strategy-positions 逐行现算(只读内存缓存,不落库、列只读) - R-028 判定型:比较运算 + and/or + inferResultKind + 结果类型一致性校验 + 判定列 ✓/— 渲染; 网格超市落地 可下空单=涨停价>基准值+网格大小、可下多单=跌停价<基准值-网格大小 - 变量选择改标签平铺(老师反馈);公式手册 docs/99-其他材料/计算字段公式说明.md - 约束同步:产品约束-002/003/004/009/013/014、技术约束-014/015/022/023/024、UI约束-002/003/005/008 - 回归:新增 test-r026/test-r027,更新 r011/r013/r017,17 个脚本全绿;typecheck/build 通过
22 KiB
技术方案约束(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:8610,OpenAPI 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 通道只能一个 interceptor(api-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 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 + |
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 老师改;R-015 二改,2026-09-02):DSH 服务端做「REST 轮询 + 行情内存缓存」,前端统一轮询 /odl/api/market-snapshot(不做前端直连,无跨域); |
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 → R-027 真相源迁移):字段定义存 settings.strategyFields({ [strategyId]: [{ key, label, type, enum?, def?, unit?, formula?, decimals? }] },type ∈ text|number|boolean|enum|formula);settings.strategies 收窄为内置身份表(仅兼容读取旧 configSchema:strategyFields 缺失时按内置 id 回填视图,首次保存即落新键,无迁移脚本、不动库数据);写路径唯一 = strategies/schema-update { strategyId, configSchema }(单策略整份覆盖,未知 strategyId 抛 strategy-not-found,校验见 技术约束-024);normalizeFields 归一化(类型白名单回退 text、decimals 夹取 0-4、formula 字段不带 def);字段值落 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 列 + 幂等补列 + 类型校验;变更(2026-09-10 R-027/R-026):定义真相源迁 strategyFields(strategies 只读兼容)、类型白名单加计算类型、写路径改单策略端点 |
| 技术约束-014 | 会话 tab 注册与顺序显隐(R-011 → R-027 静态化):客户端注册统一读 settings.tabs(唯一顺序与显隐来源,内置条目 refKey + 策略条目 refId),按 order 排序、过滤 visible 后注册(builtin 走内置 render、strategy 走 StrategyTab);R-027 后:tab 序列由内置常量派生(3 内置 tab + 2 内置策略 tab),normalizeTabs 只保留常量表内条目的 visible/order 偏好、丢弃未知条目(自建策略 tab / 已退役 tab)、补齐缺失条目;updateTabs 只接受内置 id(未知条目过滤),不再有联动增删(appendStrategyTab/removeStrategyTab 退役);旧布尔对象格式(迭代 02)继续兼容 | 2026-09-02 | 生效 | - | R-011 定稿(2026-09-02 Q1-Q5 确认):统一 tabs 有序数组 + 自动迁移 + 联动增删;变更(2026-09-10 R-027):策略静态化,tabs 由常量派生、联动增删与 strategies CRUD 一并退役 |
| 技术约束-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;变更 1:2026-09-08 R-018 数据域分界):全量实盘持仓由服务端 PositionSync 进程内内存快照管理(启动预热 + 10s 定时全量拉 /trade/positions → 校验 → 整体替换),不落库(账本与对账单分离,holding_id 交易锚点不掺易变快照);PositionManager.getAllPositions 以快照为准(strategy-positions / unallocated / summary 三接口不再请求时穿透 QMT),快照为空读穿透兜底;同步失败保留上次快照;空快照双重确认(/health 可用 + getAsset 账户身份可识别)才接受为真清仓;syncNow 允许手动调用。幽灵自动清仓(R-014 原条款:快照连续 3 轮消失 → 本地全部策略当前持仓 closeHolding 转历史)退役——同步机制只作用于对账单域,不再写 strategy_holdings 账本(R-018 老师拍板:幽灵清仓本就是一个同步机制,不可以让幽灵把爪子伸太长);账本行转历史唯一途径 = 卖出单关联减至 0 closeHolding(R-018);对账单域「码消失」仅表现为快照无此行 + 漏关联软提示(positions/orphan-hints 只读提示) | 2026-09-02 | 生效 | - | 新增(2026-09-02 R-014 定稿 + 迭代 12 实施);变更 1(2026-09-08 R-018/迭代 16 拍板):幽灵自动清仓退役(不再 closeHolding 本地账本),PositionSync 只同步对账单快照;漏关联由只读软提示承担 |
| 技术约束-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 残迹)为不落库关键证据 |
| 技术约束-019 | 历史持仓查询(R-016,2026-09-07):策略 tab 历史持仓展示走本地库只读查询(SqliteStore.getHoldingsHistory:strategy_id + closed_at IS NOT NULL + closed_at ≥ sinceMs,closed_at DESC),经独立端点 strategy-holdings/history 暴露;当前持仓路径(strategy-positions / PositionSync 快照语义)不掺历史数据(两份结果前端合并渲染);closeHolding 置 shares=0 语义维持不变(Q2 老师拍板:历史行份额显示 0,重点在追溯该持仓的历史操作而非清仓时份额);范围换算服务端做(week=7d/month=30d/quarter=90d/halfYear=182d/year=365d 自然日近似)——变更 1(2026-09-07 二轮补充 Q8-Q9 老师拍板):range 新增 'today' = 本地自然日 00:00 起(特判零点,不落回溯毫秒档),供前端「今日已清仓默认层」(恒显示、不受历史持仓开关控制);历史范围层语义收窄为今天之前;其余不变 | 2026-09-07 | 生效 | - | 变更 1(2026-09-07 R-016 二轮补充 Q8-Q9 老师拍板):+range=today 自然日边界,历史开关只控今天之前;首轮新增(Q1-Q7):历史行=追溯操作锚点,不动存储写路径 |
| 技术约束-020 | QMT 连接健康自适应探测(R-022,2026-09-09):QmtHealthMonitor 探测节奏由固定 5 分钟改为结果驱动自适应——健康 → intervalMs(默认 5 分钟,稳态低开销);异常/未知(含从未探测成功或 baseUrl 解析失败)→ retryMs(默认 10 秒)快速重试,与前端 sync-status 10s 轮询对齐,QMT Bridge 启动/恢复后指示灯 ≤20s 自动回绿(原固定 5 分钟导致恢复感知滞后,老师反馈「自动检查没生效」);间隔经构造参数 { intervalMs, retryMs } 注入(测试用短间隔);setTimeout 自适应链 + _inFlight 防并发,手动探测/配置热切换共用 probe() 并重排下轮节奏;stop() 即停;resolveActiveBaseUrl 失败也落 healthy:false 缓存走快速重试;前端读缓存秒回与 sync-status/qmt-health 端点语义不变 | 2026-09-09 | 生效 | - | 新增(2026-09-09 R-022 讨论 + 迭代 19 实施):健康灯恢复感知修复;稳态间隔维持原 5 分钟设计不变 |
| 技术约束-021 | MCP 状态自适应探测(R-023,2026-09-09):QmtMcpManager 状态缓存增加结果驱动自适应探测(原无定时回路,只在挂载/切换/手动探测时刷新)——connected → intervalMs(默认 5 分钟,稳态低开销);非 connected → retryMs(默认 10 秒)快速重试,MCP 服务(激活连接 baseUrl + /mcp)恢复后状态灯 ≤20s 自动回绿;dsh-mcp-client 自带 reconnect 只恢复真实连接、不刷新状态缓存,本类缓存必须自带刷新回路;无激活连接(url 为空)不探测不调度,dispose()/unmount() 停止调度;探测走 SDK 独立只读握手(Client connect + initialize + tools/list),与 dsh-mcp-client 自管连接互不干扰;间隔经构造参数 { intervalMs, retryMs } 注入(测试用短间隔);_probing 防并发与手动/热切换探测共用 probe() 重排节奏;前端 getStatus 读缓存与 mcp-status/sync-status 端点语义不变 | 2026-09-09 | 生效 | - | 新增(2026-09-09 R-023 讨论 + 迭代 19 实施):MCP 状态灯恢复感知修复(与 R-022 同模式);挂载/重连逻辑零改动 |
| 技术约束-022 | 公式引擎与变量目录(R-026,2026-09-10):src/formula/evaluator.js 自写小型表达式引擎(零依赖,禁止 eval/Function):词法支持 Unicode 标识符(中文变量名)与全角归一(()+-×÷),语法 = 四则 + 括号 + 一元负 + 函数 round/abs/min/max(递归下降 + 深度≤32 + 长度≤500 上限);R-028 扩展:比较 > < >= <= == != 与逻辑 and / or(含 && ||、全角 ≥ ≤ ≠ > < =),优先级 or < and < 比较 < 加减 < 乘除,判定结果缺值短路;新增 inferResultKind(ast) 静态推断结果类型(顶层比较/逻辑 → boolean,neg 下钻);validateFormula(expr, allowedVars) 返回 { ok, vars } 或 { ok:false, error:{ code: empty|syntax|unknown-var|unknown-func|arity, message, position } };求值语义 = 缺值短路(任一变量 null/undefined/NaN → 整式 null)、除零/非有限 → null、最终结果浮点归一(|v|<1e12 时四舍五入 10 位,消除 0.7000000000000002 类噪声,中间步骤不归一再保精度);src/formula/variables.js 为变量目录单一入口(行情 7 项 / 合约 2 项 / 持仓账本 3 项 + 运行时按策略非 formula 字段展示名展开「自定义字段」组),allowedVarNames 排除 formula 字段(零循环依赖) | 2026-09-10 | 生效 | - | 新增(2026-09-10 R-026 定稿 + 技术方案 §2.2/§2.3):中文标识符需自有词法(第三方库均不支持中文变量),该层即 T-006 数据池的表达式层;变更(2026-09-10 R-028):新增比较与逻辑运算 + inferResultKind(判定型计算字段);复核(2026-09-10 老师提问「有没有现成的库」):对比 expr-eval(CVE-2025-12735 变量对象注入 → RCE)、mathjs(标识符不含 CJK)、Jexl(ASCII 标识符)、CEL、解析器工具包后维持自写引擎,论证见 迭代 23 技术方案 §4.5 |
| 技术约束-023 | 计算字段现算口径(R-026,2026-09-10):src/formula/FormulaService.js#computeRows 在 API 层为 strategy-positions 返回行附加 computed: { [fieldKey]: number|null }(不改 PositionManager 份额语义);取数只读 QuoteHub 内存缓存(quotes / instruments,与 api/market.js#projectQuote 同口径),不做读穿透(高频读路径零网络;miss → 相关变量 null → 该格 —,等 QuoteSync 下轮 ≤5s 覆盖);无 formula 字段的策略直接返回原数组(零开销);值不落库(公式存 settings,值每次现算);strategy-holdings/history 不计算(历史行前端显示 —) | 2026-09-10 | 生效 | - | 新增(2026-09-10 R-026 技术方案 §2.4):计算位置与缓存口径由 AI 定并写入方案,老师验收覆盖缺数据边界 |
| 技术约束-024 | 字段定义 API 与校验(R-027/R-026,2026-09-10):写路径唯一 strategies/schema-update { strategyId, configSchema }(返回该策略对象);校验顺序 = 结构(展示名/key 非空、类型白名单、枚举选项非空)→ 唯一性(key 唯一、展示名唯一)→ 内置变量名冲突(字段展示名不得等于现价/份额/…等内置变量名)→ 公式(validateFormula,变量集合 = 内置名 ∪ 本策略非 formula 字段展示名);失败统一抛 code='field-validation'(前端 Toast + 公式框描红);formula/variables { strategyId? } 提供变量目录、formula/trial { strategyId, formula } 试算(取份额>0 首行,返回 { code, name, value, reason: no-holding|no-data|null });退役端点 strategies/add|remove|update(调用返回 not-found) | 2026-09-10 | 生效 | - | 新增(2026-09-10 R-027/R-026 定稿 + 技术方案 §1.2/§2.5):单策略写入避开整表覆盖,校验规则服务端唯一;变更(2026-09-10 R-028):公式字段增加「结果类型一致性」校验——resultKind(默认 number)必须等于 inferResultKind(compileFormula(formula).ast),不一致抛 field-validation(双向提示)|