feat: 研究Tab策略子Tab拆分当前/历史区域——当前使用(实盘/新策略)展开+历史策略默认折叠,按s.current字段分组

This commit is contained in:
hmo
2026-08-11 12:09:15 +08:00
parent 7105bba793
commit 5cf0e3035c
+70 -45
View File
@@ -2467,52 +2467,77 @@ function renderStrategyTable(strategies) {
const arrow = window._sortKey === key ? (window._sortDir === 'desc' ? '▼' : '▲') : '';
return `<th class="${cls || 'text-right'} px-2 py-1.5 cursor-pointer hover:text-slate-300 select-none" onclick="sortBy('${key}')" title="${title || '点击排序'}">${label}${arrow ? ' ' + arrow : ''}</th>`;
};
let html = '<div class="overflow-x-auto"><table class="w-full text-xs whitespace-nowrap"><thead><tr class="text-slate-500 border-b border-slate-700">' +
th('version', '版本', 'text-left') + th('name', '名称', 'text-left') +
th('sizing_slots', '仓位', 'text-center') +
'<th class="text-center px-2 py-1.5">说明</th>' +
th('composite', '综合', 'text-right') +
th('universality', '普适', 'text-right') +
th('full_return', '全参与', 'text-right') +
th('capital_final', '最终资产', 'text-right') +
th('cagr', '年化', 'text-right') +
th('trades', '执行数', 'text-right', '实际参与组合模拟的交易笔数;信号总数=为鼠标悬停查看') +
th('hold', '均持仓', 'text-right') +
th('win_rate', '胜率', 'text-right') +
th('avg_profit', '平均收益', 'text-right') + th('sharpe', '夏普', 'text-right') +
th('profit_factor', '盈亏比', 'text-right') + th('max_dd', '资产回撤', 'text-right') +
'<th class="text-center px-2 py-1.5">操作</th></tr></thead><tbody>';
for (const s of strategies) {
const st = s.summary_stats || {};
const pf = st.portfolio || {};
const hasResult = st.total_trades != null;
const smallSample = hasResult && st.total_trades < 40;
const isCurrent = (s.version === CURRENT_STRATEGY || CURRENT_FAMILY.has(s.version)) && (s.market || 'all') !== 'hk';
const deprecatedReason = DEPRECATED[s.version];
const rowCls = 'border-b border-slate-800/50 hover:bg-slate-800/30 cursor-pointer' + (isCurrent ? ' bg-emerald-900/20 border-l-2 border-l-emerald-400' : '');
html += '<tr class="' + rowCls + '" onclick="showStrategyDetail(\'' + s.version + '\')">' +
'<td class="px-2 py-1.5 font-mono font-bold ' + (deprecatedReason ? 'text-red-400/70 line-through' : (isCurrent ? 'text-emerald-400' : 'text-blue-400')) + '">' + s.version + (isCurrent ? ' <span class="text-[10px] bg-emerald-500/20 text-emerald-300 px-1 rounded">当前</span>' : '') + (deprecatedReason ? ' <span class="text-[10px] bg-red-500/20 text-red-300 px-1 rounded" title="' + deprecatedReason.replace(/"/g, '&quot;') + '">已证伪</span>' : '') + ((s.market && s.market !== 'all') ? ' <span class="text-[10px] bg-cyan-500/20 text-cyan-300 px-1 rounded">' + (s.market === 'hk' ? '港' : 'A') + '</span>' : '') + '</td>' +
'<td class="px-2 py-1.5 whitespace-normal break-words" title="' + (s.hypothesis || s.summary || '').replace(/"/g, '&quot;') + '">' + (s.name || '') + (smallSample ? ' <span class="text-amber-500" title="样本<40笔">⚠️</span>' : '') + '</td>' +
'<td class="text-center px-2 py-1.5 font-mono text-cyan-300">' + (st.sizing_slots ? st.sizing_slots + '仓' : '10仓') + (st.conviction_model ? '<button onclick="event.stopPropagation();showConviction(\'' + s.version + '\')" class="text-amber-400 hover:text-amber-300" title="查看信念分级仓位规则">⚡</button>' : '') + '</td>' +
'<td class="text-center px-2 py-1.5">' + ((s.description && s.description.title) ? '<button onclick="event.stopPropagation();showDesc(\'' + s.version + '\')" class="text-blue-400 hover:text-blue-300" title="查看策略说明">📖</button>' : '<span class="text-slate-700">—</span>') + '</td>' +
cell('composite', s._composite, v => v + (s._confidence != null && s._confidence < 1 ? '<span class="text-slate-500 text-[10px]">×' + s._confidence.toFixed(2) + '</span>' : ''), 'font-bold text-amber-300') +
cell('universality_score', (st.universality || {}).score, v => v + '<span class="text-slate-500">/' + (st.universality || {}).months + '</span>') +
cell('full_return_pct', (st.portfolio_full || {}).total_return_pct, v => v + '%' + ((st.portfolio_full||{}).std ? '<span class="text-slate-500 text-[10px]">±' + (st.portfolio_full||{}).std + '</span>' : ''), 'font-bold text-amber-300') +
cell('capital_final', (st.portfolio_full||pf).capital_final, v => '¥' + (v/10000).toFixed(0) + '万') +
cell('cagr_pct', (st.portfolio_full||pf).cagr_pct, v => v + '%', hl((st.portfolio_full||pf).cagr_pct, best.cagr_pct)) +
cell('total_trades', st.total_trades, v => ((st.portfolio_full||{}).positions_taken ?? (pf.positions_taken||0)) + '/' + (v > 999 ? Math.round(v/1000) + 'k' : v), '', false) +
cell('avg_hold_days', st.avg_hold_days, v => v + 'd') +
cell('win_rate', st.win_rate, v => v + '%', hl(st.win_rate, best.win_rate)) +
cell('avg_profit_pct', st.avg_profit_pct, v => v + '%', (st.avg_profit_pct || 0) >= 0 ? 'text-green-400' : 'text-red-400') +
cell('sharpe_ratio', st.sharpe_ratio, v => v, hl(st.sharpe_ratio, best.sharpe_ratio)) +
cell('profit_factor', st.profit_factor, v => v, hl(st.profit_factor, best.profit_factor)) +
cell('portfolio_max_dd_pct', (st.portfolio_full||pf).portfolio_max_dd_pct, v => v + '%', hl((st.portfolio_full||pf).portfolio_max_dd_pct, best.portfolio_max_dd_pct, true), true) +
'<td class="text-center px-2 py-1.5" onclick="event.stopPropagation()">' +
'<button onclick="runStrategyBacktest(\'' + s.version + '\')" class="px-2 py-0.5 bg-blue-600/60 hover:bg-blue-500 rounded text-xs">▶回测</button>' +
'</td></tr>';
// 2026-08-11 新增:拆分当前/历史区域(老莫需求)
// 当前 = current=true(实盘/新策略);历史 = 其余,默认折叠
const currentStrats = strategies.filter(s => s.current === true);
const historyStrats = strategies.filter(s => s.current !== true);
const renderGroup = (groupStrats, groupTitle, groupIcon, collapsed) => {
let ghtml = '<div class="overflow-x-auto"><table class="w-full text-xs whitespace-nowrap"><thead><tr class="text-slate-500 border-b border-slate-700">' +
th('version', '版本', 'text-left') + th('name', '名称', 'text-left') +
th('sizing_slots', '仓位', 'text-center') +
'<th class="text-center px-2 py-1.5">说明</th>' +
th('composite', '综合', 'text-right') +
th('universality', '普适', 'text-right') +
th('full_return', '全参与', 'text-right') +
th('capital_final', '最终资产', 'text-right') +
th('cagr', '年化', 'text-right') +
th('trades', '执行数', 'text-right', '实际参与组合模拟的交易笔数;信号总数=为鼠标悬停查看') +
th('hold', '均持仓', 'text-right') +
th('win_rate', '胜率', 'text-right') +
th('avg_profit', '平均收益', 'text-right') + th('sharpe', '夏普', 'text-right') +
th('profit_factor', '盈亏比', 'text-right') + th('max_dd', '资产回撤', 'text-right') +
'<th class="text-center px-2 py-1.5">操作</th></tr></thead><tbody>';
for (const s of groupStrats) {
const st = s.summary_stats || {};
const pf = st.portfolio || {};
const hasResult = st.total_trades != null;
const smallSample = hasResult && st.total_trades < 40;
const isCurrent = s.current === true;
const deprecatedReason = DEPRECATED[s.version];
const rowCls = 'border-b border-slate-800/50 hover:bg-slate-800/30 cursor-pointer' + (isCurrent ? ' bg-emerald-900/20 border-l-2 border-l-emerald-400' : '');
ghtml += '<tr class="' + rowCls + '" onclick="showStrategyDetail(\'' + s.version + '\')">' +
'<td class="px-2 py-1.5 font-mono font-bold ' + (deprecatedReason ? 'text-red-400/70 line-through' : (isCurrent ? 'text-emerald-400' : 'text-blue-400')) + '">' + s.version + (isCurrent ? ' <span class="text-[10px] bg-emerald-500/20 text-emerald-300 px-1 rounded">当前</span>' : '') + (deprecatedReason ? ' <span class="text-[10px] bg-red-500/20 text-red-300 px-1 rounded" title="' + deprecatedReason.replace(/"/g, '&quot;') + '">已证伪</span>' : '') + ((s.market && s.market !== 'all') ? ' <span class="text-[10px] bg-cyan-500/20 text-cyan-300 px-1 rounded">' + (s.market === 'hk' ? '港' : 'A') + '</span>' : '') + '</td>' +
'<td class="px-2 py-1.5 whitespace-normal break-words" title="' + (s.hypothesis || s.summary || '').replace(/"/g, '&quot;') + '">' + (s.name || '') + (smallSample ? ' <span class="text-amber-500" title="样本<40笔">⚠️</span>' : '') + '</td>' +
'<td class="text-center px-2 py-1.5 font-mono text-cyan-300">' + (st.sizing_slots ? st.sizing_slots + '仓' : '10仓') + (st.conviction_model ? '<button onclick="event.stopPropagation();showConviction(\'' + s.version + '\')" class="text-amber-400 hover:text-amber-300" title="查看信念分级仓位规则">⚡</button>' : '') + '</td>' +
'<td class="text-center px-2 py-1.5">' + ((s.description && s.description.title) ? '<button onclick="event.stopPropagation();showDesc(\'' + s.version + '\')" class="text-blue-400 hover:text-blue-300" title="查看策略说明">📖</button>' : '<span class="text-slate-700">—</span>') + '</td>' +
cell('composite', s._composite, v => v + (s._confidence != null && s._confidence < 1 ? '<span class="text-slate-500 text-[10px]">×' + s._confidence.toFixed(2) + '</span>' : ''), 'font-bold text-amber-300') +
cell('universality_score', (st.universality || {}).score, v => v + '<span class="text-slate-500">/' + (st.universality || {}).months + '月</span>') +
cell('full_return_pct', (st.portfolio_full || {}).total_return_pct, v => v + '%' + ((st.portfolio_full||{}).std ? '<span class="text-slate-500 text-[10px]">±' + (st.portfolio_full||{}).std + '</span>' : ''), 'font-bold text-amber-300') +
cell('capital_final', (st.portfolio_full||pf).capital_final, v => '¥' + (v/10000).toFixed(0) + '') +
cell('cagr_pct', (st.portfolio_full||pf).cagr_pct, v => v + '%', hl((st.portfolio_full||pf).cagr_pct, best.cagr_pct)) +
cell('total_trades', st.total_trades, v => ((st.portfolio_full||{}).positions_taken ?? (pf.positions_taken||0)) + '/' + (v > 999 ? Math.round(v/1000) + 'k' : v), '', false) +
cell('avg_hold_days', st.avg_hold_days, v => v + 'd') +
cell('win_rate', st.win_rate, v => v + '%', hl(st.win_rate, best.win_rate)) +
cell('avg_profit_pct', st.avg_profit_pct, v => v + '%', (st.avg_profit_pct || 0) >= 0 ? 'text-green-400' : 'text-red-400') +
cell('sharpe_ratio', st.sharpe_ratio, v => v, hl(st.sharpe_ratio, best.sharpe_ratio)) +
cell('profit_factor', st.profit_factor, v => v, hl(st.profit_factor, best.profit_factor)) +
cell('portfolio_max_dd_pct', (st.portfolio_full||pf).portfolio_max_dd_pct, v => v + '%', hl((st.portfolio_full||pf).portfolio_max_dd_pct, best.portfolio_max_dd_pct, true), true) +
'<td class="text-center px-2 py-1.5" onclick="event.stopPropagation()">' +
'<button onclick="runStrategyBacktest(\'' + s.version + '\')" class="px-2 py-0.5 bg-blue-600/60 hover:bg-blue-500 rounded text-xs">▶回测</button>' +
'</td></tr>';
}
ghtml += '</tbody></table></div>';
return ghtml;
};
// 当前区域(展开)+ 历史区域(默认折叠)
let html = '';
if (currentStrats.length) {
html += '<div class="mb-4">' +
'<div class="flex items-center gap-2 mb-2"><span class="text-sm font-semibold text-emerald-400">🟢 当前使用</span><span class="text-xs text-slate-500">(' + currentStrats.length + '个)</span></div>' +
renderGroup(currentStrats, '当前', '🟢', false) +
'</div>';
}
html += '</tbody></table></div>' +
'<div class="text-xs text-slate-600 mt-2">总收益/最终资产 = 100万本金·最多10等分仓位·24个月组合模拟 · ⚠️样本&lt;40笔 · 绿色=该列最优 · 悬停名称查看改进假设</div>';
if (historyStrats.length) {
html += '<div class="mb-4">' +
'<button class="w-full flex items-center justify-between px-3 py-2 bg-slate-800/50 hover:bg-slate-800 rounded-lg border border-slate-700 text-sm" onclick="this.nextElementSibling.classList.toggle(\'hidden\'); this.querySelector(\'span\').textContent = this.nextElementSibling.classList.contains(\'hidden\') ? \'▶\' : \'▼\'">' +
'<span class="text-slate-400">📚 历史策略</span><span class="text-slate-500 text-xs">(' + historyStrats.length + '个)</span><span class="text-slate-500">▶</span></button>' +
'<div class="hidden mt-2">' + renderGroup(historyStrats, '历史', '📚', true) + '</div>' +
'</div>';
}
if (!currentStrats.length && !historyStrats.length) {
html = '<div class="text-slate-500">暂无策略版本</div>';
}
html += '<div class="text-xs text-slate-600 mt-2">总收益/最终资产 = 100万本金·最多10等分仓位·24个月组合模拟 · ⚠️样本&lt;40笔 · 绿色=该列最优 · 悬停名称查看改进假设</div>';
el.innerHTML = html;
}