feat: 研究Tab组合方案展示 — 市场阶段分工(v_next4趋势+v_mr超跌)可视化

- 后端: evolution_api.py 新增 get_combo_dashboard() (regime+组合回测+成员指标+routing)
- 后端: server.py 新增 /api/evolution/combo 路由
- 前端: 研究Tab加子Tab(策略/组合), 组合Tab展示当前regime路由+成员策略10y指标+v_combo回测列表+组合自我进化
- 前端: 策略表格名称列启用换行(whitespace-normal), 消除长名称撑出水平滚动条
- 修复: market_regime 查询用 date 排序(id列不存在)
This commit is contained in:
xxm
2026-08-02 23:17:34 +08:00
parent 95ad45c096
commit 98236c440c
3 changed files with 229 additions and 12 deletions
+76
View File
@@ -68,6 +68,82 @@ def get_evolution_dashboard():
}
def get_combo_dashboard():
"""组合方案 Dashboard 数据 (2026-08-02 新增)
返回: 当前组合方案(v_next4+v_mr按regime分工) + 组合回测版本(v_combo) + 市场阶段
"""
conn = sqlite3.connect(DB)
conn.row_factory = sqlite3.Row
# 1. 当前市场阶段 (market_regime)
regime = None
r = conn.execute("SELECT * FROM market_regime ORDER BY date DESC LIMIT 1").fetchone()
if r:
regime = dict(r)
# 2. 组合回测版本 (v_combo 家族)
combos = []
rows = conn.execute(
"SELECT id, version, market, period_tag, created_at, results_json"
" FROM strategy_research WHERE version LIKE '%combo%' OR version LIKE 'v_combo%'"
" ORDER BY id DESC"
).fetchall()
for r in rows:
d = dict(r)
res = json.loads(d.pop("results_json") or "{}")
s = res.get("summary", {})
pf = s.get("portfolio_full", {})
p5 = s.get("portfolio", {})
d["summary_stats"] = {
"total_trades": s.get("total_trades"),
"win_rate": s.get("win_rate"),
"avg_profit_pct": s.get("avg_profit_pct"),
"avg_hold_days": s.get("avg_hold_days"),
"sharpe_ratio": s.get("sharpe_ratio"),
"profit_factor": s.get("profit_factor"),
"universality": s.get("universality", {}),
"portfolio": p5,
"portfolio_full": pf,
}
combos.append(d)
# 3. 组合成员策略的独立指标
members = {}
for v in ["v_next4", "v_mr"]:
r = conn.execute(
"SELECT results_json FROM strategy_research"
" WHERE version=? AND period_tag='10y' ORDER BY id DESC LIMIT 1",
(v,),
).fetchone()
if r:
res = json.loads(r[0])
s = res.get("summary", {})
pf = s.get("portfolio_full", {})
members[v] = {
"role": "趋势市主战" if v == "v_next4" else "震荡/下跌市接管",
"trades": s.get("total_trades"),
"win_rate": s.get("win_rate"),
"avg_profit_pct": s.get("avg_profit_pct"),
"avg_hold_days": s.get("avg_hold_days"),
"cagr_pct": pf.get("cagr_pct"),
"return_pct": pf.get("total_return_pct"),
"max_dd_pct": pf.get("portfolio_max_dd_pct"),
"universality": s.get("universality", {}),
}
conn.close()
return {
"regime": regime,
"combos": combos,
"members": members,
"routing": [
{"regime": "trend_up", "active": "v_next4", "action": "追涨买入/加仓放行", "desc": "大盘MA20上方+ADX强, 趋势追涨主战场"},
{"regime": "choppy", "active": "v_mr", "action": "追涨降级为关注", "desc": "震荡市, 超跌反弹主战场, 趋势追涨让位"},
{"regime": "trend_down", "active": "v_mr", "action": "禁止追涨", "desc": "深超跌主战场, 只做均值回复"},
],
}
def get_health_trend(version='v_next4', days=30):
"""健康度趋势"""
conn = sqlite3.connect(DB)
+11
View File
@@ -1823,6 +1823,17 @@ def api_evolution_health_trend():
return jsonify({'error': str(e)}), 500
@app.route("/api/evolution/combo")
def api_evolution_combo():
"""组合方案 Dashboard (v_next4+v_mr regime分工 + v_combo回测)"""
try:
sys.path.insert(0, '/home/hmo/MoFin/evolution')
from evolution_api import get_combo_dashboard
return jsonify(get_combo_dashboard())
except Exception as e:
return jsonify({'error': str(e)}), 500
if __name__ == "__main__":
port = int(os.environ.get("PORT", 8899))
print(f"🚀 MoFin Dashboard → http://0.0.0.0:{port}")
+133 -3
View File
@@ -1929,7 +1929,14 @@ function inMd(t) {
function renderResearch() {
const el = document.getElementById('tab-research');
if (!el) return;
el.innerHTML = '<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>' +
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>' +
'</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>' +
'<div class="flex gap-2 items-center mb-3 flex-wrap">' +
'<h2 class="text-lg font-bold">📋 策略研究 — 版本迭代对比</h2>' +
'<select id="btPeriod" onchange="saveBtPeriod()" class="bg-slate-800 text-sm rounded-lg px-2 py-1 border border-slate-700">' +
@@ -1939,7 +1946,20 @@ function renderResearch() {
'<option value="all">全部市场</option><option value="a">A股</option><option value="hk">港股</option></select>' +
'<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 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();
};
});
// 恢复用户上次选择的周期(防自动刷新重置,2026-07-30老爸抓包)
const savedPt = window._btPeriodVal || '2y';
const ptSel = document.getElementById('btPeriod');
@@ -2024,6 +2044,116 @@ async function loadEvolutionBox() {
// 研究Tab活跃时加载进化模块
new MutationObserver(()=>{if(!document.getElementById('tab-research')?.classList?.contains('hidden'))loadEvolutionBox();}).observe(document.body,{attributes:true,subtree:true});
// ══════════════════════════════════════════════════════
// 组合方案子Tab2026-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">v_next4 趋势追涨 + v_mr 均值回复,按大盘市场阶段自动分工(strategy_lifecycle 6.5 实盘落地)</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-[10px] bg-emerald-500/20 text-emerald-300 px-1.5 py-0.5 rounded">当前生效</span>' : '') +
'</div>';
}
h += '</div></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 || {};
h += '<div class="bg-slate-800/50 rounded-lg p-2.5">' +
'<div class="flex items-center justify-between"><span class="font-mono font-bold text-' + (v === 'v_next4' ? 'emerald-400' : 'amber-300') + '">' + v + '</span>' +
'<span class="text-[10px] text-slate-500">' + (m.role || '') + '</span></div>' +
'<div class="grid grid-cols-4 gap-1 mt-1.5 text-center text-xs">' +
'<div><div class="text-slate-500 text-[10px]">年化</div><div class="font-mono text-amber-200">' + (m.cagr_pct != null ? m.cagr_pct + '%' : '—') + '</div></div>' +
'<div><div class="text-slate-500 text-[10px]">胜率</div><div class="font-mono">' + (m.win_rate != null ? m.win_rate + '%' : '—') + '</div></div>' +
'<div><div class="text-slate-500 text-[10px]">笔数</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-[10px]">覆盖</div><div class="font-mono">' + (u.months || '—') + '月</div></div>' +
'</div>' +
'<div class="text-[10px] text-slate-500 mt-1">均持' + (m.avg_hold_days != null ? m.avg_hold_days + '天' : '—') + ' · 均收益' + (m.avg_profit_pct != null ? m.avg_profit_pct + '%' : '—') + ' · 回撤' + (m.max_dd_pct != null ? m.max_dd_pct + '%' : '—') + '</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;
@@ -2243,7 +2373,7 @@ function renderStrategyTable(strategies) {
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' : '');
html += '<tr class="' + rowCls + '" onclick="showStrategyDetail(\'' + s.version + '\')">' +
'<td class="px-2 py-1.5 font-mono font-bold ' + (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>' : '') + ((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" title="' + (s.hypothesis || s.summary || '').replace(/"/g, '&quot;') + '">' + (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, '&quot;') + '">' + (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') +