feat: 温区表分区(已激活上区+备选下区折叠)+策略评分分级(不合格/待观察/合格/良好/优秀)+状态评分分离+综合排名效率惩罚修复

This commit is contained in:
xxm
2026-08-17 08:19:58 +08:00
parent f59460e206
commit 3245c3b851
+81 -12
View File
@@ -2595,7 +2595,8 @@ function renderStrategyTable(strategies) {
(rw && rw.trades != null ? ' <span class="text-[10px] text-slate-600">' + rw.trades + '笔</span>' : '') +
'</td>' +
'<td class="px-2 py-1.5 font-mono font-bold ' + (deprecatedReason ? 'text-red-400/70 line-through' : (rgActive ? 'text-emerald-400' : 'text-blue-400')) + '">' + s.version + (rgActive ? ' <span class="text-[10px] bg-emerald-500/20 text-emerald-300 px-1 rounded">激活</span>' : '') + (s.state === 'available' && !rgActive ? ' <span class="text-[10px] bg-blue-500/20 text-blue-300 px-1 rounded" title="候选池内,当前温区未激活">可用</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="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>' : '') + ' ' +
(rGrade ? '<span class="text-[10px] px-1 py-0.5 rounded ' + GRADE_STD[rGrade].cls + '" title="评分: ' + GRADE_STD[rGrade].need + '">' + GRADE_STD[rGrade].label + '</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', compositeShow, 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') +
@@ -2623,6 +2624,29 @@ function renderStrategyTable(strategies) {
// ── 2026-08-15 按温区分组:当前温区激活区块改为温区优先(当前温区排最前高亮),
// 每个温区下列出所有在该温区有数据的策略(按该温区胜率降序)──
const REGIME_ORDER = ['trend_up', 'choppy', 'trend_down'];
// ── 2026-08-17 策略评分分级(老莫):基于资格叉数 + 综合分排名 ──
// 不合格=≥2个叉 / 待观察=1个叉 / 合格/良好/优秀=3勾+综合分区间
const GRADE_STD = {
poor: {label: '不合格', cls: 'bg-red-500/20 text-red-300', need: '叉≥2'},
watch: {label: '待观察', cls: 'bg-amber-500/20 text-amber-300', need: '叉=1'},
ok: {label: '合格', cls: 'bg-slate-500/20 text-slate-300', need: '3勾+综合60-74'},
good: {label: '良好', cls: 'bg-emerald-500/25 text-emerald-200', need: '3勾+综合75-84'},
great: {label: '优秀', cls: 'bg-emerald-400/30 text-emerald-100', need: '3勾+综合≥85'},
};
const gradeOf = (qg, comp) => {
if (!qg) return null;
const ok3 = qg.long_ok && qg.mid_ok && qg.short_ok;
const xCount = [qg.long_ok, qg.mid_ok, qg.short_ok].filter(x => !x).length;
if (!ok3) {
if (xCount >= 2) return 'poor';
if (xCount === 1) return 'watch';
return null;
}
if (comp >= 85) return 'great';
if (comp >= 75) return 'good';
return 'ok';
};
const renderRegimeRow = (s, rg) => {
// 2026-08-15 行级激活按策略自身(后端已按所属市场温区算好 s.current),不再按表格 isCur
const rw = (s.regime_winrates || {})[rg] || {};
@@ -2660,15 +2684,27 @@ function renderStrategyTable(strategies) {
const r3 = rLong && rMid && rShort; // 三项全过
const rNearOk = rMid || rShort; // 近期有亮点(含长期不过但近期过:❌❌✅/❌✅✅)
const rManual = s.manual_available !== false;
let rState, rStateCls, rStateBadge, rRowBg;
if (!rManual) { rState = 'manual_off'; rStateCls = 'text-slate-500'; rStateBadge = '<span class="text-[10px] bg-slate-500/20 text-slate-400 px-1 rounded">手动停用</span>'; rRowBg = 'opacity-50'; }
else if (r3) { rState = 'ok3'; rStateCls = 'text-emerald-400'; rStateBadge = (s.current === true) ? '<span class="text-[10px] bg-emerald-500/30 text-emerald-200 px-1 rounded">已激活</span>' : '<span class="text-[10px] bg-emerald-500/20 text-emerald-300 px-1 rounded">可激活</span>'; rRowBg = 'bg-emerald-900/10'; }
else if (rNearOk) { rState = 'ok2'; rStateCls = 'text-blue-400'; rStateBadge = '<span class="text-[10px] bg-blue-500/20 text-blue-300 px-1 rounded">可用</span>'; rRowBg = ''; }
else { rState = 'bad'; rStateCls = 'text-red-400/80'; rStateBadge = '<span class="text-[10px] bg-red-500/20 text-red-300 px-1 rounded">不合格</span>'; rRowBg = 'opacity-60'; }
const isActive = rState === 'ok3';
// ── 2026-08-17 状态与评分分离(老莫):
// 状态 = 已激活(current=true,实盘跑)/未激活(current=false,不跑)
// 评分 = 不合格(≥2叉)/待观察(1叉)/合格/良好/优秀(3勾+综合分区间)
const rGrade = gradeOf(qg, comp);
let rStateCls, rStateBadge, rRowBg;
if (!rManual) { rStateCls = 'text-slate-500'; rStateBadge = '<span class="text-[10px] bg-slate-500/20 text-slate-400 px-1 rounded">手动停用</span>'; rRowBg = 'opacity-50'; }
else if (s.current === true) {
rStateCls = 'text-emerald-400';
rStateBadge = '<span class="text-[10px] bg-emerald-500/40 text-emerald-100 px-1 rounded font-bold">已激活</span>';
rRowBg = 'bg-emerald-900/15';
} else {
rStateCls = 'text-blue-400';
rStateBadge = '<span class="text-[10px] bg-slate-500/20 text-slate-300 px-1 rounded">未激活</span>';
rRowBg = '';
}
const rGradeBadge = rGrade && rGrade !== 'ok' && rGrade !== 'good' && rGrade !== 'great' ? '' : '';
const isActive = s.current === true;
return '<tr class="border-b border-slate-800/50 hover:bg-slate-800/30 cursor-pointer ' + rRowBg + '" onclick="showStrategyDetail(\'' + s.version + '\')">' +
'<td class="px-2 py-1.5 font-mono font-bold ' + rStateCls + '">' + s.version + ' ' + rStateBadge + ((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="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>' : '') + ' ' +
(rGrade ? '<span class="text-[10px] px-1 py-0.5 rounded ' + GRADE_STD[rGrade].cls + '" title="评分: ' + GRADE_STD[rGrade].need + '">' + GRADE_STD[rGrade].label + '</span>' : '') + '</td>' +
// 2026-08-16 资格列:长期/近期/当下 三项达标徽章
'<td class="text-center px-1 py-1.5 whitespace-nowrap" onclick="event.stopPropagation()">' + (s.qualification && s.qualification[rg] ? (
'<span title="长期10y年化' + (s.qualification[rg].cagr_10y != null ? s.qualification[rg].cagr_10y + '%' : '?') + ' vs 大盘' + (s.qualification[rg].bench_10y != null ? s.qualification[rg].bench_10y + '%' : '?') + '">' + (s.qualification[rg].long_ok ? '✅' : '❌') + '</span>' +
@@ -2724,7 +2760,11 @@ function renderStrategyTable(strategies) {
const rn = rw.trades || 0;
const rm = rw.universality ? (rw.universality.months || 0) : 0;
const rconf = Math.min(1, rn / 40) * (rm > 0 ? Math.min(1, rm / 6) : 1);
return Math.round((ret + w + sh + pfc + dd) * rconf);
// 2026-08-17 效率惩罚(与 renderRegimeRow 一致)——否则排序分≠显示分,排名错乱
const _sig = rn, _pos = rw.positions_taken || 0;
const _ratio = _pos > 0 ? _sig / _pos : 99;
const _eff = _ratio <= 2 ? 1.0 : _ratio <= 5 ? 0.9 : _ratio <= 10 ? 0.75 : 0.5;
return Math.round((ret + w + sh + pfc + dd) * rconf * _eff);
}
case 'universality': return (rw.universality || {}).score || 0;
case 'max_dd': return pf.portfolio_max_dd_pct != null ? pf.portfolio_max_dd_pct : 999;
@@ -2762,9 +2802,38 @@ function renderStrategyTable(strategies) {
th('hold', '均持仓', 'text-right', '平均持仓天数', 'hold') + th('cagr', '年化', 'text-right', '年化收益率', 'cagr') + th('composite', '综合', 'text-right', '多维度综合评分', 'composite') +
th('sharpe', '夏普', 'text-right', '每单位波动超额收益', 'sharpe') + th('profit_factor', '盈亏比', 'text-right', '总盈利/总亏损', 'profit_factor') +
th('universality', '普适', 'text-right', '跨独立年份有效性(非特例)', 'universality') + th('max_dd', '回撤', 'text-right', '组合模拟最大回撤', 'max_dd') +
'<th class="text-center px-2 py-1.5">操作</th></tr></thead><tbody>' +
metas.map(s => renderRegimeRow(s, rg)).join('') +
'</tbody></table></div></div>';
'<th class="text-center px-2 py-1.5">操作</th></tr></thead>' +
// ── 2026-08-17 分区(老莫):上区=已激活,下区=备选(资格达标未激活),默认折叠 ──
(() => {
const active = metas.filter(s => s.current === true);
const backup = metas.filter(s => s.current !== true);
let html = '<tbody class="align-top">';
if (active.length) {
html += '<tr class="bg-emerald-900/20"><td colspan="14" class="px-2 py-1 text-[10px] text-emerald-300 font-bold">✅ 已激活 (' + active.length + ')</td></tr>';
html += active.map(s => renderRegimeRow(s, rg)).join('');
}
if (backup.length) {
html += '<tr class="border-t border-slate-700/50"><td colspan="14" class="px-2 py-1 text-[10px] text-slate-400">' +
'<button onclick="toggleBackup(\'' + marketLabel + rg + '\')" class="hover:text-slate-200">' +
'<span id="bk-arrow-' + marketLabel + rg + '">▶</span> 备选 (资格达标未激活, ' + backup.length + ') <span class="text-slate-600">点击展开</span></button></td></tr>';
html += '<tr id="bk-body-' + marketLabel + rg + '" class="hidden">' +
'<td colspan="14" class="p-0">' +
'<table class="w-full"><tbody>' + backup.map(s => renderRegimeRow(s, rg)).join('') + '</tbody></table>' +
'</td></tr>';
}
html += '</tbody></table></div></div>';
return html;
})();
};
// ── 2026-08-17 备选区展开/折叠(老莫)──
window.toggleBackup = function(key) {
const body = document.getElementById('bk-body-' + key);
const arrow = document.getElementById('bk-arrow-' + key);
if (body) {
const hidden = body.classList.contains('hidden');
body.classList.toggle('hidden');
if (arrow) arrow.textContent = hidden ? '▼' : '▶';
}
};
// ── 2026-08-15 按市场×温区展示:6表格 = A股当前市 → 港股当前市 → A股其他2市 → 港股其他2市,
// 每表格列出该市场在该温区有数据的所有可用策略(含未激活,按该温区胜率降序),激活策略高亮;温度并入当前市标题尾部