迭代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 + 存量回归全绿
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
# 技术实现方案: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 + 存量回归全绿(见迭代复盘)。
|
||||
@@ -0,0 +1,38 @@
|
||||
# 迭代复盘:14-策略tab历史持仓展示
|
||||
|
||||
> 复盘日期:2026-09-07 | 迭代状态:**已实施,待老师人工验收**
|
||||
> 关联需求:R-016 | 关联计划:PLAN-015
|
||||
|
||||
## 结果
|
||||
|
||||
迭代 14 达成:两个策略 tab(做T / 网格超市)title 旁新增「历史持仓」开关 + 清仓时间范围筛选(近一周默认 / 近 1 个月 / 近 3 个月 / 近半年 / 近 1 年);已清仓持仓行灰显 + 「已清仓」徽标(含清仓日期)追加在当前持仓行后(closed_at DESC),展开复用 R-010 查看关联交易——补上「清仓即失联」的操作追溯缺口。当前持仓快照路径(R-014 语义)与所有写路径(closeHolding 置 shares=0,Q2 拍板)零改动。
|
||||
|
||||
## 过程事实
|
||||
|
||||
1. **讨论驱动定稿**(2026-09-07,Q1-Q7 老师逐项拍板):Q2 为本轮唯一改判——AI 建议改 closeHolding 保留清仓时份额,老师否决:「显示 0 就好了,即使显示了清仓时的 shares 也没多少意义,主要目标是追溯这次持仓相关的历史操作」——需求核心从「数据完整性」校正为「操作追溯锚点」,方案随之收敛为零写路径变更;其余六问均按 AI 建议定稿;
|
||||
2. **实现**(严格按 PLAN-015 步骤):SqliteStore.getHoldingsHistory(strategy_id + closed_at IS NOT NULL + sinceMs 过滤 + DESC,只读新增)→ DataStore 门面透传 → strategy-holdings/history 端点(range 五档服务端换算自然日近似 + name 关联委托兜底 + 参数校验 bad-request)→ StrategyTab 开关/范围下拉/历史行渲染;
|
||||
3. **实现中发现并修复一个真实隐患**:R-010 展开状态与交易缓存原以 code 为键——同码「当前行 + 历史行」并存(清仓后重新买入)时两行会同时展开、缓存串行;本轮把展开状态/缓存/行键统一收敛为唯一 rowKey(当前行 = code,历史行 = 'h-' + holdingId),toggleExpand 签名同步变更(当前调用方仅 tbody 一处);
|
||||
4. **验证**:test-r016-history.mjs 19/19(清仓转历史可查 / 5 档范围边界 / 排序 / 策略隔离 / 未清仓不出现 / 参数校验 / name 兜底 / 端点分发零破坏);typecheck 通过;存量回归 test-position-sync 35/35、test-r013 21/21、test-quote-sync 27/27;build 通过(client bundle 164KB wrapped);
|
||||
5. **文档链**:R-016 定稿 + PLAN-015 + 迭代三件套 + 技术约束-019 + 需求池索引(R-016 登记;顺带补登遗漏的 R-015 行);
|
||||
6. **部署态排查(老师首验无数据,2026-09-07)**:实锤定位 = **运行中的 DSH 服务端进程仍是旧代码**——同进程 /odl/api/summary 正常应答、新端点返回 unknown method(curl 实测);客户端 bundle 因插件 symlink 直连源码 lib/ 而被刷新(按钮已出现),服务端模块注册表却停留在进程启动时;处置 = ① loadHistory 失败从静默吞空改为 Toast 可见化(静默兜底=排障盲区,本轮教训)并重新构建(test-r016 19/19 复跑通过);② 服务端生效依赖 DSH web 进程重启(插件 symlink 方式安装,无需重装,重启即取新 lib)。
|
||||
|
||||
## 经验教训(复盘沉淀)
|
||||
|
||||
### 1. 需求的「核心目标」要听老师的定调,不要替老师拔高
|
||||
- AI 在 Q2 提议改 closeHolding 语义以「保留清仓时份额」,出发点是账本完整性;老师一句话把需求核心定调为「追溯历史操作」——份额数字本身没有追溯价值,holding_id 锚点 + 关联委托才有;
|
||||
- 沉淀:讨论中 AI 的方案建议要标明「我认定的价值假设」,让老师有机会否定假设本身,而不只是选项。
|
||||
|
||||
### 2. 复用既有交互时,键的唯一性要先审后用
|
||||
- 展开状态按 code 键控在「一码一行」时代是隐含正确的;历史行引入打破了「一码一行」不变量——同类隐含假设(缓存 Map、React key、loading 标记)都要在数据形态变化时重新过一遍;
|
||||
- 沉淀:向既有列表引入第二类行时,先列出所有「按 X 键控」的状态,逐个判断是否需要升级为行唯一键。
|
||||
|
||||
### 3. 只读需求的实现应当「零写路径」可断言
|
||||
- 本轮全程未触碰任何 INSERT/UPDATE 语句与语义(closeHolding 分毫未动),回归脚本专门加了「strategy-positions 分发不变」断言——历史查询与当前查询的隔离在端点层就已成立(独立 method),不需要靠约定;
|
||||
- 沉淀:「只读功能」的范围声明落成验收断言(分发不变 + 写路径 diff 为空),比口头承诺可靠。
|
||||
|
||||
## 遗留/后续
|
||||
|
||||
1. **归属候选不纳入清仓持仓**(R-016 关联观察,2026-09-07 大连热电实际遇到):清仓后当日委托无法再通过下拉关联到对应 holding——是否立独立需求(候选纳入近期清仓持仓)待老师定;
|
||||
2. **历史行成本价/收益推导**:本期不做(边界定稿);若后续要做,数据源 = 关联委托成交记录,属展示层推导,不动存储;
|
||||
3. **清仓方式标记**(手动 vs 幽灵自动):数据未区分,本期不做;若要做需 closeHolding 增加来源标记(涉及写路径,另立需求);
|
||||
4. **R-015 / 迭代 13 人工验收**仍待老师确认(与本迭代无依赖)。
|
||||
@@ -0,0 +1,29 @@
|
||||
# 迭代目标:14-策略tab历史持仓展示
|
||||
|
||||
> 迭代编号:14 | 创建:2026-09-07 | 状态:已实施,待验收
|
||||
> 依据计划:PLAN-015 | 需求:R-016(已定稿,2026-09-07,Q1-Q7 老师拍板)
|
||||
|
||||
## 目标描述
|
||||
|
||||
两个策略 tab(做T / 网格超市)title 旁添加「历史持仓」功能:① 显示/隐藏已清仓历史持仓的开关;② 显示历史时默认展示近一周清仓的持仓,并提供 近 1 个月 / 近 3 个月 / 近半年 / 近 1 年 范围选项。历史持仓行复用 R-010 行展开查看关联交易记录——补上「清仓即失联」的操作追溯缺口(核心目标:追溯该持仓相关的历史操作,老师 2026-09-07 定调)。
|
||||
|
||||
## 目标分解
|
||||
|
||||
1. **存储查询**(SqliteStore.getHoldingsHistory):closed_at 非空 + closed_at ≥ sinceMs + strategy_id 过滤,按 closed_at DESC;只读新增,不动任何写路径(closeHolding 置 shares=0 语义维持,Q2 老师拍板「显示 0 就好」);
|
||||
2. **门面透传**(DataStore.getHoldingsHistory);
|
||||
3. **API 端点**(strategy-holdings/history):strategyId 必填 + range ∈ week|month|quarter|halfYear|year(服务端换算 sinceMs;week 为前端默认但服务端不设隐式默认,参数缺失报错);
|
||||
4. **前端**(StrategyTab):头部按钮区加「历史持仓」开关 + 范围下拉(开启时显示,默认 week);懒加载 + 范围切换重拉 + 关闭隐藏(缓存保留);历史行追加在当前持仓行后(清仓时间降序)、灰显 + 「已清仓」徽标、标题计数不含历史行;历史行字段 = 代码/名称(关联委托 name 兜底)/份额(0)/清仓时间/持有天数,行情列 —,自定义字段列只读;行展开复用 R-010;
|
||||
5. **回归**:scripts/test-r016-history.mjs(ODL_TEST_DATA_DIR 临时目录)+ typecheck + build + 存量回归三脚本。
|
||||
|
||||
## 二轮补充(2026-09-07 验收前追加:今日已清仓默认层,Q8-Q9 老师拍板)
|
||||
|
||||
- **补充诉求**:当天(本地自然日 00:00 起)已清仓的持仓**默认显示**;「历史持仓」开关只控制**今天之前**的历史——历史持仓关 ≠ 当天已清仓也被藏起(首轮「进 tab 默认只看当前」使当天清仓行当场不可见,补上该缺口);
|
||||
- **实现增量**:① 服务端 range='today' 特判本地自然日零点(不落回溯毫秒档);② 前端 rowsToRender 分两层——L1 今日已清仓恒显示(缓存键 'today',随每次持仓重载刷新)+ L2 历史范围层受开关控制,层间按 holdingId 去重;③ 视觉/计数维持首轮(灰显+徽标、计数不含已清仓行);
|
||||
- 影响面:只动只读查询 + 前端渲染合成;**零写路径变更**(closeHolding 语义仍不动);
|
||||
- 回归:test-r016 追加 range=today 自然日边界用例(24/24)。
|
||||
|
||||
## 对老师/主理人的配合需求
|
||||
|
||||
- 实现完成后需老师真实环境人工验收(开关/范围切换/历史行展示/展开复盘/与真实清仓数据对照);
|
||||
- 二轮补充后重点验收:**「历史持仓」关时当天清仓的持仓仍显示**、「历史持仓」开时今天之前的行按范围显示、层间无重复行;
|
||||
- 验收通过后本迭代标记「验收通过」,R-016 进入归档流程。
|
||||
@@ -0,0 +1,38 @@
|
||||
# 验收标准:14-策略tab历史持仓展示
|
||||
|
||||
> 迭代编号:14 | 依据:PLAN-015 验收要点 + R-016
|
||||
|
||||
## 验收标准线
|
||||
|
||||
1. **自动化**:typecheck + build 通过;test-r016-history.mjs 全绿(清仓转历史可查 / 5 档范围过滤与边界 / closed_at DESC 排序 / 策略隔离 / 未清仓行不出现 / 参数校验 / name 兜底);存量回归 test-position-sync、test-r013、test-quote-sync 全绿;
|
||||
2. **入口与默认态**:做T / 网格超市两个 tab title 旁出现「历史持仓」开关;默认关闭 = 与现状完全一致(进 tab 无历史行);开启后默认范围 = 近一周;**全部持仓 tab 无此功能**;
|
||||
3. **范围筛选**:5 档(近一周/近1个月/近3个月/近半年/近1年)切换即重拉;用 2026-09-07 大连热电真实数据验证——holding_id=15(做T,当日 09:32:35 幽灵清仓)在近一周内可见、切近 1 个月仍可见;
|
||||
4. **历史行展示**:灰显 + 「已清仓 YYYY-MM-DD」徽标;清仓时间/持有天数正确;份额显示 0(Q2);行情列(现价/涨幅/涨停跌停/今开/最高/成本/最后成交价)显示 —;名称有则显示、无则 —;标题计数(N 只)不含历史行;当前持仓在前、历史行按清仓时间降序在后;
|
||||
5. **展开复盘**:历史行展开可见该 holding 关联委托(大连热电 09:31:26 卖出 1000 股 @7.42 应出现);自定义字段历史行只读(无编辑入口);
|
||||
6. **零破坏**:strategy-positions / unallocated / summary 等现有端点行为不变;当前持仓行展示与现状一致;无任何写路径变更(closeHolding 语义不变)。
|
||||
|
||||
## 验收方法
|
||||
|
||||
- 自动化项 AI 执行出具结果;
|
||||
- 2~5 老师真实环境人工验收:重载插件 → 两个策略 tab 开关历史持仓 → 切换范围 → 对照大连热电当日清仓数据 → 展开历史行核对关联委托;
|
||||
- 全部通过后:迭代 14 标记「验收通过」,R-016 归档 已完成/。
|
||||
|
||||
## 验收目标
|
||||
|
||||
- 6 条验收线通过,迭代 14 标记「验收通过」,R-016 更新实现状态(已实现)并归档。
|
||||
|
||||
## 二轮补充验收线(2026-09-07 Q8-Q9 追加:今日已清仓默认层)
|
||||
|
||||
7. **今日已清仓默认显示(核心)**:「历史持仓」**关**时,策略 tab 仍显示 当前持仓 + 今天(本地自然日 00:00 起)已清仓的持仓行;当天卖出/幽灵清仓的持仓行不因开关关闭而消失;
|
||||
8. **开关只控更早历史**:「历史持仓」开时,追加显示今天之前各范围(近一周/近1个月/近3个月/近半年/近1年)已清仓行——近一周范围**不含**今天已显示的行(层间按 holdingId 去重,无重复行);
|
||||
9. **边界与展示**:昨天 23:59:59 清仓的行在开关关时不可见(属更早历史);今日行沿用灰显 + 已清仓徽标;标题计数(N 只)仍不含已清仓行;行情列 — / 份额 0 / 只读字段 / 行展开复用 R-010 均维持首轮;
|
||||
10. **自动化**:test-r016 新增 range=today 用例全绿(24/24);typecheck + build + 存量回归(test-position-sync / test-r013 / test-quote-sync)全绿。
|
||||
|
||||
## 验收方法(二轮补充)
|
||||
|
||||
- 自动化项 AI 执行出具结果(已执行:test-r016 24/24、position-sync 35、r013 21、quote-sync 27、typecheck 0、r017 12);
|
||||
- 老师真实环境人工验收:刷新页面 → 在不开启「历史持仓」的情况下核对当天已清仓持仓仍显示 → 开启开关核对今天之前的行按范围出现且与今日行无重复 → 对照大连热电当日清仓数据。
|
||||
|
||||
## 验收目标
|
||||
|
||||
- 首轮 6 条 + 二轮 4 条验收线通过后,迭代 14 标记「验收通过」,R-016 归档 已完成/。
|
||||
Reference in New Issue
Block a user