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:
@@ -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)
|
||||
|
||||
@@ -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
@@ -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});
|
||||
|
||||
// ══════════════════════════════════════════════════════
|
||||
// 组合方案子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">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, '"') + '">' + (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, '"') + '">' + (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') +
|
||||
|
||||
Reference in New Issue
Block a user