51c70c48fb
- 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 + 存量回归全绿
80 lines
5.5 KiB
Markdown
80 lines
5.5 KiB
Markdown
# 技术实现方案: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_at,Q2 定稿);
|
||
- sinceMs 缺省 = 不过滤(内部能力预留;API 层始终传值);
|
||
- 与 getHoldingHistory(code) 并存:后者是全表按 code 查(R-008 遗留),不满足本需求维度,不复用不改动。
|
||
|
||
## 2. 门面层(DataStore)
|
||
|
||
```js
|
||
async getHoldingsHistory(strategyId, opts) { await this._ensure(); return this.sqlite.getHoldingsHistory(strategyId, opts); }
|
||
```
|
||
|
||
## 3. API(src/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 取最新一笔的 name(SQL 单查,无则 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 原值(=0,Q2);行情类列(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:**特判本地自然日零点**(localDayStartMs,setHours(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 + 存量回归全绿(见迭代复盘)。
|