feat: 研究Tab重构——策略子Tab按适应温区展开多行(每策略每温区一行+温区列+温区胜率/均盈/样本), 当前使用改为当前温区激活(regime_weights.active), 移除组合子Tab(温区动态激活替代固定组合方案), /api/research/strategies返回regime_winrates+regime_active
This commit is contained in:
@@ -540,9 +540,30 @@ def api_research_strategies():
|
||||
from strategy_lab import list_strategies, STRATEGY_DESCRIPTIONS
|
||||
pt = request.args.get('period_tag')
|
||||
strats = list_strategies(period_tag=pt)
|
||||
# 2026-08-13 温区自适应:为每个策略附加各温区表现(strategy_regime_perf)
|
||||
# + 当前温区激活状态(regime_weights)。前端按"适应温区"展开多行。
|
||||
_weights = _load_json(DATA_DIR / "strategy_weights.json", None) or {}
|
||||
_active_set = set(_weights.get("active") or [])
|
||||
_regime_winrates = {}
|
||||
try:
|
||||
import sqlite3 as _sq
|
||||
_c = _sq.connect(str(DATA_DIR / "mofin.db"), timeout=10)
|
||||
_c.execute("PRAGMA busy_timeout=10000")
|
||||
for _r in _c.execute(
|
||||
"SELECT strategy, regime, trades, win_rate, avg_pnl FROM strategy_regime_perf"
|
||||
).fetchall():
|
||||
_regime_winrates.setdefault(_r[0], {})[_r[1]] = {
|
||||
"trades": _r[2], "win_rate": _r[3], "avg_pnl": _r[4]
|
||||
}
|
||||
_c.close()
|
||||
except Exception:
|
||||
pass
|
||||
for s in strats:
|
||||
s['description'] = STRATEGY_DESCRIPTIONS.get(s['version'], {})
|
||||
return jsonify({'strategies': strats})
|
||||
s['regime_winrates'] = _regime_winrates.get(s['version'], {})
|
||||
# current = 当前温区激活(替代旧的 CURRENT_VERSIONS 硬编码)
|
||||
s['current'] = s['version'] in _active_set
|
||||
return jsonify({'strategies': strats, 'regime_active': sorted(_active_set)})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
+56
-309
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
@@ -2111,9 +2111,8 @@ function renderResearch() {
|
||||
if (!el) return;
|
||||
el.innerHTML =
|
||||
'<div class="flex gap-2 items-center mb-3">' +
|
||||
'<button class="rsub-tab-btn px-3 py-1.5 text-sm rounded-lg border border-slate-700 bg-slate-800 text-blue-400" data-rtab="strategy">📊 策略</button>' +
|
||||
'<button class="rsub-tab-btn px-3 py-1.5 text-sm rounded-lg border border-slate-700 text-slate-400" data-rtab="combo">🧬 组合</button>' +
|
||||
'<span class="text-xs text-slate-500">策略=版本迭代对比 · 组合=市场阶段分工方案</span>' +
|
||||
'<span class="text-sm font-semibold text-slate-300">📋 策略研究</span>' +
|
||||
'<span class="text-xs text-slate-500">按温区自适应展开 · 每策略每温区一行 · 🟢=当前温区激活</span>' +
|
||||
'</div>' +
|
||||
'<div id="rsub-strategy">' +
|
||||
'<div id="evo-box" class="mb-3 bg-slate-800/60 border border-slate-700 rounded-lg p-3"><div class="text-xs text-slate-500">🧬 进化模块加载中...</div></div>' +
|
||||
@@ -2128,19 +2127,11 @@ function renderResearch() {
|
||||
'<span class="text-xs text-slate-500">每版策略基于上一版归因分析迭代 · 点击行展开明细</span></div>' +
|
||||
'<div id="strategyList"><div class="text-slate-500 text-sm">加载中...</div></div>' +
|
||||
'<div id="strategyDetail" class="mt-4"></div>' +
|
||||
'</div>' +
|
||||
'<div id="rsub-combo" class="hidden"></div>';
|
||||
// 子Tab切换
|
||||
el.querySelectorAll('.rsub-tab-btn').forEach(btn => {
|
||||
btn.onclick = () => {
|
||||
el.querySelectorAll('.rsub-tab-btn').forEach(b => { b.classList.remove('bg-slate-800', 'text-blue-400'); b.classList.add('text-slate-400'); });
|
||||
btn.classList.add('bg-slate-800', 'text-blue-400'); btn.classList.remove('text-slate-400');
|
||||
const target = btn.dataset.rtab;
|
||||
el.querySelectorAll('[id^="rsub-"]').forEach(div => div.classList.add('hidden'));
|
||||
document.getElementById('rsub-' + target).classList.remove('hidden');
|
||||
if (target === 'combo') renderResearchCombo();
|
||||
};
|
||||
});
|
||||
'</div>';
|
||||
// 首次渲染即加载策略列表(无子Tab切换)
|
||||
loadStrategyList();
|
||||
loadEvolutionBox();
|
||||
loadStrategyRegimeBanner();
|
||||
// 恢复用户上次选择的周期(防自动刷新重置,2026-07-30老爸抓包)
|
||||
const savedPt = window._btPeriodVal || '2y';
|
||||
const ptSel = document.getElementById('btPeriod');
|
||||
@@ -2162,6 +2153,8 @@ async function loadStrategyList() {
|
||||
const resp = await fetch('/api/research/strategies?period_tag=' + pt);
|
||||
const data = await resp.json();
|
||||
if (data.error) { listEl.innerHTML = '<div class="text-red-400">' + data.error + '</div>'; return; }
|
||||
// 2026-08-13 温区激活策略集合(供策略表高亮)
|
||||
window._regimeActive = data.regime_active || [];
|
||||
const mkt = document.getElementById('btMarket')?.value || 'all';
|
||||
let strats = data.strategies || [];
|
||||
// 市场语义:'all'=未分市场的A股回测(港股回填前跑的),'hk'=港股宇宙,'a'=显式A股
|
||||
@@ -2264,274 +2257,6 @@ async function loadStrategyRegimeBanner() {
|
||||
}
|
||||
new MutationObserver(()=>{if(!document.getElementById('tab-research')?.classList?.contains('hidden'))loadStrategyRegimeBanner();}).observe(document.body,{attributes:true,subtree:true});
|
||||
|
||||
// ══════════════════════════════════════════════════════
|
||||
// 组合方案子Tab(2026-08-02 新增)
|
||||
// 展示: 当前组合方案(v_next4趋势+v_mr超跌, regime分工) + 组合回测版本 + 组合自我进化
|
||||
// ══════════════════════════════════════════════════════
|
||||
async function renderResearchCombo() {
|
||||
const el = document.getElementById('rsub-combo');
|
||||
if (!el) return;
|
||||
el.innerHTML = '<div class="text-slate-500 text-sm">🧬 组合方案加载中...</div>';
|
||||
try {
|
||||
const r = await fetch('/api/evolution/combo');
|
||||
const d = await r.json();
|
||||
if (d.error) { el.innerHTML = '<div class="text-red-400">' + d.error + '</div>'; return; }
|
||||
|
||||
let h = '';
|
||||
|
||||
// ── 1. 当前市场阶段 + 组合方案路由卡片 ──
|
||||
const regime = d.regime || {};
|
||||
const regName = {trend_up:'📈 趋势市', choppy:'〰️ 震荡市', trend_down:'📉 下跌市'}[regime.regime] || (regime.regime || '未知');
|
||||
const regColor = regime.regime === 'trend_up' ? 'text-emerald-400' : regime.regime === 'choppy' ? 'text-amber-400' : 'text-red-400';
|
||||
h += '<div class="bg-slate-800/60 border border-slate-700 rounded-lg p-3 mb-3">' +
|
||||
'<div class="flex items-center justify-between mb-2">' +
|
||||
'<span class="text-base font-semibold text-blue-300">🧬 当前组合方案</span>' +
|
||||
'<span class="text-sm">市场阶段: <span class="' + regColor + ' font-bold">' + regName + '</span>' + (regime.adx ? ' <span class="text-slate-500 text-xs">ADX=' + regime.adx + '</span>' : '') + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="text-xs text-slate-400 mb-2">当前组合(2026-08-11):v_weak(弱市超跌确认,实盘)+ p_oversold(预测超跌反弹,新策略)+ 12维重评(持仓管理)——选股与持仓管理分离,超跌家族并行</div>' +
|
||||
'<div class="space-y-1.5">';
|
||||
for (const rt of (d.routing || [])) {
|
||||
const active = regime.regime === rt.regime;
|
||||
h += '<div class="flex items-center gap-2 text-sm ' + (active ? 'bg-blue-900/30 border border-blue-500/40 rounded px-2 py-1.5' : 'text-slate-500 px-2 py-1') + '">' +
|
||||
'<span class="w-24">' + ({trend_up:'📈 趋势市', choppy:'〰️ 震荡市', trend_down:'📉 下跌市'}[rt.regime] || rt.regime) + '</span>' +
|
||||
'<span class="font-mono font-bold ' + (active ? 'text-emerald-400' : '') + '">' + rt.active + '</span>' +
|
||||
'<span class="text-slate-400 flex-1">' + rt.action + ' · ' + rt.desc + '</span>' +
|
||||
(active ? '<span class="text-xs bg-emerald-500/20 text-emerald-300 px-1.5 py-0.5 rounded">当前生效</span>' : '') +
|
||||
'</div>';
|
||||
}
|
||||
h += '</div></div>';
|
||||
|
||||
// ── 1.5 温区自适应横幅(2026-08-13)──
|
||||
const rw = d.regime_weights || null;
|
||||
if (rw && rw.state) {
|
||||
const bandIcon = {panic:'🔴', fear:'🟠', neutral:'🟡', greed:'🟢', euphoria:'🔵'}[rw.temp_band] || '⚪';
|
||||
const bandName = {panic:'恐慌', fear:'偏冷', neutral:'中性', greed:'偏热', euphoria:'亢奋'}[rw.temp_band] || rw.temp_band;
|
||||
const activeList = (rw.active || []).slice(0, 10);
|
||||
const allList = Object.entries(rw.weights || {});
|
||||
h += '<div class="bg-indigo-900/25 border border-indigo-500/40 rounded-lg p-3 mb-3">' +
|
||||
'<div class="flex items-center justify-between flex-wrap gap-2 mb-2">' +
|
||||
'<span class="text-sm font-semibold text-indigo-300">🌡️ 温区自适应 · 当前激活</span>' +
|
||||
'<span class="text-sm">平滑温区: <span class="font-bold ' + regColor + '">' + regName + '</span>' +
|
||||
' · 温度: <b>' + bandIcon + ' ' + bandName + '</b>' +
|
||||
(rw.temp_rsi ? ' <span class="text-slate-500 text-xs">RSI=' + rw.temp_rsi + '</span>' : '') +
|
||||
(rw.state_date ? ' <span class="text-slate-600 text-xs">(' + rw.state_date + ')</span>' : '') + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="text-xs text-slate-400 mb-2">策略全温区发信号,适用温区由实测胜率动态决定(strategy_regime_perf);组合按当前温区自动激活,选股/买卖/重评已温区感知</div>' +
|
||||
'<div class="flex flex-wrap gap-1.5 mb-2">';
|
||||
if (activeList.length) {
|
||||
for (const n of activeList) {
|
||||
const w = rw.weights && rw.weights[n];
|
||||
const wr = w && w.best_win_rate != null ? w.best_win_rate + '%' : '';
|
||||
h += '<span class="text-xs bg-emerald-500/15 text-emerald-300 border border-emerald-500/30 rounded px-2 py-0.5">' +
|
||||
'<span class="font-mono font-bold">' + n + '</span> ' + wr + ' <span class="text-emerald-500/70">✓</span></span>';
|
||||
}
|
||||
} else {
|
||||
h += '<span class="text-xs text-slate-500">当前温区无激活策略</span>';
|
||||
}
|
||||
h += '</div>';
|
||||
// 观察中策略(权重<0.5 或未匹配)
|
||||
const obsList = allList.filter(([k,v]) => !v.matched).slice(0, 8);
|
||||
if (obsList.length) {
|
||||
h += '<div class="flex flex-wrap gap-1.5">' + obsList.map(([k,v]) =>
|
||||
'<span class="text-[11px] bg-slate-800 text-slate-500 border border-slate-700 rounded px-2 py-0.5">' +
|
||||
'<span class="font-mono">' + k + '</span> ' + (v.best_win_rate != null ? v.best_win_rate + '%' : '') + ' 观察</span>'
|
||||
).join('') + '</div>';
|
||||
}
|
||||
h += '</div>';
|
||||
}
|
||||
|
||||
// ── 2. 组合成员策略指标 ──
|
||||
const members = d.members || {};
|
||||
if (Object.keys(members).length) {
|
||||
h += '<div class="text-sm font-bold text-slate-300 mb-1.5">成员策略(10年回测,组合收益已扣费)</div>' +
|
||||
'<div class="grid grid-cols-2 gap-2 mb-3">';
|
||||
for (const [v, m] of Object.entries(members)) {
|
||||
const u = m.universality || {};
|
||||
const yearly5 = m.positions_taken_5slot && m.positions_taken_5slot > 0 ? Math.round(m.positions_taken_5slot / 10) : '—';
|
||||
h += '<div class="bg-slate-800/50 rounded-lg p-3">' +
|
||||
'<div class="flex items-center justify-between mb-1.5"><span class="font-mono font-bold text-base text-' + (v === 'v_next4' ? 'emerald-400' : 'amber-300') + '">' + v + (m.is_sel ? ' <span class="text-[11px] bg-emerald-500/20 text-emerald-300 px-1.5 rounded ml-1 align-middle">✅推荐</span>' : '') + '</span>' +
|
||||
'<span class="text-xs text-slate-400">' + (m.role || '') + '</span></div>' +
|
||||
// 盈利率大字突出(用户关注的核心指标)
|
||||
'<div class="flex items-end gap-4 mb-2">' +
|
||||
'<div><div class="text-xs text-slate-400">单笔均收益(毛利)</div><div class="font-mono font-bold text-lg ' + ((m.avg_profit_pct || 0) >= 0 ? 'text-green-400' : 'text-red-400') + '">' + (m.avg_profit_pct != null ? m.avg_profit_pct + '%' : '—') + '</div></div>' +
|
||||
'<div><div class="text-xs text-slate-400">组合年化(扣费)</div><div class="font-mono font-bold text-lg text-amber-200">' + (m.cagr_pct != null ? m.cagr_pct + '%' : '—') + '</div></div>' +
|
||||
'<div><div class="text-xs text-slate-400">组合10年收益</div><div class="font-mono font-bold text-lg text-amber-200">' + (m.return_pct != null ? m.return_pct + '%' : '—') + '</div></div>' +
|
||||
'</div>' +
|
||||
'<div class="grid grid-cols-4 gap-1 mb-1 text-center text-xs">' +
|
||||
'<div><div class="text-slate-500 text-xs">胜率</div><div class="font-mono">' + (m.win_rate != null ? m.win_rate + '%' : '—') + '</div></div>' +
|
||||
'<div><div class="text-slate-500 text-xs">信号总数</div><div class="font-mono">' + (m.trades != null ? (m.trades > 999 ? Math.round(m.trades/1000) + 'k' : m.trades) : '—') + '</div></div>' +
|
||||
'<div><div class="text-slate-500 text-xs">组合执行</div><div class="font-mono">' + (m.positions_taken_5slot != null ? m.positions_taken_5slot + '笔' : '—') + '</div></div>' +
|
||||
'<div><div class="text-slate-500 text-xs">覆盖月</div><div class="font-mono">' + (u.months || '—') + '</div></div>' +
|
||||
'</div>' +
|
||||
'<div class="text-xs text-slate-400">均持' + (m.avg_hold_days != null ? m.avg_hold_days + '天' : '—') + ' · 回撤' + (m.max_dd_pct != null ? m.max_dd_pct + '%' : '—') + ' · 组合年均' + yearly5 + '笔(人工可操作)</div>' +
|
||||
(v === 'v_next4' ? '<div class="text-[13px] text-amber-300/90 mt-1.5 border-t border-slate-800 pt-1">⚠️ <b>适用池:系统跟踪池(267只,科创板/沪市为主)</b>——2026-08-04验证:v_next4全部历史回测跑在跟踪池上(当时stock_daily未全市场扩表),全市场复跑失效(wr 35.9%/组合-34.3%)。edge=池子策展×v8.1评分×信念叠乘,不可外推全市场。实盘范围=跟踪池,池内数字有效。</div>' : '') +
|
||||
(v === 'v_mr' ? '<div class="text-[13px] text-slate-300 mt-1.5 border-t border-slate-800 pt-1">✅ <b>v_weak 弱市策略</b>(2026-08-03 六步方法论+12维框架定稿):大盘弱市(MA20下)+ADX∈[25,30]甜区 + 个股bias60∈[-35,-20]深超跌 + RSI≤25极度超卖 + 5日急跌确认;出场tp30%/sl12%/40日。回测:234笔/单笔avg+7.17%/胜率62.4%/去2018仍+8.02%/正贡献8/11年/<b>组合8槽收益178.3%/年化12.8%/回撤16.4%</b>(每月≤30信号,等权8槽,收益/回撤比10.9)。<b>关键认知:弱市深超跌反弹的edge前提=大盘ADX∈[25,30]</b>(v_mr 缺失的大盘前置条件),负面剔除(过深/RSI偏高/阴跌)比正面排序更有效。</div>' : '') +
|
||||
'</div>';
|
||||
}
|
||||
h += '</div>';
|
||||
}
|
||||
|
||||
// ── 3. 组合回测版本列表 ──
|
||||
const combos = d.combos || [];
|
||||
h += '<div class="text-sm font-bold text-slate-300 mb-1.5">组合方案版本(回测记录)</div>';
|
||||
if (combos.length) {
|
||||
h += '<div class="overflow-x-auto mb-2"><table class="w-full text-xs"><thead><tr class="text-slate-500 border-b border-slate-700">' +
|
||||
'<th class="text-left px-2 py-1">版本</th><th class="text-left px-2 py-1">名称</th>' +
|
||||
'<th class="text-right px-2 py-1">区间</th><th class="text-right px-2 py-1">执行数</th>' +
|
||||
'<th class="text-right px-2 py-1">胜率</th><th class="text-right px-2 py-1">平均收益</th>' +
|
||||
'<th class="text-right px-2 py-1">年化</th><th class="text-right px-2 py-1">回撤</th>' +
|
||||
'<th class="text-right px-2 py-1">普适</th></tr></thead><tbody>';
|
||||
for (const c of combos) {
|
||||
const st = c.summary_stats || {};
|
||||
const pf = st.portfolio_full || {};
|
||||
const u = st.universality || {};
|
||||
const name = c.version === 'v_combo' ? 'v8.1波段+B20动量' : c.version;
|
||||
h += '<tr class="border-b border-slate-800/40">' +
|
||||
'<td class="px-2 py-1 font-mono font-bold text-blue-400">' + c.version + '</td>' +
|
||||
'<td class="px-2 py-1 whitespace-normal break-words">' + name + '</td>' +
|
||||
'<td class="text-right px-2 py-1 font-mono text-slate-500">' + (c.period_tag || '') + '</td>' +
|
||||
'<td class="text-right px-2 py-1 font-mono">' + ((pf.positions_taken || 0)) + '/' + (st.total_trades != null ? (st.total_trades > 999 ? Math.round(st.total_trades/1000) + 'k' : st.total_trades) : '—') + '</td>' +
|
||||
'<td class="text-right px-2 py-1 font-mono">' + (st.win_rate != null ? st.win_rate + '%' : '—') + '</td>' +
|
||||
'<td class="text-right px-2 py-1 font-mono ' + ((st.avg_profit_pct || 0) >= 0 ? 'text-green-400' : 'text-red-400') + '">' + (st.avg_profit_pct != null ? st.avg_profit_pct + '%' : '—') + '</td>' +
|
||||
'<td class="text-right px-2 py-1 font-mono text-amber-200">' + (pf.cagr_pct != null ? pf.cagr_pct + '%' : '—') + '</td>' +
|
||||
'<td class="text-right px-2 py-1 font-mono">' + (pf.portfolio_max_dd_pct != null ? pf.portfolio_max_dd_pct + '%' : '—') + '</td>' +
|
||||
'<td class="text-right px-2 py-1 font-mono">' + (u.score != null ? u.score + '/' + (u.months || 0) + '月' : '—') + '</td>' +
|
||||
'</tr>';
|
||||
}
|
||||
h += '</tbody></table></div>';
|
||||
h += '<div class="text-xs text-slate-600 mb-3">⚠️ v_combo 为历史组合实验:2年+47.9%但5年验证2023/24震荡转折年亏损(B动量族假突破),未采纳——当前组合采用 v_next4+v_mr regime 分工方案</div>';
|
||||
} else {
|
||||
h += '<div class="text-xs text-slate-600 mb-3">暂无组合回测记录</div>';
|
||||
}
|
||||
|
||||
// ── 4. 组合自我进化 ──
|
||||
h += '<div class="bg-slate-800/60 border border-slate-700 rounded-lg p-3 mt-3">' +
|
||||
'<div class="flex items-center justify-between mb-2"><span class="text-base font-semibold text-emerald-400">🧬 组合方案自我进化</span><button onclick="renderResearchCombo()" class="text-sm text-blue-400 hover:text-blue-300">刷新</button></div>' +
|
||||
'<div class="text-sm text-slate-400">组合方案 = 策略分工路由(regime → 策略),进化方向:</div>' +
|
||||
'<ul class="text-sm text-slate-300 mt-1.5 space-y-1">' +
|
||||
'<li>① 市场阶段判定参数(MA20/ADX 阈值)随实盘验证迭代</li>' +
|
||||
'<li>② 各成员策略独立进化(v_next4 参数迭代见策略Tab)</li>' +
|
||||
'<li>③ 组合权重/接管时机按回测与实盘偏差调优</li>' +
|
||||
'</ul>' +
|
||||
'<div class="text-xs text-slate-500 mt-2">📌 组合级进化记录待接入:当前进化系统只跟踪单策略(strategy_evolution 表),组合路由的自动调优需要新的 evolution 数据表。</div>' +
|
||||
'</div>';
|
||||
|
||||
el.innerHTML = h;
|
||||
} catch(e) {
|
||||
el.innerHTML = '<div class="text-red-400">组合方案加载失败: ' + e.message + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function showDesc(version) {
|
||||
const s = (window._strats || []).find(x => x.version === version);
|
||||
const d = s && s.description;
|
||||
if (!d || !d.title) { showToast('暂无说明'); return; }
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm';
|
||||
modal.onclick = () => modal.remove();
|
||||
modal.innerHTML = `
|
||||
<div class="bg-slate-900 border border-slate-700 rounded-xl max-w-2xl w-full mx-4 p-6 shadow-2xl" onclick="event.stopPropagation()">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<h3 class="text-lg font-bold text-blue-300">${d.title}</h3>
|
||||
<button onclick="this.closest('.fixed').remove()" class="text-slate-500 hover:text-white text-xl leading-none">×</button>
|
||||
</div>
|
||||
<div class="space-y-4 text-sm text-slate-300">
|
||||
<div><div class="text-xs font-semibold text-blue-400 mb-1">算法</div><p class="leading-relaxed">${d.algorithm || ''}</p></div>
|
||||
<div><div class="text-xs font-semibold text-blue-400 mb-1">思路</div><p class="leading-relaxed">${d.rationale || ''}</p></div>
|
||||
<div><div class="text-xs font-semibold text-blue-400 mb-1">证据</div><p class="leading-relaxed">${d.evidence || ''}</p></div>
|
||||
</div>
|
||||
</div>`;
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
function showConviction(version) {
|
||||
const s = (window._strats || []).find(x => x.version === version);
|
||||
const st = (s && s.summary_stats) || {};
|
||||
const model = st.conviction_model;
|
||||
if (!model) { showToast('该策略无信念分级仓位'); return; }
|
||||
const slots = st.sizing_slots || 10;
|
||||
const isStack = model.includes('叠乘');
|
||||
// boost分布(从trades统计在前端没有, 用summary里的说明)
|
||||
const rules = isStack ? [
|
||||
['动量基因 DNA', '收缩突破形态(ATR处20日最低1/4位+破20日新高+量比>1.2)', '×2'],
|
||||
['行业确认牛', '行业板块ADX>25(行业级趋势强化)', '×2'],
|
||||
['资金加速度', 'flow_delta>0(主力资金净流入在加速)', '×2'],
|
||||
] : [
|
||||
['动量基因 DNA', '收缩突破形态(ATR处20日最低1/4位+破20日新高+量比>1.2)', '任一命中 ×2'],
|
||||
['行业趋势强', '行业板块ADX≥20', '任一命中 ×2'],
|
||||
['资金加速度', 'flow_delta>0(主力资金净流入在加速)', '任一命中 ×2'],
|
||||
];
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm';
|
||||
modal.onclick = () => modal.remove();
|
||||
modal.innerHTML = `
|
||||
<div class="bg-slate-900 border border-slate-700 rounded-xl max-w-lg w-full mx-4 p-6 shadow-2xl" onclick="event.stopPropagation()">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<h3 class="text-lg font-bold text-amber-300">⚡ 信念分级仓位</h3>
|
||||
<button onclick="this.closest('.fixed').remove()" class="text-slate-500 hover:text-white text-xl leading-none">×</button>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-slate-300">
|
||||
<div class="bg-slate-800/60 rounded-lg px-3 py-2 font-mono text-xs text-amber-200">${model}</div>
|
||||
<div class="text-xs text-slate-400">基准 ${slots} 仓(总资金分${slots}份,每份一票)。命中信念因子的票按规则放大单票仓位:</div>
|
||||
${rules.map(r => `<div class="flex items-start gap-2 border-b border-slate-800 pb-2">
|
||||
<div class="text-amber-400 font-semibold whitespace-nowrap w-28">${r[0]}</div>
|
||||
<div class="flex-1 text-xs text-slate-400">${r[1]}</div>
|
||||
<div class="text-emerald-400 font-mono font-bold">${r[2]}</div>
|
||||
</div>`).join('')}
|
||||
<div class="text-xs text-slate-500">${isStack ? '多个因子同时命中则叠乘(如 DNA+行业牛 = ×4),封顶 ×4;均不命中 ×1(基准仓)。' : '多个因子同时命中不叠乘,统一 ×2;均不命中 ×1(基准仓)。'}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
function showToast(msg) {
|
||||
const t = document.createElement('div');
|
||||
t.className = 'fixed top-4 right-4 z-50 bg-slate-800 border border-slate-600 rounded-lg px-4 py-2 text-sm text-slate-200 shadow-lg';
|
||||
t.textContent = msg;
|
||||
document.body.appendChild(t);
|
||||
setTimeout(() => t.remove(), 2000);
|
||||
}
|
||||
|
||||
function sortBy(key) {
|
||||
if (window._sortKey === key) {
|
||||
window._sortDir = window._sortDir === 'desc' ? 'asc' : 'desc';
|
||||
} else {
|
||||
window._sortKey = key;
|
||||
window._sortDir = ASC_DEFAULT.has(key) ? 'asc' : 'desc';
|
||||
}
|
||||
renderStrategyTable(window._strats || []);
|
||||
}
|
||||
|
||||
function sortStrategies(strategies, key, dir) {
|
||||
const get = (s) => {
|
||||
const st = s.summary_stats || {}; const pf = st.portfolio || {};
|
||||
switch(key) {
|
||||
case 'version': return s.version || '';
|
||||
case 'name': return s.name || '';
|
||||
case 'sizing_slots': return st.sizing_slots || 10;
|
||||
case 'composite': return s._composite != null ? s._composite : -999;
|
||||
case 'universality': return (st.universality || {}).score || 0;
|
||||
case 'total_return': return pf.total_return_pct != null ? pf.total_return_pct : -999;
|
||||
case 'full_return': return (st.portfolio_full || {}).total_return_pct != null ? st.portfolio_full.total_return_pct : -999;
|
||||
case 'capital_final': return (st.portfolio_full || pf).capital_final || 0;
|
||||
case 'cagr': return (st.portfolio_full || pf).cagr_pct != null ? (st.portfolio_full || pf).cagr_pct : -999;
|
||||
case 'trades': return st.total_trades || 0;
|
||||
case 'hold': return st.avg_hold_days || 0;
|
||||
case 'win_rate': return st.win_rate != null ? st.win_rate : -999;
|
||||
case 'avg_profit': return st.avg_profit_pct != null ? st.avg_profit_pct : -999;
|
||||
case 'sharpe': return st.sharpe_ratio != null ? st.sharpe_ratio : -999;
|
||||
case 'profit_factor': return st.profit_factor != null ? st.profit_factor : -999;
|
||||
case 'max_dd': return (st.portfolio_full || pf).portfolio_max_dd_pct != null ? (st.portfolio_full || pf).portfolio_max_dd_pct : 999;
|
||||
default: return 0;
|
||||
}
|
||||
};
|
||||
const arr = [...strategies];
|
||||
arr.sort((a, b) => {
|
||||
const va = get(a), vb = get(b);
|
||||
let cmp = (typeof va === 'string') ? va.localeCompare(vb) : va - vb;
|
||||
return dir === 'desc' ? -cmp : cmp;
|
||||
});
|
||||
return arr;
|
||||
}
|
||||
|
||||
function renderStrategyTable(strategies) {
|
||||
const el = document.getElementById('strategyList');
|
||||
if (!strategies.length) { el.innerHTML = '<div class="text-slate-500">暂无策略版本</div>'; return; }
|
||||
@@ -2623,12 +2348,14 @@ function renderStrategyTable(strategies) {
|
||||
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>`;
|
||||
};
|
||||
// 2026-08-11 新增:拆分当前/历史区域(老莫需求)
|
||||
// 当前 = current=true(实盘/新策略);历史 = 其余,默认折叠
|
||||
// 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 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">' +
|
||||
'<th class="text-left px-2 py-1.5">适应温区</th>' +
|
||||
th('version', '版本', 'text-left') + th('name', '名称', 'text-left') +
|
||||
th('sizing_slots', '仓位', 'text-center') +
|
||||
'<th class="text-center px-2 py-1.5">说明</th>' +
|
||||
@@ -2650,27 +2377,47 @@ function renderStrategyTable(strategies) {
|
||||
const smallSample = hasResult && st.total_trades < 40;
|
||||
const isCurrent = s.current === true;
|
||||
const deprecatedReason = DEPRECATED[s.version];
|
||||
const rowCls = 'border-b border-slate-800/50 hover:bg-slate-800/30 cursor-pointer' + (isCurrent ? ' bg-emerald-900/20 border-l-2 border-l-emerald-400' : '');
|
||||
ghtml += '<tr class="' + rowCls + '" onclick="showStrategyDetail(\'' + s.version + '\')">' +
|
||||
'<td class="px-2 py-1.5 font-mono font-bold ' + (deprecatedReason ? 'text-red-400/70 line-through' : (isCurrent ? 'text-emerald-400' : 'text-blue-400')) + '">' + s.version + (isCurrent ? ' <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 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>' +
|
||||
cell('composite', s._composite, 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') +
|
||||
cell('universality_score', (st.universality || {}).score, v => v + '<span class="text-slate-500">/' + (st.universality || {}).months + '月</span>') +
|
||||
cell('full_return_pct', (st.portfolio_full || {}).total_return_pct, v => v + '%' + ((st.portfolio_full||{}).std ? '<span class="text-slate-500 text-[10px]">±' + (st.portfolio_full||{}).std + '</span>' : ''), 'font-bold text-amber-300') +
|
||||
cell('capital_final', (st.portfolio_full||pf).capital_final, v => '¥' + (v/10000).toFixed(0) + '万') +
|
||||
cell('cagr_pct', (st.portfolio_full||pf).cagr_pct, v => v + '%', hl((st.portfolio_full||pf).cagr_pct, best.cagr_pct)) +
|
||||
cell('total_trades', st.total_trades, v => ((st.portfolio_full||{}).positions_taken ?? (pf.positions_taken||0)) + '/' + (v > 999 ? Math.round(v/1000) + 'k' : v), '', false) +
|
||||
cell('avg_hold_days', st.avg_hold_days, v => v + 'd') +
|
||||
cell('win_rate', st.win_rate, v => v + '%', hl(st.win_rate, best.win_rate)) +
|
||||
cell('avg_profit_pct', st.avg_profit_pct, v => v + '%', (st.avg_profit_pct || 0) >= 0 ? 'text-green-400' : 'text-red-400') +
|
||||
cell('sharpe_ratio', st.sharpe_ratio, v => v, hl(st.sharpe_ratio, best.sharpe_ratio)) +
|
||||
cell('profit_factor', st.profit_factor, v => v, hl(st.profit_factor, best.profit_factor)) +
|
||||
cell('portfolio_max_dd_pct', (st.portfolio_full||pf).portfolio_max_dd_pct, v => v + '%', hl((st.portfolio_full||pf).portfolio_max_dd_pct, best.portfolio_max_dd_pct, true), true) +
|
||||
'<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>';
|
||||
// 温区展开:有 regime_winrates 则每温区一行;无则单行("整体")
|
||||
const rws = s.regime_winrates || {};
|
||||
const regimes = Object.keys(rws).length ? Object.keys(rws).sort() : ['overall'];
|
||||
for (const rg of regimes) {
|
||||
const rw = rws[rg];
|
||||
const isOverall = rg === 'overall';
|
||||
const rgMeta = REGIME_META[rg] || {icon:'🌐', name: rg === 'overall' ? '整体' : rg};
|
||||
const rgActive = !isOverall && isCurrent && (window._regimeActive || []).includes(s.version);
|
||||
// 温区行数据:该温区胜率/均盈/样本(有则覆盖,无则用整体)
|
||||
const wrShow = rw && rw.win_rate != null ? rw.win_rate : (isOverall ? st.win_rate : null);
|
||||
const pnlShow = rw && rw.avg_pnl != null ? rw.avg_pnl : (isOverall ? st.avg_profit_pct : null);
|
||||
const tradesShow = rw && rw.trades != null ? rw.trades : (isOverall ? st.total_trades : null);
|
||||
const rowCls = 'border-b border-slate-800/50 hover:bg-slate-800/30 cursor-pointer' + (isCurrent && !isOverall ? ' bg-emerald-900/20 border-l-2 border-l-emerald-400' : '');
|
||||
ghtml += '<tr class="' + rowCls + '" onclick="showStrategyDetail(\'' + s.version + '\')">' +
|
||||
'<td class="px-2 py-1.5 whitespace-nowrap">' +
|
||||
(isOverall ? '<span class="text-slate-500">整体</span>' :
|
||||
'<span title="' + rgMeta.name + '">' + rgMeta.icon + ' ' + rgMeta.name + '</span>') +
|
||||
(rw && rw.win_rate != null ? ' <span class="text-[10px] text-slate-500">胜率' + rw.win_rate + '%</span>' : '') +
|
||||
(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' : (isCurrent ? 'text-emerald-400' : 'text-blue-400')) + '">' + s.version + (isCurrent ? ' <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 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>' +
|
||||
cell('composite', s._composite, 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') +
|
||||
cell('universality_score', (st.universality || {}).score, v => v + '<span class="text-slate-500">/' + (st.universality || {}).months + '月</span>') +
|
||||
cell('full_return_pct', (st.portfolio_full || {}).total_return_pct, v => v + '%' + ((st.portfolio_full||{}).std ? '<span class="text-slate-500 text-[10px]">±' + (st.portfolio_full||{}).std + '</span>' : ''), 'font-bold text-amber-300') +
|
||||
cell('capital_final', (st.portfolio_full||pf).capital_final, v => '¥' + (v/10000).toFixed(0) + '万') +
|
||||
cell('cagr_pct', (st.portfolio_full||pf).cagr_pct, v => v + '%', hl((st.portfolio_full||pf).cagr_pct, best.cagr_pct)) +
|
||||
cell('total_trades', tradesShow, v => (v > 999 ? Math.round(v/1000) + 'k' : v) + (isOverall ? '' : ''), '', false) +
|
||||
cell('avg_hold_days', st.avg_hold_days, v => v + 'd') +
|
||||
cell('win_rate', wrShow, v => v + '%', (isOverall ? hl(wrShow, best.win_rate) : (wrShow >= 50 ? 'text-green-400' : 'text-red-400'))) +
|
||||
cell('avg_profit_pct', pnlShow, v => v + '%', (pnlShow || 0) >= 0 ? 'text-green-400' : 'text-red-400') +
|
||||
cell('sharpe_ratio', st.sharpe_ratio, v => v, hl(st.sharpe_ratio, best.sharpe_ratio)) +
|
||||
cell('profit_factor', st.profit_factor, v => v, hl(st.profit_factor, best.profit_factor)) +
|
||||
cell('portfolio_max_dd_pct', (st.portfolio_full||pf).portfolio_max_dd_pct, v => v + '%', hl((st.portfolio_full||pf).portfolio_max_dd_pct, best.portfolio_max_dd_pct, true), true) +
|
||||
'<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>';
|
||||
}
|
||||
}
|
||||
ghtml += '</tbody></table></div>';
|
||||
return ghtml;
|
||||
@@ -2679,7 +2426,7 @@ function renderStrategyTable(strategies) {
|
||||
let html = '';
|
||||
if (currentStrats.length) {
|
||||
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) +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user