feat: 前端资格列(长期/近期/当下)+手动可用性开关+批量可用/不可用按钮+大盘基准阈值显示+警告突出

This commit is contained in:
xxm
2026-08-16 07:49:13 +08:00
parent 861c5a4a61
commit a942db51bf
+55 -1
View File
@@ -2193,6 +2193,15 @@ async function loadStrategyList() {
// 市场语义:'all'=未分市场的A股回测(港股回填前跑的),'hk'=港股宇宙,'a'=显式A股
if (mkt === 'hk') strats = strats.filter(s => (s.market || 'all') === 'hk');
else if (mkt === 'a') strats = strats.filter(s => (s.market || 'all') !== 'hk');
// 2026-08-16 大盘基准(从任一策略 qualification 提取,供温区表头显示)
for (const s of strats) {
if (s.qualification && s.qualification.bench_1y != null) {
window._bench1y = s.qualification.bench_1y;
window._bench2y = s.qualification.bench_2y;
window._bench10y = s.qualification.bench_10y;
break;
}
}
renderStrategyTable(strats);
} catch(e) {
listEl.innerHTML = '<div class="text-red-400">加载失败: ' + e.message + '</div>';
@@ -2509,6 +2518,19 @@ function renderStrategyTable(strategies) {
return '<tr class="border-b border-slate-800/50 hover:bg-slate-800/30 cursor-pointer' + (isActive ? ' bg-emerald-900/10' : '') + '" onclick="showStrategyDetail(\'' + s.version + '\')">' +
'<td class="px-2 py-1.5 font-mono font-bold ' + (isActive ? 'text-emerald-400' : 'text-blue-400') + '">' + s.version + (isActive ? ' <span class="text-[10px] bg-emerald-500/20 text-emerald-300 px-1 rounded">激活</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>' +
// 2026-08-16 资格列:长期/近期/当下 三项达标徽章
'<td class="text-center px-1 py-1.5 whitespace-nowrap" onclick="event.stopPropagation()">' + (s.qualification ? (
'<span title="长期10y年化' + (s.qualification.cagr_10y != null ? s.qualification.cagr_10y + '%' : '?') + ' vs 大盘' + (s.qualification.bench_10y != null ? s.qualification.bench_10y + '%' : '?') + '">' + (s.qualification.long_ok ? '✅' : '❌') + '</span>' +
'<span title="近期2y年化' + (s.qualification.cagr_2y != null ? s.qualification.cagr_2y + '%' : '?') + ' vs 大盘' + (s.qualification.bench_2y != null ? s.qualification.bench_2y + '%' : '?') + '">' + (s.qualification.mid_ok ? '✅' : '❌') + '</span>' +
'<span title="当下1y年化' + (s.qualification.cagr_1y != null ? s.qualification.cagr_1y + '%' : '?') + ' vs 大盘' + (s.qualification.bench_1y != null ? s.qualification.bench_1y + '%' : '?') + '">' + (s.qualification.short_ok ? '✅' : '❌') + '</span>'
) : '<span class="text-slate-600">—</span>') +
(s.qual_warning ? '<div class="text-[9px] ' + (s.qual_warning.indexOf('回升') >= 0 ? 'text-green-400' : 'text-amber-400') + '">' + s.qual_warning + '</div>' : '') +
'</td>' +
// 2026-08-16 可用性开关(手动把关)
'<td class="text-center px-1 py-1.5" onclick="event.stopPropagation()">' +
'<input type="checkbox" ' + (s.manual_available !== false ? 'checked' : '') +
' onclick="toggleAvail(\'' + s.version + '\', this.checked)" title="手动可用性(激活只能是可用的策略)" class="accent-emerald-500 cursor-pointer">' +
'</td>' +
cell('win_rate', wr, v => v + '%', wr != null && wr >= 50 ? 'text-green-400' : 'text-red-400') +
cell('avg_profit_pct', pnl, v => v + '%', (pnl || 0) >= 0 ? 'text-green-400' : 'text-red-400') +
cell('total_trades', tr, v => v, '', false) +
@@ -2521,6 +2543,30 @@ function renderStrategyTable(strategies) {
cell('portfolio_max_dd_pct', ddV, v => v + '%', '') +
'<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-16 手动可用性:切换单个策略(激活只能是可用的策略)
async function toggleAvail(version, checked) {
try {
await fetch('/api/research/availability', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({version: version, available: checked, note: '手动切换'})
});
loadStrategyList(); // 刷新(state 可能变化)
} catch(e) { console.error('toggleAvail failed', e); }
}
// 2026-08-16 批量设置可用性(当前温区表全部)
async function batchAvail(rg, available) {
const list = (window._strats || [])
.filter(s => s.best_regime === rg && (s.market || 'all') !== 'hk')
.map(s => s.version);
if (!list.length) return;
try {
await fetch('/api/research/availability?batch=1', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({versions: list, available: available, note: '批量' + (available ? '可用' : '不可用')})
});
loadStrategyList();
} catch(e) { console.error('batchAvail failed', e); }
}
const renderRegimeTable = (strats, rg, isCur, marketLabel, tempHtml) => {
// 2026-08-15 方案A:策略只在适应温区(best_regime)表显示一行——所有列都是该策略在适应温区的数据
// 2026-08-16 温区感知排序:尊重 window._sortKey(点列头排序),字段从 regime_winrates[rg] 取
@@ -2572,9 +2618,17 @@ function renderStrategyTable(strategies) {
'<div class="flex items-center gap-2 px-3 py-1.5 text-sm ' + (isCur ? 'text-emerald-300' : 'text-slate-400') + '">' +
marketLabel + ' · ' + meta.icon + ' ' + meta.name + (isCur ? ' <span class="text-[10px] bg-emerald-500/20 text-emerald-300 px-1 rounded">当前温区</span>' : '') + (tempHtml || '') +
'<span class="text-xs text-slate-500">(' + metas.length + '个策略)</span>' +
'<span class="text-[10px] text-slate-600">' + (isCur ? '当前温区匹配即激活' : '策略适应温区(best_regime') + '</span></div>' +
'<span class="text-[10px] text-slate-600">' + (isCur ? '当前温区匹配即激活' : '策略适应温区(best_regime') + '</span>' +
'<span class="text-[10px] text-slate-500 ml-auto">大盘基准: 1y' + (window._bench1y != null ? window._bench1y + '%' : '?') +
' 2y' + (window._bench2y != null ? window._bench2y + '%' : '?') +
' 10y' + (window._bench10y != null ? window._bench10y + '%' : '?') + '</span>' +
'<button onclick="batchAvail(\'' + rg + '\', true)" class="text-[10px] px-1.5 py-0.5 bg-emerald-500/20 text-emerald-300 rounded hover:bg-emerald-500/30">✅全可用</button>' +
'<button onclick="batchAvail(\'' + rg + '\', false)" class="text-[10px] px-1.5 py-0.5 bg-red-500/20 text-red-300 rounded hover:bg-red-500/30">🚫全不可用</button>' +
'</div>' +
'<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 class="text-center px-2 py-1.5" title="长期10y/近期2y/当下1y 适应温区年化 vs 大盘">资格</th>' +
'<th class="text-center px-2 py-1.5">可用</th>' +
th('win_rate', '胜率', 'text-right') + th('avg_profit', '平均收益', 'text-right') + th('trades', '笔数', 'text-right') +
th('hold', '均持仓', 'text-right') + th('cagr', '年化', 'text-right') + th('composite', '综合', 'text-right') +
th('sharpe', '夏普', 'text-right') + th('profit_factor', '盈亏比', 'text-right') +