迭代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:
2026-09-07 18:15:07 +08:00
parent ed43ecae39
commit 51c70c48fb
14 changed files with 681 additions and 29 deletions
+1
View File
@@ -18,6 +18,7 @@
* strategies/remove → 删除策略 { strategyId },联动清份额
* strategies/move → 排序 { strategyId, dir }
* strategy-positions → 某策略持仓 { strategyId }
* strategy-holdings/history → 某策略已清仓历史持仓 { strategyId, range }R-016
* unallocated → 未分配持仓
* summary → 分仓摘要
* add-shares → 添加份额 { code, strategyId, shares }
+53 -2
View File
@@ -4,6 +4,7 @@
* 端点:
* strategies / strategies/update / strategies/add / strategies/remove
* strategy-positions / unallocated / summary
* strategy-holdings/historyR-016 迭代 14:已清仓历史持仓,范围筛选)
* add-shares / move-all-shares / remove-shares
* tabs / tabs/update
*/
@@ -14,6 +15,24 @@ import {
getStrategyColumns, updateStrategyColumns,
} from '../settings.js';
/** R-016 范围档 → 回溯毫秒(自然日近似,展示用途足够;技术约束-019)。
* range='today' 不走回溯毫秒——二轮补充(2026-09-07 老师定):本地自然日 00:00 起清仓的持仓
* 作为「今日已清仓」默认层(开关「历史持仓」不控制它,当天清仓默认显示),sinceMs 在 handler 内特判。 */
const HISTORY_RANGE_MS = {
week: 7 * 86400000,
month: 30 * 86400000,
quarter: 90 * 86400000,
halfYear: 182 * 86400000,
year: 365 * 86400000,
};
/** 本地自然日零点毫秒(range='today' 用:今天 00:00 起 = 当日清仓的持仓) */
function localDayStartMs() {
const d = new Date();
d.setHours(0, 0, 0, 0);
return d.getTime();
}
/** 策略/份额/tabs 端点方法表 */
export const STRATEGY_METHODS = new Set([
'strategies',
@@ -31,15 +50,16 @@ export const STRATEGY_METHODS = new Set([
'holdings/values-update', // R-013:写某持仓行的自定义字段值
'strategy-columns', // 迭代11列显隐:读某策略列配置(归一化)
'strategy-columns/update', // 迭代11列显隐:写某策略列配置
'strategy-holdings/history', // R-016 迭代 14:某策略已清仓历史持仓(范围筛选,只读)
]);
/**
* 处理策略/份额/tabs 端点
* @param {string} method
* @param {object} args
* @param {object} runtime { manager, settings }
* @param {object} runtime { manager, settings, storage }
*/
export async function handleStrategy(method, args, { manager, settings }) {
export async function handleStrategy(method, args, { manager, settings, storage }) {
switch (method) {
case 'strategies':
return getStrategies(settings);
@@ -78,6 +98,37 @@ export async function handleStrategy(method, args, { manager, settings }) {
case 'strategy-columns/update':
await updateStrategyColumns(settings, args.strategyId, args.columns);
return getStrategyColumns(settings, args.strategyId);
case 'strategy-holdings/history': {
// R-016 迭代 14:某策略已清仓历史持仓(当前持仓快照路径零改动,技术约束-019)
if (!args.strategyId) {
throw Object.assign(new Error('缺少 strategyId 参数'), { code: 'bad-request' });
}
const range = args.range ?? 'week';
// R-016 二轮补充(2026-09-07 老师定):range='today' = 本地自然日 00:00 起清仓的持仓
// (前端默认层——当天清仓默认显示,与「历史持仓」开关解耦);其余 5 档维持回溯毫秒语义
let sinceMs;
if (range === 'today') {
sinceMs = localDayStartMs();
} else {
const ms = HISTORY_RANGE_MS[range];
if (!ms) {
throw Object.assign(new Error('range 非法: ' + range + 'today|week|month|quarter|halfYear|year'), { code: 'bad-request' });
}
sinceMs = Date.now() - ms;
}
const rows = await storage.getHoldingsHistory(args.strategyId, { sinceMs });
// name 兜底:该 holding 最近一笔关联委托的 name(前端免二次请求)
const ids = rows.map((r) => r.holdingId);
const nameMap = new Map();
if (ids.length > 0) {
const ph = ids.map(() => '?').join(',');
const orows = storage.sqlite.db.prepare(
"SELECT holding_id, name FROM trade_orders WHERE holding_id IN (" + ph + ") AND name != '' ORDER BY insert_ts DESC"
).all(...ids);
for (const o of orows) if (!nameMap.has(o.holding_id)) nameMap.set(o.holding_id, o.name);
}
return rows.map((r) => ({ ...r, name: nameMap.get(r.holdingId) ?? null }));
}
default:
throw Object.assign(new Error('unknown strategy method: ' + method), { code: 'not-found' });
}