feat: 研究Tab当前温区激活区块按温区分组——当前温区排最前高亮,组内按该温区胜率降序,每策略一行(该温区数据),API返回regime_state
This commit is contained in:
@@ -736,7 +736,8 @@ def api_research_strategies():
|
|||||||
_entry['state_reason'] = '当前温区路由激活'
|
_entry['state_reason'] = '当前温区路由激活'
|
||||||
_entry['note'] = f"({pt or '当前周期'}无回测记录,显示10y数据)"
|
_entry['note'] = f"({pt or '当前周期'}无回测记录,显示10y数据)"
|
||||||
strats.append(_entry)
|
strats.append(_entry)
|
||||||
return jsonify({'strategies': strats, 'regime_active': sorted(_active_set)})
|
return jsonify({'strategies': strats, 'regime_active': sorted(_active_set),
|
||||||
|
'regime_state': _weights.get('state', 'unknown')})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|||||||
+65
-2
@@ -2179,6 +2179,7 @@ async function loadStrategyList() {
|
|||||||
if (data.error) { listEl.innerHTML = '<div class="text-red-400">' + data.error + '</div>'; return; }
|
if (data.error) { listEl.innerHTML = '<div class="text-red-400">' + data.error + '</div>'; return; }
|
||||||
// 2026-08-13 温区激活策略集合(供策略表高亮)
|
// 2026-08-13 温区激活策略集合(供策略表高亮)
|
||||||
window._regimeActive = data.regime_active || [];
|
window._regimeActive = data.regime_active || [];
|
||||||
|
window._regimeState = data.regime_state || 'unknown';
|
||||||
const mkt = document.getElementById('btMarket')?.value || 'all';
|
const mkt = document.getElementById('btMarket')?.value || 'all';
|
||||||
let strats = data.strategies || [];
|
let strats = data.strategies || [];
|
||||||
// 市场语义:'all'=未分市场的A股回测(港股回填前跑的),'hk'=港股宇宙,'a'=显式A股
|
// 市场语义:'all'=未分市场的A股回测(港股回填前跑的),'hk'=港股宇宙,'a'=显式A股
|
||||||
@@ -2496,12 +2497,74 @@ function renderStrategyTable(strategies) {
|
|||||||
ghtml += '</tbody></table></div>';
|
ghtml += '</tbody></table></div>';
|
||||||
return ghtml;
|
return ghtml;
|
||||||
};
|
};
|
||||||
|
// ── 2026-08-15 按温区分组:当前温区激活区块改为温区优先(当前温区排最前高亮),
|
||||||
|
// 每个温区下列出所有在该温区有数据的策略(按该温区胜率降序)──
|
||||||
|
const REGIME_ORDER = ['trend_up', 'choppy', 'trend_down'];
|
||||||
|
const renderRegimeRow = (s, rg, isCur) => {
|
||||||
|
const rw = (s.regime_winrates || {})[rg] || {};
|
||||||
|
const rwPf = rw.portfolio || null;
|
||||||
|
const wr = rw.win_rate != null ? rw.win_rate : null;
|
||||||
|
const pnl = rw.avg_pnl != null ? rw.avg_pnl : null;
|
||||||
|
const tr = rw.trades != null ? rw.trades : null;
|
||||||
|
const smallSample = tr != null && tr < 40;
|
||||||
|
let comp = null;
|
||||||
|
if (rwPf) {
|
||||||
|
const ret = Math.min(rwPf.total_return_pct || 0, 100) / 100 * 30;
|
||||||
|
const w = (wr || 0) / 100 * 20;
|
||||||
|
const sh = Math.min(Math.max((rwPf.sharpe_ratio || 0), 0), 20) / 20 * 20;
|
||||||
|
const pfc = Math.min((rwPf.profit_factor || 0), 5) / 5 * 15;
|
||||||
|
const dd = (1 - Math.min(rwPf.portfolio_max_dd_pct || 0, 50) / 50) * 15;
|
||||||
|
comp = Math.round((ret + w + sh + pfc + dd) * (s._confidence || 1));
|
||||||
|
}
|
||||||
|
const cagr = rwPf ? rwPf.cagr_pct : null;
|
||||||
|
const ddV = rwPf ? rwPf.portfolio_max_dd_pct : null;
|
||||||
|
const univ = rw.universality ? rw.universality.score : null;
|
||||||
|
const univM = rw.universality ? (rw.universality.months + '/' + (rw.universality.regime_total_months || '?')) : null;
|
||||||
|
return '<tr class="border-b border-slate-800/50 hover:bg-slate-800/30 cursor-pointer' + (isCur ? ' bg-emerald-900/10' : '') + '" onclick="showStrategyDetail(\'' + s.version + '\')">' +
|
||||||
|
'<td class="px-2 py-1.5 font-mono font-bold ' + (isCur ? 'text-emerald-400' : 'text-blue-400') + '">' + s.version + (isCur ? ' <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, '"') + '">' + (s.name || '') + (smallSample ? ' <span class="text-amber-500" title="样本<40笔">⚠️</span>' : '') + '</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) +
|
||||||
|
cell('avg_hold_days', rw.avg_hold_days != null ? rw.avg_hold_days : null, v => v + 'd') +
|
||||||
|
cell('cagr_pct', cagr, v => v + '%', 'font-bold text-amber-300') +
|
||||||
|
cell('composite', comp, v => v, 'font-bold text-amber-300') +
|
||||||
|
cell('universality_score', univ, v => v + '<span class="text-slate-500">/' + (univM || '?') + '月</span>') +
|
||||||
|
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>';
|
||||||
|
};
|
||||||
|
const renderGroupByRegime = (strats, currentRegime) => {
|
||||||
|
const order = [currentRegime, ...REGIME_ORDER.filter(r => r !== currentRegime)];
|
||||||
|
let g = '';
|
||||||
|
for (const rg of order) {
|
||||||
|
const metas = strats
|
||||||
|
.filter(s => (s.regime_winrates || {})[rg] && (s.regime_winrates || {})[rg].trades != null)
|
||||||
|
.sort((x, y) => ((y.regime_winrates || {})[rg].win_rate || -1) - ((x.regime_winrates || {})[rg].win_rate || -1));
|
||||||
|
if (!metas.length) continue;
|
||||||
|
const isCur = rg === currentRegime;
|
||||||
|
const meta = REGIME_META[rg] || {icon: '🌐', name: rg};
|
||||||
|
g += '<div class="mb-3 rounded-lg border ' + (isCur ? 'border-emerald-700/60 bg-emerald-900/10' : 'border-slate-700') + '">' +
|
||||||
|
'<div class="flex items-center gap-2 px-3 py-1.5 text-sm ' + (isCur ? 'text-emerald-300' : 'text-slate-400') + '">' +
|
||||||
|
meta.icon + ' ' + meta.name + (isCur ? ' <span class="text-[10px] bg-emerald-500/20 text-emerald-300 px-1 rounded">当前温区</span>' : '') +
|
||||||
|
'<span class="text-xs text-slate-500">(' + metas.length + '个策略)</span>' +
|
||||||
|
'<span class="text-[10px] text-slate-600">' + (isCur ? '当前温区匹配即激活' : '该温区下有数据(参考)') + '</span></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('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('universality', '普适', 'text-right') + th('max_dd', '回撤', 'text-right') +
|
||||||
|
'<th class="text-center px-2 py-1.5">操作</th></tr></thead><tbody>' +
|
||||||
|
metas.map(s => renderRegimeRow(s, rg, isCur)).join('') +
|
||||||
|
'</tbody></table></div></div>';
|
||||||
|
}
|
||||||
|
return g;
|
||||||
|
};
|
||||||
// 当前区域(展开)+ 历史区域(默认折叠)
|
// 当前区域(展开)+ 历史区域(默认折叠)
|
||||||
let html = '';
|
let html = '';
|
||||||
if (currentStrats.length) {
|
if (currentStrats.length) {
|
||||||
html += '<div class="mb-4">' +
|
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>' +
|
'<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) +
|
renderGroupByRegime(currentStrats, window._regimeState) +
|
||||||
'</div>';
|
'</div>';
|
||||||
}
|
}
|
||||||
if (availableStrats.length) {
|
if (availableStrats.length) {
|
||||||
|
|||||||
Reference in New Issue
Block a user