feat: 策略三状态模型——派生(不存储)激活/可用未激活/不可用+前端三分组展示(不可用淡显折叠)+可用徽章,与候选池(regime_perf+hk_strategies)对齐
This commit is contained in:
@@ -628,6 +628,7 @@ def api_research_strategies():
|
||||
_c2.close()
|
||||
except Exception:
|
||||
pass
|
||||
_candidate_pool |= set(_regime_winrates.keys())
|
||||
for s in strats:
|
||||
# 2026-08-15 温区加权综合/普适:整体行的综合/普适 = 按温区时长占比加权各温区表现
|
||||
# (业务逻辑:策略只在特定温区激活,评估应该限定在温区范围才值得参考)
|
||||
@@ -684,6 +685,19 @@ def api_research_strategies():
|
||||
s['current'] = s['version'] in _active_set
|
||||
# 2026-08-15 证伪数据驱动:从 strategy_research.deprecated 读(替代前端硬编码)
|
||||
s['deprecated'] = _deprecated.get(s['version'])
|
||||
# 2026-08-15 三状态(派生,不存储):激活/可用未激活/不可用
|
||||
if s['version'] in _active_set:
|
||||
s['state'] = 'active'
|
||||
s['state_reason'] = '当前温区路由激活'
|
||||
elif s.get('deprecated'):
|
||||
s['state'] = 'unavailable'
|
||||
s['state_reason'] = '已证伪: ' + str(s['deprecated'])[:80]
|
||||
elif s['version'] in _candidate_pool:
|
||||
s['state'] = 'available'
|
||||
s['state_reason'] = '候选池内,当前温区未激活(温区切换即可激活)'
|
||||
else:
|
||||
s['state'] = 'unavailable'
|
||||
s['state_reason'] = '不在候选池(历史版本,路由不会选中)'
|
||||
|
||||
# 2026-08-13 关键修复:激活策略必须始终在列表中(即使该 period 无回测记录)
|
||||
# 否则 2y 等默认周期下激活策略缺失(只显示 v_oversold)。
|
||||
@@ -712,6 +726,8 @@ def api_research_strategies():
|
||||
}
|
||||
_entry['regime_winrates'] = _regime_winrates.get(_mv, {})
|
||||
_entry['current'] = True
|
||||
_entry['state'] = 'active'
|
||||
_entry['state_reason'] = '当前温区路由激活'
|
||||
_entry['note'] = f"({pt or '当前周期'}无回测记录,显示10y数据)"
|
||||
strats.append(_entry)
|
||||
return jsonify({'strategies': strats, 'regime_active': sorted(_active_set)})
|
||||
|
||||
+14
-6
@@ -2401,7 +2401,8 @@ function renderStrategyTable(strategies) {
|
||||
// 2026-08-13 温区自适应:按"适应温区"展开——每策略每温区一行
|
||||
// 当前 = 当前温区激活(后端 s.current 已改为 regime_weights.active 判断);历史 = 其余,默认折叠
|
||||
const currentStrats = strategies.filter(s => s.current === true);
|
||||
const historyStrats = strategies.filter(s => s.current !== true);
|
||||
const availableStrats = strategies.filter(s => s.current !== true && s.state === 'available');
|
||||
const unavailableStrats = strategies.filter(s => s.current !== true && s.state !== 'available');
|
||||
const REGIME_META = {trend_up:{icon:'📈',name:'趋势市'}, choppy:{icon:'〰️',name:'震荡市'}, trend_down:{icon:'📉',name:'下跌市'}};
|
||||
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">' +
|
||||
@@ -2471,7 +2472,7 @@ function renderStrategyTable(strategies) {
|
||||
(rw && rw.avg_pnl != null ? ' <span class="text-[10px] ' + (rw.avg_pnl >= 0 ? 'text-green-500/70' : 'text-red-500/70') + '">' + (rw.avg_pnl >= 0 ? '+' : '') + rw.avg_pnl + '%</span>' : '') +
|
||||
(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>' : '') + (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 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="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>' +
|
||||
@@ -2503,14 +2504,21 @@ function renderStrategyTable(strategies) {
|
||||
renderGroup(currentStrats, '当前', '🟢', false) +
|
||||
'</div>';
|
||||
}
|
||||
if (historyStrats.length) {
|
||||
if (availableStrats.length) {
|
||||
html += '<div class="mb-4">' +
|
||||
'<button class="w-full flex items-center justify-between px-3 py-2 bg-slate-800/50 hover:bg-slate-800 rounded-lg border border-slate-700 text-sm" onclick="this.nextElementSibling.classList.toggle(\'hidden\'); this.querySelector(\'span\').textContent = this.nextElementSibling.classList.contains(\'hidden\') ? \'▶\' : \'▼\'">' +
|
||||
'<span class="text-slate-400">📚 历史策略</span><span class="text-slate-500 text-xs">(' + historyStrats.length + '个)</span><span class="text-slate-500">▶</span></button>' +
|
||||
'<div class="hidden mt-2">' + renderGroup(historyStrats, '历史', '📚', true) + '</div>' +
|
||||
'<span class="text-blue-300">🔵 可用未激活</span><span class="text-slate-500 text-xs">(' + availableStrats.length + '个)</span><span class="text-slate-500">▶</span></button>' +
|
||||
'<div class="hidden mt-2">' + renderGroup(availableStrats, '可用', '🔵', true) + '</div>' +
|
||||
'</div>';
|
||||
}
|
||||
if (!currentStrats.length && !historyStrats.length) {
|
||||
if (unavailableStrats.length) {
|
||||
html += '<div class="mb-4 opacity-70">' +
|
||||
'<button class="w-full flex items-center justify-between px-3 py-2 bg-slate-800/50 hover:bg-slate-800 rounded-lg border border-slate-700 text-sm" onclick="this.nextElementSibling.classList.toggle(\'hidden\'); this.querySelector(\'span\').textContent = this.nextElementSibling.classList.contains(\'hidden\') ? \'▶\' : \'▼\'">' +
|
||||
'<span class="text-slate-400">📚 不可用(已证伪 / 历史版本)</span><span class="text-slate-500 text-xs">(' + unavailableStrats.length + '个)</span><span class="text-slate-500">▶</span></button>' +
|
||||
'<div class="hidden mt-2">' + renderGroup(unavailableStrats, '不可用', '📚', true) + '</div>' +
|
||||
'</div>';
|
||||
}
|
||||
if (!currentStrats.length && !availableStrats.length && !unavailableStrats.length) {
|
||||
html = '<div class="text-slate-500">暂无策略版本</div>';
|
||||
}
|
||||
html += '<div class="text-xs text-slate-600 mt-2">总收益/最终资产 = 100万本金·最多10等分仓位·24个月组合模拟 · ⚠️样本<40笔 · 绿色=该列最优 · 悬停名称查看改进假设</div>';
|
||||
|
||||
Reference in New Issue
Block a user