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,71 @@
# 技术实现方案:08-策略持仓行展开关联交易记录(Holding → 交易汇总)
> 依据:PLAN-009 需求:R-010(Q1-Q3 定稿)| 设计约束:技术约束-012、技术约束-011(测试隔离)
## 技术选型
- 复用 R-009 的 holding_id 关联(trade_orders.holding_id);
- 服务端:strategy-positions 附加 holding_id + trades/by-holding 端点;
- 前端:StrategyTab 持仓行展开(懒加载内嵌汇总表)。
## 数据流
```
策略持仓 tab
load() → strategy-positions(每行含 holding_id
用户点击持仓行 → 展开 → trades/by-holding?holdingId=13 → 委托汇总列表
懒加载:不展开不请求
```
## 实现细节
### 1. PositionManager.getStrategyPositions 附加 holding_id
```js
async getStrategyPositions(strategyId) {
const [positions, dataset, holdings] = await Promise.all([
this.getAllPositions(),
this.storage.getDataset(strategyId),
this.storage.getCurrentHoldings(strategyId), // 含 holding_id
]);
const shareMap = new Map(dataset.map(d => [d.code, d.shares]));
const holdingMap = new Map(holdings.map(h => [h.code, h.holdingId]));
return positions.map(p => {
const shares = shareMap.get(p.code) ?? 0;
return shares > 0 ? { ...p, shares, holdingId: holdingMap.get(p.code) ?? null } : null;
}).filter(Boolean);
}
```
### 2. SqliteStore.getOrdersByHolding
```js
getOrdersByHolding(holdingId) {
this.init();
return this.db.prepare(
'SELECT * FROM trade_orders WHERE holding_id=? ORDER BY insert_ts DESC'
).all(holdingId).map(r => this._mapOrderRow(r));
}
```
### 3. api/trades.jstrades/by-holding 端点
```js
case 'trades/by-holding':
return await storage.getTradeOrdersByHolding(args.holdingId);
```
### 4. StrategyTab 展开 UI
- 持仓行加展开箭头(类似交易记录 tab);
- 展开时调 trades/by-holding,内嵌小表格显示委托汇总:
时间 / 方向 / 状态 / 委托量 / 成交量 / 均价 / 金额 / 费用;
- 只显示委托汇总(ORDER_STATUS 中文映射复用);
- 懒加载:展开时才请求,折叠清空。
## 涉及设计约束
| 约束 | 内容 |
|---|---|
| 技术约束-012 | 数据存储遵循数据存储设计.md(复用 trade_orders.holding_id |
| 技术约束-011 | 测试用独立数据目录(ODL_TEST_DATA_DIR |
@@ -0,0 +1,38 @@
# 迭代复盘:08-策略持仓行展开关联交易记录(Holding → 交易汇总)
> 复盘日期:2026-09-02 | 迭代状态:**已完成(老师确认)**
> 关联需求:R-010(策略持仓行展开关联交易记录,已定稿)
> 关联计划:PLAN-009(计划-策略持仓行展开关联交易记录)
## 结果
迭代 08 达成:策略持仓 tab 每个持仓行(Holding)可展开,展开显示与该 holding 关联的交易记录(委托汇总,不分笔成交);持仓行新增成本价(avgPrice)+ 最后一笔成交价(lastTradePrice)两列;神之一手全部 tab 激活时隐藏 AI 对话输入框(纯 CSS)。实现「持仓 ↔ 交易」双向追溯。
## 过程事实
1. **需求定稿(R-010**:老师提出持仓行展开看关联交易(只要委托汇总)→ AI 登记 Q1-Q3(附加 holding_id / by-holding 端点 / 展开 UI)→ 老师确认 → 补充 Q4(成本价 + 最后一笔成交价,取最新有成交的 tradedPrice,无则默认成本价);
2. **服务端**strategy-positions 附加 holding_idPositionManager 查 strategy_holdings+ 计算 avgPrice/lastTradePrice(查该 holding 关联委托,取最新有成交的 tradedPrice);SqliteStore.getOrdersByHolding + DataStore 委托;api trades/by-holding 端点;
3. **前端**StrategyTab 持仓行展开(箭头 + 懒加载 + 内嵌委托汇总表,含交易日/时间列);成本价/最后一笔成交价两列;
4. **隐藏输入框**:借鉴 dsh-context 插件的纯 CSS 方案(:has(.odl-root) 命中时隐藏 composer)——神之一手 tab 激活时隐藏输入框,chat/其他 tab 正常显示;
5. **验证**:回归测试(by-holding 查询 6 项 + lastTradePrice 计算 4 项)通过、真实数据验证(001330.SZ holding 13 → 博纳影业卖出委托;成本价 6.84 / 最后成交 6.00)、构建 + typecheck 通过。
## 经验教训(复盘沉淀)
### 1. 隐藏宿主 UI 优先借鉴成熟插件方案
- 曾深挖 DSH composer chain / selector 机制(复杂度高、依赖内部 store),后经老师提示参考 dsh-context 插件——它用一行纯 CSS(:has() 选择器)实现「特定 tab 激活时隐藏输入框」,零宿主改动、零风险;
- **沉淀**:改宿主 UI 前先看同类插件怎么做的;纯 CSS :has() 是最优雅的 view 条件渲染方案。
### 2. 前端声明顺序 TDZ(本次多次踩坑)
- 迭代 07/08 多次遇到「Cannot access X before initialization」(TDZ):useEffect 依赖数组/回调在渲染时求值,引用了后声明的 const;
- **沉淀**React 组件内所有 useCallback/useEffect 的依赖引用必须在其声明之后;新增代码时严格核对声明顺序,构建后浏览器验证。
### 3. 服务端 vs 客户端生效机制
- 服务端改动(PositionManager/API/SqliteStore)需重启 DSH 生效;客户端 bundle 刷新即载(rev 变化自动同步);
- **沉淀**:改完先确认服务端端点/响应是新行为,再让老师重启。
## 遗留/后续
1. **持仓展开的交易汇总**:当前只显示该 holding 关联的委托汇总;后续可按 holding 聚合复盘(目标-006);
2. **关注列表 tab**:仍为占位(PlaceholderTab),后续迭代实现;
3. **全部持仓 tab**:未做展开(老师只要求策略持仓 tab);如需可复用同样模式;
4. **迭代 07 遗留**:QMT Bridge 历史接口(老师完善后接入)、盘中行情写 SQLite 验证(开盘后确认)。
@@ -0,0 +1,24 @@
# 迭代目标:08-策略持仓行展开关联交易记录(Holding → 交易汇总)
> 迭代编号:08 | 创建:2026-09-02 状态:进行中
> 依据计划:PLAN-009 需求:R-010(已定稿,2026-09-02
## 目标描述
策略持仓 tab 每个持仓行(Holding)可展开,展开显示与该 holding 关联的交易记录(仅委托汇总,不分笔成交),实现「持仓 ↔ 交易」双向追溯(R-009 反向)。
## 目标分解
1. **strategy-positions 附加 holding_id**:持仓行带出关联锚点;
2. **trades/by-holding 端点**:按 holding_id 查委托汇总;
3. **持仓行展开 UI**:展开显示委托汇总表(懒加载)。
## 讨论过程
- 2026-09-02 老师提出需求(持仓行展开看关联交易,只要委托汇总);
- 2026-09-02 AI 登记 R-010(讨论中),提出 Q1-Q3(附加 holding_id / by-holding 端点 / 展开 UI);
- 2026-09-02 老师确认 Q1-Q3 定稿,进入本迭代。
## 对老师的配合需求
- 无阻塞依赖;验收时用真实持仓(如 001330.SZ holding 13)验证展开效果。
@@ -0,0 +1,23 @@
# 验收标准:08-策略持仓行展开关联交易记录(Holding → 交易汇总)
## 验收标准线
1. **strategy-positions 附加 holding_id**:每个持仓行含 holding_id(同策略同 code 当前持仓);
2. **trades/by-holding 端点**:按 holding_id 返回该 holding 的委托汇总(时间/方向/状态/量/价/金额/费用;无成交委托也显示);
3. **持仓行展开**:策略持仓 tab 每行可展开(箭头指示),展开显示委托汇总表;
4. **仅汇总不分笔**:展开内容只显示委托汇总,无分笔成交明细;
5. **懒加载**:不展开不请求 trades/by-holding
6. **不回归**:策略持仓份额操作(添加/移出/全部移入)、交易记录 tab、行情等现有功能正常;
7. **测试隔离**:回归测试在独立数据目录执行(技术约束-011)。
## 验收方法
- 独立数据目录构造持仓 + 委托(holding_id 关联)→ 启动 → 验证 strategy-positions 含 holding_id、trades/by-holding 返回正确;
- 真实数据:网格超市 tab 展开 001330.SZholding 13)→ 看到博纳影业卖出委托汇总;
- 前端:展开/折叠、懒加载、汇总表列正确;
- 回归:份额操作、交易记录、行情不回归。
## 验收目标
- 持仓 ↔ 交易双向追溯闭环(R-009 交易→持仓 + 本迭代持仓→交易);
- 为按 holding 复盘交易铺路(目标-006)。