Files
one_divine_lot/docs/04-迭代记录/14-策略tab历史持仓展示/技术实现方案.md
T
kyugao 51c70c48fb 迭代14: 策略tab历史持仓展示(R-016)+ 今日已清仓默认层
- R-016 Q1-Q7: 两个策略 tab title 旁「历史持仓」开关 + 清仓范围筛选(近一周默认/1月/3月/半年/1年)
  - SqliteStore.getHoldingsHistory(strategy_id + closed_at 非空 + sinceMs,closed_at DESC,只读)
  - DataStore 门面透传 + strategy-holdings/history 端点(name 兜底 + 参数校验)
  - 前端: 开关/范围下拉/历史行灰显+「已清仓」徽标/展开复用 R-010(rowKey 唯一化)
- R-016 二轮补充(Q8-Q9 老师拍板): 今日已清仓默认层
  - range=today = 本地自然日 00:00 起(特判零点,不落回溯毫秒档)
  - 当天已清仓的持仓默认恒显示、不受「历史持仓」开关控制(开关只控今天之前)
  - rowsToRender 分层合成 + holdingId 去重;标题计数不含已清仓行;零写路径变更
- 回归: test-r016-history 24/24 + 存量回归全绿
2026-09-07 18:15:07 +08:00

80 lines
5.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 技术实现方案:14-策略tab历史持仓展示
> 迭代编号:14 依据:PLAN-015 + R-016(老师拍板:Q2 显示 0 / 其余按建议)
## 1. 存储层(SqliteStore.getHoldingsHistory
```js
/** 某策略已清仓持仓(closed_at 非空),closed_at ≥ sinceMs,按 closed_at DESC */
getHoldingsHistory(strategyId, { sinceMs } = {}) {
// SELECT holding_id, strategy_id, code, shares, created_at, closed_at, "values"
// FROM strategy_holdings
// WHERE strategy_id=? AND closed_at IS NOT NULL [AND closed_at >= ?]
// ORDER BY closed_at DESC
// 返回 _mapHolding 同构(holdingId/strategyId/code/shares/createdAt/closedAt/values
}
```
- 只读新增;**零写路径变更**closeHolding 维持 shares=0 + closed_atQ2 定稿);
- sinceMs 缺省 = 不过滤(内部能力预留;API 层始终传值);
- 与 getHoldingHistory(code) 并存:后者是全表按 code 查(R-008 遗留),不满足本需求维度,不复用不改动。
## 2. 门面层(DataStore
```js
async getHoldingsHistory(strategyId, opts) { await this._ensure(); return this.sqlite.getHoldingsHistory(strategyId, opts); }
```
## 3. APIsrc/api/strategies.js 新 method
- method`strategy-holdings/history`
- args`{ strategyId: string, range: 'week'|'month'|'quarter'|'halfYear'|'year' }`
- range → sinceMs 换算(服务端):week=7d / month=30d / quarter=90d / halfYear=182d / year=365d(自然日近似,回溯窗口展示用途足够,精确日历边界无业务含义);
- 缺 strategyId 或 range 非法 → 参数错误(code: 'bad-request');
- 返回行附加 name 兜底:按 holdingId 查 trade_orders 取最新一笔的 nameSQL 单查,无则 null)——前端直接展示,免二次请求。
## 4. 前端(StrategyTab.jsx
- 状态:`showHistory`(默认 false)、`historyRange`(默认 'week')、`historyRows``historyLoading`
- 头部按钮区:「历史持仓」toggle 按钮 + 开启时显示范围下拉(近一周/近1个月/近3个月/近半年/近1年);
- 数据流:开启或切范围 → call('one-divine-lot/strategy-holdings/history', { strategyId, range });缓存按 range 键保留,关闭仅隐藏;
- 渲染:rowsForRender = 当前持仓行 +showHistory ? 历史行 : []);历史行标识 `isHistory: true`
- 行样式:opacity/灰色调(--dsw-alias-label-tertiary+ 徽标「已清仓 YYYY-MM-DD」;
- 单元格:代码/名称/清仓时间(YYYY-MM-DD HH:mm/持有天数(ceil((closedAt-createdAt)/86400000),同日=1);shares 列显示 DB 原值(=0Q2);行情类列(lastPrice/pctChange/lastClose/avgPrice/lastTradePrice/upStop/downStop/open/high)一律 —(不注册行情、不取价);自定义字段列:p.values 只读展示,禁点击编辑(isHistory 不进入 FieldCellEditor 分支);
- 展开:复用 toggleExpand(code, holdingId) → trades/by-holding(历史行同样有效);
- 排序:当前行维持现状(涨幅排序仅作用于当前行集合);历史行固定清仓时间 DESC 追加在后;
- 计数:标题(N 只)= 当前持仓数,不含历史行。
## 5. 回归脚本(scripts/test-r016-history.mjs
- ODL_TEST_DATA_DIR 临时目录(技术约束-011,不碰真实数据);
- 覆盖:建仓→清仓→history 可查 / 范围过滤边界(7d/30d…)/ 排序 / 策略隔离 / 未清仓行不出现 / API 参数校验 / name 兜底。
## 6. 验证链
typecheck → build → test-r016 → 存量回归(test-position-sync 34 / test-r013 21 / test-quote-sync)→ 人工验收清单。
## 7. 二轮补充实现:今日已清仓默认层(2026-09-07 老师拍板 Q8-Q9
> 在首轮(§1-6)基础上追加;首轮实现保持可追溯,新增改动如下。
### 7.1 服务端(src/api/strategies.js
- range 新增 today**特判本地自然日零点**localDayStartMssetHours(0,0,0,0)),sinceMs = 本地今天 00:00;不落入 HISTORY_RANGE_MS 回溯毫秒档(那 5 档语义 = now − 范围,today 语义 = 自然日边界,两类不可混);
- 参数校验错误信息扩为 today|week|month|quarter|halfYear|year;其余逻辑(strategy_id 过滤 + name 兜底 + DESC 排序)与 5 档完全复用——**SqliteStore/DataStore 零改动**sinceMs 本就通用)。
### 7.2 前端(src/client/views/StrategyTab.jsx
- 状态:historyRows 缓存键新增 today(与 5 档并存互不干扰);
- **数据流**loadHistory 定义提前到 load 之前;load() 的 finally 里调 loadHistory('today')——进 tab 即拉 + 每次持仓重载(移出/加仓/编辑后 load())都刷新今日已清仓层,当天清仓转历史后 tab 内不因此少行;
- **渲染合成(rowsToRender**分两层:
- L1 = histRowsFor('today') —— **恒显示**showHistory 关也显示);当前持仓行后追加,清仓时间 DESC;
- L2 = showHistory 开时追加 histRowsFor(historyRange)**按 holdingId 去重**(剔除已含于 L1 的行,避免今天清仓 + 开启近一周出现两行);
- 交互文案:开关 title 更新为「显示/隐藏今天之前已清仓的历史持仓(当天已清仓的持仓始终显示)」;文件头注释同步;
- 标题计数(N 只)仍只算当前持仓,两层已清仓行不计入。
### 7.3 回归(scripts/test-r016-history.mjs
- 新增 [4b] range=today:昨天 23:59:59 清仓不出现 / 今天 00:00:01 清仓出现 / 不含 40 天前行 / today 行 name 兜底同样生效 / range 非法校验不受 today 引入影响;
- 验证链复跑:test-r016 24/24 + 存量回归全绿(见迭代复盘)。