diff --git a/server.py b/server.py
index 2ad2dbed..26f70fde 100644
--- a/server.py
+++ b/server.py
@@ -533,28 +533,66 @@ def get_tracking():
-@app.route("/api/research/backtest")
-def get_research_backtest():
- import sqlite3
- from datetime import datetime, timedelta
- period = request.args.get("period", "1y")
- capital = float(request.args.get("capital", 1000000))
- end_date = datetime.now().strftime("%Y-%m-%d")
- if period == "6m":
- start_date = (datetime.now() - timedelta(days=180)).strftime('%Y-%m-%d')
- elif period == "1y":
- start_date = (datetime.now() - timedelta(days=365)).strftime('%Y-%m-%d')
- else:
- start_date = (datetime.now() - timedelta(days=730)).strftime('%Y-%m-%d')
+@app.route("/api/research/strategies")
+def api_research_strategies():
+ """策略版本列表(含回测结果摘要)"""
try:
- from backtest_framework import run_strategy_research
- result = run_strategy_research(start_date, end_date, capital)
- from flask import jsonify
- return jsonify(result)
+ from strategy_lab import list_strategies
+ return jsonify({'strategies': list_strategies()})
except Exception as e:
- from flask import jsonify
return jsonify({'error': str(e)}), 500
+
+@app.route("/api/research/backtest")
+def api_research_backtest():
+ """运行指定策略版本的回测"""
+ from datetime import datetime, timedelta
+ version = request.args.get('strategy', 'v4.1')
+ period = request.args.get('period', '6m')
+ capital = float(request.args.get('capital', 1000000))
+ end_date = '2026-07-24' # 数据完整截止日
+ days = {'1m': 30, '6m': 185, '1y': 365, '2y': 730}.get(period, 185)
+ start_date = (datetime.strptime(end_date, '%Y-%m-%d') - timedelta(days=days)).strftime('%Y-%m-%d')
+ try:
+ from strategy_lab import run_backtest, analyze_trade_list, save_analysis
+ result = run_backtest(version, start_date, end_date, capital)
+ analysis = analyze_trade_list(result.get('trades', []), version)
+ save_analysis(version, analysis)
+ result['insights'] = analysis.get('insights', [])
+ return jsonify(result)
+ except ValueError as e:
+ return jsonify({'error': str(e)}), 404
+ except Exception as e:
+ return jsonify({'error': str(e)}), 500
+
+
+@app.route("/api/research/analysis")
+def api_research_analysis():
+ """指定策略版本的因子归因分析"""
+ version = request.args.get('strategy', 'v4.1')
+ try:
+ from strategy_lab import analyze_trades
+ return jsonify(analyze_trades(version))
+ except Exception as e:
+ return jsonify({'error': str(e)}), 500
+
+
+@app.route("/api/research/trades")
+def api_research_trades():
+ """指定策略版本的交易明细"""
+ import sqlite3 as _sq, json as _json
+ version = request.args.get('strategy', 'v4.1')
+ conn = _sq.connect("/home/hmo/MoFin/data/mofin.db")
+ row = conn.execute(
+ "SELECT results_json FROM strategy_research WHERE version=? ORDER BY id DESC LIMIT 1",
+ (version,)).fetchone()
+ conn.close()
+ if not row:
+ return jsonify({'error': f'无 {version} 回测结果'}), 404
+ res = _json.loads(row[0])
+ return jsonify({'strategy': version, 'trades': res.get('trades', [])})
+
+
@app.route("/api/overview")
def api_overview():
"""概览数据"""
diff --git a/static/index.html b/static/index.html
index 9e01d814..da149050 100644
--- a/static/index.html
+++ b/static/index.html
@@ -1929,84 +1929,156 @@ function inMd(t) {
function renderResearch() {
const el = document.getElementById('tab-research');
if (!el) return;
- el.innerHTML = '
📋 策略研究 — 全流程回测
' +
- '' +
- '' +
- '点击「运行」开始回测验证
';
- window.btEl = el;
+ el.innerHTML = '' +
+ '
📋 策略研究 — 版本迭代对比
' +
+ '' +
+ '每版策略基于上一版归因分析迭代 · 点击行展开明细' +
+ '' +
+ '';
+ loadStrategyList();
}
-async function runBacktest() {
- const period = document.getElementById('btPeriod')?.value || '6m';
- const resultsEl = document.getElementById('btResults');
- if (!resultsEl) return;
- resultsEl.innerHTML = '⏳ 回测运行中(约 15-30 秒)...
';
+async function loadStrategyList() {
+ const listEl = document.getElementById('strategyList');
try {
- const resp = await fetch('/api/research/backtest?period=' + period);
+ const resp = await fetch('/api/research/strategies');
const data = await resp.json();
- if (data.error) { resultsEl.innerHTML = '错误: ' + data.error + '
'; return; }
- displayBacktestResults(data);
+ if (data.error) { listEl.innerHTML = '' + data.error + '
'; return; }
+ renderStrategyTable(data.strategies || []);
} catch(e) {
- resultsEl.innerHTML = '请求失败: ' + e.message + '
';
+ listEl.innerHTML = '加载失败: ' + e.message + '
';
}
}
-function displayBacktestResults(data) {
- const el = document.getElementById('btResults');
- if (!el) return;
- const s = data.summary || {};
- const winRate = s.win_rate || 0;
- const winRateClass = winRate >= 50 ? 'text-green-400' : winRate >= 40 ? 'text-yellow-400' : 'text-red-400';
- const sharpeClass = (s.sharpe_ratio || 0) >= 1 ? 'text-green-400' : (s.sharpe_ratio || 0) >= 0 ? 'text-yellow-400' : 'text-red-400';
-
- let html = '' +
- '
测试周期
' + (data.period || '') + '
' +
- '
初始资金
¥' + (s.capital_end ? (data.capital || 1000000).toLocaleString() : '100万') + '
' +
- '
最终资金
¥' + (s.capital_end ? s.capital_end.toLocaleString() : '-') + '
' +
- '
总交易
' + (s.total_trades || 0) + ' 笔
' +
- '
' +
- '
平均收益
' + (s.avg_profit_pct || 0) + '%
' +
- '
夏普比率
' + (s.sharpe_ratio || 0).toFixed(2) + '
' +
- '
最大回撤
' + (s.max_drawdown_pct || 0).toFixed(1) + '%
' +
- '
' +
- '' +
- '
✅ 盈利: ' + (s.wins || 0) + ' 均+' + (s.avg_win_pct || 0).toFixed(1) + '%
' +
- '
❌ 亏损: ' + (s.losses || 0) + ' 均' + (s.avg_loss_pct || 0).toFixed(1) + '%
' +
- '
📊 盈亏比: ' + (s.profit_factor || 0).toFixed(2) + '
' +
- '
📈 筛选: ' + (data.total_stocks_screened || 0) + ' 只
' +
- '
';
-
- // Trade list
- const trades = data.trades || [];
- html += '交易明细(前 50 笔)
' +
- '' +
- '| 代码 | 名称 | 买入日 | ' +
- '买入价 | 卖出价 | 收益 | ' +
- '结果 | 持仓 | 评分 |
';
- for (const t of trades) {
- const pnlClass = (t.profit_pct || 0) >= 0 ? 'text-green-400' : 'text-red-400';
- const reasonMap = { 'target': '🎯 止盈', 'stop': '🛑 止损', 'keep': '⏳ 持仓中' };
- html += '' +
- '| ' + (t.code || '') + ' | ' +
- '' + (t.name || '') + ' | ' +
- '' + (t.entry_date || '') + ' | ' +
- '' + (t.entry_price || 0).toFixed(2) + ' | ' +
- '' + (t.exit_price || 0).toFixed(2) + ' | ' +
- '' + (t.profit_pct || 0).toFixed(1) + '% | ' +
- '' + (reasonMap[t.exit_reason] || t.exit_reason || '') + ' | ' +
- '' + (t.hold_days || 0) + 'd | ' +
- '' + (t.score || 0) + ' |
';
+function renderStrategyTable(strategies) {
+ const el = document.getElementById('strategyList');
+ if (!strategies.length) { el.innerHTML = '暂无策略版本
'; return; }
+ // 找每列最优值用于高亮
+ const best = { win_rate: -999, sharpe_ratio: -999, profit_factor: -999, avg_profit_pct: -999, max_drawdown_pct: 999 };
+ for (const s of strategies) {
+ const st = s.summary_stats || {};
+ if (st.win_rate != null) best.win_rate = Math.max(best.win_rate, st.win_rate);
+ if (st.sharpe_ratio != null) best.sharpe_ratio = Math.max(best.sharpe_ratio, st.sharpe_ratio);
+ if (st.profit_factor != null) best.profit_factor = Math.max(best.profit_factor, st.profit_factor);
+ if (st.avg_profit_pct != null) best.avg_profit_pct = Math.max(best.avg_profit_pct, st.avg_profit_pct);
+ if (st.max_drawdown_pct != null) best.max_drawdown_pct = Math.min(best.max_drawdown_pct, st.max_drawdown_pct);
}
- html += '
';
-
+ const hl = (val, bestVal, invert) => {
+ if (val == null) return '';
+ const isBest = invert ? (val === bestVal && bestVal !== 999) : (val === bestVal && bestVal !== -999);
+ return isBest ? 'text-green-400 font-bold' : '';
+ };
+ let html = '' +
+ '| 版本 | 名称 | ' +
+ '改进假设 | ' +
+ '交易数 | 胜率 | ' +
+ '平均收益 | 夏普 | ' +
+ '盈亏比 | 最大回撤 | ' +
+ '操作 |
';
+ for (const s of strategies) {
+ const st = s.summary_stats || {};
+ const hasResult = st.total_trades != null;
+ const smallSample = hasResult && st.total_trades < 40;
+ html += '' +
+ '| ' + s.version + ' | ' +
+ '' + (s.name || '') + ' | ' +
+ '' + (s.hypothesis || s.summary || '') + ' | ' +
+ '' + (hasResult ? st.total_trades + (smallSample ? '⚠️' : '') : '—') + ' | ' +
+ '' + (st.win_rate != null ? st.win_rate + '%' : '—') + ' | ' +
+ '' + (st.avg_profit_pct != null ? st.avg_profit_pct + '%' : '—') + ' | ' +
+ '' + (st.sharpe_ratio != null ? st.sharpe_ratio : '—') + ' | ' +
+ '' + (st.profit_factor != null ? st.profit_factor : '—') + ' | ' +
+ '' + (st.max_drawdown_pct != null ? st.max_drawdown_pct + '%' : '—') + ' | ' +
+ '' +
+ '' +
+ ' |
';
+ }
+ html += '
' +
+ '⚠️ = 样本量<40笔,统计意义有限 · 绿色 = 该列最优 · 回测期间: 2026-01-21 ~ 2026-07-24
';
el.innerHTML = html;
}
+async function runStrategyBacktest(version) {
+ const period = document.getElementById('btPeriod')?.value || '6m';
+ const detail = document.getElementById('strategyDetail');
+ detail.innerHTML = '⏳ 正在回测 ' + version + '(约15-30秒)...
';
+ try {
+ const resp = await fetch('/api/research/backtest?strategy=' + version + '&period=' + period);
+ const data = await resp.json();
+ if (data.error) { detail.innerHTML = '' + data.error + '
'; return; }
+ await loadStrategyList();
+ showStrategyResult(data);
+ } catch(e) {
+ detail.innerHTML = '失败: ' + e.message + '
';
+ }
+}
+
+async function showStrategyDetail(version) {
+ const detail = document.getElementById('strategyDetail');
+ detail.innerHTML = '加载 ' + version + ' 明细...
';
+ try {
+ const [anaResp, tradesResp] = await Promise.all([
+ fetch('/api/research/analysis?strategy=' + version),
+ fetch('/api/research/trades?strategy=' + version)
+ ]);
+ const ana = await anaResp.json();
+ const tradesData = await tradesResp.json();
+ showStrategyResult({ strategy: version, trades: tradesData.trades || [], insights: (ana.insights || []), analysis: ana });
+ } catch(e) {
+ detail.innerHTML = '失败: ' + e.message + '
';
+ }
+}
+
+function showStrategyResult(data) {
+ const el = document.getElementById('strategyDetail');
+ const s = data.summary || {};
+ let html = '' +
+ '
' + (data.strategy || '') + ' ' + (data.strategy_name || '') + '
' +
+ '' + (data.period || '') + '';
+ if (s.total_trades != null) {
+ html += '
' +
+ '
' +
+ '
' +
+ '
平均收益
' + s.avg_profit_pct + '%
' +
+ '
' +
+ '
盈亏比
' + (s.profit_factor || '—') + '
' +
+ '
回撤
' + s.max_drawdown_pct + '%
';
+ }
+ // 归因洞察
+ if (data.insights && data.insights.length) {
+ html += '
🔬 因子归因洞察(下一版迭代依据)
' +
+ '
' + data.insights.map(i => '
' + i + '
').join('') + '
';
+ }
+ // 交易明细
+ const trades = (data.trades || []).slice(0, 50);
+ if (trades.length) {
+ html += '
交易明细(前50笔)
' +
+ '
' +
+ '| 代码 | 名称 | 入场 | ' +
+ '买入 | 卖出 | 收益 | ' +
+ '结果 | 持仓 | 评分 |
';
+ const reasonMap = { target: '🎯止盈', stop: '🛑止损', keep: '⏳到期' };
+ for (const t of trades) {
+ const pc = (t.profit_pct || 0) >= 0 ? 'text-green-400' : 'text-red-400';
+ html += '' +
+ '| ' + t.code + ' | ' + (t.name || '') + ' | ' +
+ '' + (t.entry_date || '') + ' | ' +
+ '' + (t.entry_price || 0).toFixed(2) + ' | ' +
+ '' + (t.exit_price || 0).toFixed(2) + ' | ' +
+ '' + (t.profit_pct || 0).toFixed(1) + '% | ' +
+ '' + (reasonMap[t.exit_reason] || t.exit_reason) + ' | ' +
+ '' + (t.hold_days || 0) + 'd | ' +
+ '' + (t.score || 0) + ' |
';
+ }
+ html += '
';
+ }
+ html += '
';
+ el.innerHTML = html;
+}
+
+