2aacd8a25d
- PLAN-008/PLAN-009 计划(已完成) - 迭代 07/08 四件套(目标/技术方案/验收标准/复盘) - 设计约束:数据存储设计 §10/§10.1、技术约束-012/013、产品约束-008 - R-009/R-010 需求归档(含二次定稿:手动归属、Q4 成本价/成交价) - 需求池索引同步
6.5 KiB
6.5 KiB
迭代复盘:07-交易记录本地存储(SQLite)+ 策略关联
复盘日期:2026-09-01 | 迭代状态:已完成(验收通过) 关联需求:R-009(交易记录本地存储 + 策略关联,已定稿) 关联计划:PLAN-008(计划-交易记录本地存储SQLite与策略关联)
结果
迭代 07 达成:QMT 当日交易数据(委托 + 成交)本地持久化到 SQLite(trade_orders + trade_fills 两表,跨日积累成历史库),按外键链(成交→委托→策略→holding)与策略体系关联,支持按策略过滤 / 复盘交易;交易记录 tab 历史范围从「接口开发中」占位切换为本地历史查询。
过程事实
- 需求定稿(R-009):老师提出 → AI 登记(讨论中)提出 Q1-Q8 → 老师修正关联方式(外键链 + 两表零冗余)→ 确认定稿进入迭代 07;
- 表结构:trade_orders(委托主行,order_id 主键 + insert_ts 派生时间列)+ trade_fills(成交明细,trade_id 主键、order_id 关联委托)两表;零冗余 strategy_id/holding_id(Q3 老师定稿);
- 策略归属推导(FK 链):查询期委托时间(insert_ts)join strategy_holdings 生命周期窗口(created_at ≤ t < closed_at)推导;一码多策略取份额最大;未命中=未关联;attachStrategyAttribution 供今日实时委托附加归属;
- TradeSync 同步:启动预热 + 60s 定时拉当日 orders+trades → UPSERT 落库(幂等,状态覆盖更新);前端今日轮询命中 orders 端点时写穿(机会式);
- 本地历史查询:api/trades.js 新增 trades/history 端点(时间段/code/策略/方向过滤,策略过滤走 FK 链 join);
- 前端:TradeRecordsTab 加策略过滤下拉(全部/各策略/未关联)+ 历史范围查本地库(不再占位);
- 验证:typecheck 通过、构建成功、独立数据目录回归测试 17/17 通过 + TradeSync 集成测试 5/5 通过(幂等/归属推导/过滤/写穿)。
经验教训(复盘沉淀)
1. 时间基准必须一致(本次踩坑)
- 教训:insert_ts 合成先用 Date.UTC(),而 strategy_holdings.created_at 是本地 Date.now() —— 时区差导致持仓窗口匹配失败(回归测试 6 项失败);
- 沉淀:同一库内时间戳必须同一基准(本地时间);跨模块时间比较前先核对基准。
2. 测试数据要构造真实时间窗
- 第一次归属推导测试失败是因为用「当前时间」建仓(23:32)而委托是 09:30 —— 窗口本就不覆盖,代码是对的、测试数据不对;
- 沉淀:生命周期窗口类测试必须用 SQL 精确控制 created_at/closed_at,模拟真实时间关系。
3. 大改组件优先整体重写(前端)
- 迭代中对 TradeRecordsTab 做多次定点替换时部分替换未生效,产生中间态(引用未定义组件);
- 沉淀:结构性大改(新增过滤/分支重构)直接整文件重写更稳,避免局部替换残留。
4. QMT 委托/成交 code 无后缀 + 无独立交易日字段(真实数据发现)
- 发现:重启后启动预热同步的真实委托/成交,code 为无后缀
001330(持仓体系是001330.SZ),且委托无m_strTradeDate(交易日 =m_strInsertDate)—— 直接导致 FK 链 join 匹配不上(博纳影业被判未关联)+ 历史按时间段过滤漏委托; - 修复:QmtBridgeRestDataSource 加
normalizeInstrumentCode(6 位数字 + 交易所后缀;SH/SZ/BJ;无交易所按首位推断 6/9→SH、0/3→SZ;已带后缀保留)+ mapOrder 补 tradeDate(insertDate 兜底)+ mapTrade code 归一化; - 沉淀:QMT 各接口的证券代码格式不一致(委托/成交无后缀、持仓带后缀),语义化映射层必须统一归一化;交易「日」概念在委托接口 = insertDate(无独立 tradeDate 字段)。
5. 迁移持仓 created_at = 迁移时间戳 → FK 链窗口失真(真实数据发现 + 方案 A 修正)
- 发现:迁移自 JSON 的持仓 created_at 全部是迁移时刻时间戳(2026-09-01 17:36),晚于当日真实交易时间 → 当日委托 join 窗口不匹配 → 全部判「未关联」;
- 决策(方案 A,2026-09-01 老师确认):当前持仓(closed_at IS NULL)只做同 code 匹配(不要求 created_at ≤ 委托时间,现在持有=当日交易可归);已清仓(closed_at 非空)才按时间窗口(created_at ≤ t < closed_at)判断;
- 实现:_resolveStrategyAttribution 窗口条件分支;回归测试覆盖迁移时间戳场景(17/17 通过);
- 沉淀:迁移数据的 created_at 语义 = 迁移时间而非真实建仓时间,时间窗口类推导必须考虑该失真;当前持仓用「存在即归属」更贴合业务语义。
6. 归属判定改「手动设置」(R-009 二次定稿,老师拍板)
- 发现:算法推导(时间窗 + 份额最大)无法区分一票多策略(300057.SZ 同时分属 grid-supermarket 与 manual-t 各 1000 股,明天有成交不知道该归谁);
- 决策(老师二次定稿 Q1-Q4):归属由用户在交易记录 tab 手动设置(全手动选、可随时改、以最终为准);trade_orders 冗余存 strategy_id + holding_id;UPSERT 不覆盖归属列(手动指定为插件逻辑);
- 实现:trade_orders 加 strategy_id/holding_id 列(存量库 ALTER 迁移,插件启动时执行,只读打开容忍);setOrderAttribution + getAttributionCandidates(候选 = 该 code 当前持仓策略)+ api 两端点(orders/set-attribution、orders/attribution-candidates);前端 TradeRecordsTab 归属列下拉(候选含策略名 + 份额);
- 验证:37 项测试通过(核心 14 含 UPSERT 不覆盖归属/归属可改/候选列表/持久化过滤);
- 沉淀:算法只能给候选,归属是人的决定(符合「人机合一」目标-007);手工指定的数据不能被自动同步覆盖——UPSERT 语义要区分「系统字段」与「用户字段」。
遗留/后续
- QMT Bridge 历史接口:老师完善后,今日实时可扩展为历史接口查询(本地库仍为兜底/积累);
- 手动修正策略关联入口:本期未做(自动推导 + 未关联兜底已满足复盘),后续按需;
- 导出/清理:本地历史库持续积累,导出/清理管理界面后续迭代;
- 盘中行情写 SQLite 验证(迭代 06 遗留):开盘后确认行情防抖写回 store.db(updated_at / mtime)。