feat: 温区表分区(已激活上区+备选下区折叠)+策略评分分级(不合格/待观察/合格/良好/优秀)+状态评分分离+综合排名效率惩罚修复
This commit is contained in:
+81
-12
@@ -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, '"') + '">已证伪</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, '"') + '">' + (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, '"') + '">' + (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, '"') + '">' + (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, '"') + '">' + (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市,
|
||||
// 每表格列出该市场在该温区有数据的所有可用策略(含未激活,按该温区胜率降序),激活策略高亮;温度并入当前市标题尾部
|
||||
|
||||
Reference in New Issue
Block a user