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 + 存量回归全绿
5.5 KiB
5.5 KiB
技术实现方案:14-策略tab历史持仓展示
迭代编号:14 | 依据:PLAN-015 + R-016(老师拍板:Q2 显示 0 / 其余按建议)
1. 存储层(SqliteStore.getHoldingsHistory)
/** 某策略已清仓持仓(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)
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 + 存量回归全绿(见迭代复盘)。