feat: 策略表表头加ⓘ§按钮——ⓘ显示该列含义(columns spec),§显示模块spec

This commit is contained in:
xxm
2026-08-17 02:13:03 +08:00
parent 726c8e0002
commit aa053b5b22
+57 -20
View File
@@ -32,6 +32,9 @@ body { background: #0f0f13; color: #e2e8f0; }
.spec-btn.ai { color: #d29922; background: rgba(210,153,34,0.1); }
.spec-btn.ai:hover { background: rgba(210,153,34,0.25); }
.spec-btns { display: inline-flex; gap: 4px; }
.spec-btn.colinfo { color: #58a6ff; background: rgba(88,166,255,0.1); }
.spec-btn.colinfo:hover { background: rgba(88,166,255,0.25); }
.spec-th-help { margin-left: 3px; display: inline-flex; gap: 3px; vertical-align: middle; }
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: #0f0f13; }
::-webkit-scrollbar-thumb { background: #333; border-radius: 3px; }
@@ -1198,6 +1201,39 @@ function closeSpecModal() {
if (el) { el.classList.add('hidden'); el.classList.remove('flex'); }
}
function showColumnHelp(colKey) {
// 显示策略表某列的含义说明(数据源:specs/strategy.json human_help.columns[colKey]
const cacheKey = 'strategy:human';
const cached = _specCache[cacheKey];
const modal = document.getElementById('specModal');
modal.classList.remove('hidden');
modal.classList.add('flex');
document.getElementById('specModalTitle').textContent = '加载中...';
document.getElementById('specModalSubtitle').textContent = '';
document.getElementById('specModalBody').innerHTML = '<div class="text-slate-500">正在获取列说明...</div>';
const render = function(spec) {
const cols = (spec && spec.human_help && spec.human_help.columns) || {};
const col = cols[colKey];
if (!col) {
document.getElementById('specModalBody').innerHTML = `<div class="text-red-400">未找到列说明: ${colKey}</div>`;
return;
}
document.getElementById('specModalTitle').textContent = col.label || colKey;
document.getElementById('specModalSubtitle').textContent = '列说明 · 这列是干嘛的';
let html = '<div class="mb-4"><div class="text-xs font-semibold text-[#58a6ff] mb-1">这列是什么</div><p class="text-[#c9d1d9]">' + (col.desc || '—') + '</p></div>';
if (col.see) html += '<div class="mb-2"><div class="text-xs font-semibold text-[#3fb950] mb-1">怎么读 / 注意</div><p class="text-[#8b949e]">' + col.see + '</p></div>';
html += '<div class="text-[10px] text-slate-600 mt-4">来源:specs/strategy.json → human_help.columns.' + colKey + 'SSOT</div>';
document.getElementById('specModalBody').innerHTML = html;
};
if (cached) { render(cached); }
else {
fetchJSON('/api/module-spec/strategy').then(spec => {
if (spec && typeof spec === 'object') { _specCache[cacheKey] = spec; render(spec); }
else document.getElementById('specModalBody').innerHTML = '<div class="text-slate-500">无规范数据</div>';
});
}
}
function showModuleHelp(module, type) {
const cacheKey = module + ':' + type;
const cached = _specCache[cacheKey];
@@ -2469,9 +2505,10 @@ function renderStrategyTable(strategies) {
const sty = bar(c, v, invert);
return `<td class="${cls}" style="${sty}">${v != null ? fmt(v) : '—'}</td>`;
};
const th = (key, label, cls, title) => {
const th = (key, label, cls, title, colKey) => {
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>`;
const helpBtns = colKey ? '<span class="spec-th-help"><span class="spec-btn colinfo" title="看这列是干嘛的" onclick="event.stopPropagation();showColumnHelp('' + colKey + '')">ⓘ</span><span class="spec-btn ai" title="看策略模块spec(接口/约束)" onclick="event.stopPropagation();showModuleHelp('strategy','ai')">§</span></span>' : '';
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}${helpBtns}${arrow ? ' ' + arrow : ''}</th>`;
};
// 2026-08-13 温区自适应:按"适应温区"展开——每策略每温区一行
// 当前 = 当前温区激活(后端 s.current 已改为 regime_weights.active 判断);历史 = 其余,默认折叠
@@ -2479,19 +2516,19 @@ function renderStrategyTable(strategies) {
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 class="text-left px-2 py-1.5">适应温区</th>' +
th('version', '版本', 'text-left') + th('name', '名称', 'text-left') +
th('version', '版本', 'text-left', '策略版本标识', 'version') + th('name', '名称', 'text-left', '策略人话名称', 'name') +
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('composite', '综合', 'text-right', '多维度综合评分', 'composite') +
th('universality', '普适', 'text-right', '跨独立年份有效性(非特例)', 'universality') +
th('full_return', '全参与', 'text-right', '全参与模拟总收益', 'full_return') +
th('capital_final', '最终资产', 'text-right', '组合模拟期末资产', 'capital_final') +
th('cagr', '年化', 'text-right', '年化收益率', 'cagr') +
th('trades', '执行数', 'text-right', '实际参与组合模拟的交易笔数;信号总数=为鼠标悬停查看', 'trades') +
th('hold', '均持仓', 'text-right', '平均持仓天数', 'hold') +
th('win_rate', '胜率', 'text-right', '盈利笔数占比', 'win_rate') +
th('avg_profit', '平均收益', 'text-right', '每笔平均收益率', 'avg_profit') + th('sharpe', '夏普', 'text-right', '每单位波动超额收益', 'sharpe') +
th('profit_factor', '盈亏比', 'text-right', '总盈利/总亏损', 'profit_factor') + th('max_dd', '资产回撤', 'text-right', '组合模拟最大回撤', 'max_dd') +
'<th class="text-center px-2 py-1.5">操作</th></tr></thead><tbody>';
for (const s of groupStrats) {
const st = s.summary_stats || {};
@@ -2701,13 +2738,13 @@ function renderStrategyTable(strategies) {
'<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', '实际入场笔数 / 触发信号数(组合模拟10等分仓位下实际成交)') +
th('hold', '均持仓', 'text-right') + th('cagr', '年化', 'text-right') + th('composite', '综合', 'text-right') +
th('sharpe', '夏普', 'text-right') + th('profit_factor', '盈亏比', 'text-right') +
th('universality', '普适', 'text-right') + th('max_dd', '回撤', 'text-right') +
th('version', '版本', 'text-left', '策略版本标识', 'version') + th('name', '名称', 'text-left', '策略人话名称', 'name') +
'<th class="text-center px-2 py-1.5" title="长期10y/近期2y/当下1y 适应温区年化 vs 大盘">资格<span class="spec-th-help"><span class="spec-btn colinfo" title="看这列是干嘛的" onclick="event.stopPropagation();showColumnHelp(\'qualification\')">ⓘ</span><span class="spec-btn ai" title="看策略模块spec" onclick="event.stopPropagation();showModuleHelp(\'strategy\',\'ai\')">§</span></span></th>' +
'<th class="text-center px-2 py-1.5">可用<span class="spec-th-help"><span class="spec-btn colinfo" title="看这列是干嘛的" onclick="event.stopPropagation();showColumnHelp(\'manual_available\')">ⓘ</span><span class="spec-btn ai" title="看策略模块spec" onclick="event.stopPropagation();showModuleHelp(\'strategy\',\'ai\')">§</span></span></th>' +
th('win_rate', '胜率', 'text-right', '盈利笔数占比', 'win_rate') + th('avg_profit', '平均收益', 'text-right', '每笔平均收益率', 'avg_profit') + th('trades', '成交/信号', 'text-right', '实际入场笔数 / 触发信号数(组合模拟10等分仓位下实际成交)', 'trades') +
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>';