docs(迭代07/08): 交易记录本地存储 + 策略持仓展开全量文档 + 需求归档(R-009/R-010)

- PLAN-008/PLAN-009 计划(已完成)
- 迭代 07/08 四件套(目标/技术方案/验收标准/复盘)
- 设计约束:数据存储设计 §10/§10.1、技术约束-012/013、产品约束-008
- R-009/R-010 需求归档(含二次定稿:手动归属、Q4 成本价/成交价)
- 需求池索引同步
This commit is contained in:
2026-09-02 01:21:10 +08:00
parent 859938c4f1
commit 2aacd8a25d
16 changed files with 821 additions and 6 deletions
@@ -0,0 +1,61 @@
# 迭代复盘:07-交易记录本地存储(SQLite)+ 策略关联
> 复盘日期:2026-09-01 | 迭代状态:**已完成(验收通过)**
> 关联需求:R-009(交易记录本地存储 + 策略关联,已定稿)
> 关联计划:PLAN-008(计划-交易记录本地存储SQLite与策略关联)
## 结果
迭代 07 达成:QMT 当日交易数据(委托 + 成交)本地持久化到 SQLitetrade_orders + trade_fills 两表,跨日积累成历史库),按外键链(成交→委托→策略→holding)与策略体系关联,支持按策略过滤 / 复盘交易;交易记录 tab 历史范围从「接口开发中」占位切换为本地历史查询。
## 过程事实
1. **需求定稿(R-009**:老师提出 → AI 登记(讨论中)提出 Q1-Q8 → 老师修正关联方式(外键链 + 两表零冗余)→ 确认定稿进入迭代 07;
2. **表结构**trade_orders(委托主行,order_id 主键 + insert_ts 派生时间列)+ trade_fills(成交明细,trade_id 主键、order_id 关联委托)两表;**零冗余 strategy_id/holding_id**Q3 老师定稿);
3. **策略归属推导(FK 链)**:查询期委托时间(insert_tsjoin strategy_holdings 生命周期窗口(created_at ≤ t < closed_at)推导;一码多策略取份额最大;未命中=未关联;attachStrategyAttribution 供今日实时委托附加归属;
4. **TradeSync 同步**:启动预热 + 60s 定时拉当日 orders+trades → UPSERT 落库(幂等,状态覆盖更新);前端今日轮询命中 orders 端点时写穿(机会式);
5. **本地历史查询**api/trades.js 新增 trades/history 端点(时间段/code/策略/方向过滤,策略过滤走 FK 链 join);
6. **前端**TradeRecordsTab 加策略过滤下拉(全部/各策略/未关联)+ 历史范围查本地库(不再占位);
7. **验证**: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 补 tradeDateinsertDate 兜底)+ mapTrade code 归一化;
- **沉淀**:QMT 各接口的证券代码格式不一致(委托/成交无后缀、持仓带后缀),语义化映射层必须统一归一化;交易「日」概念在委托接口 = insertDate(无独立 tradeDate 字段)。
### 5. 迁移持仓 created_at = 迁移时间戳 → FK 链窗口失真(真实数据发现 + 方案 A 修正)
- **发现**:迁移自 JSON 的持仓 created_at 全部是迁移时刻时间戳(2026-09-01 17:36),晚于当日真实交易时间 → 当日委托 join 窗口不匹配 → 全部判「未关联」;
- **决策(方案 A2026-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 语义要区分「系统字段」与「用户字段」。
## 遗留/后续
1. **QMT Bridge 历史接口**:老师完善后,今日实时可扩展为历史接口查询(本地库仍为兜底/积累);
2. **手动修正策略关联入口**:本期未做(自动推导 + 未关联兜底已满足复盘),后续按需;
3. **导出/清理**:本地历史库持续积累,导出/清理管理界面后续迭代;
4. **盘中行情写 SQLite 验证**(迭代 06 遗留):开盘后确认行情防抖写回 store.dbupdated_at / mtime)。