- 归档 evolution/ + meta_growth/meta_watchdog/ab_research_daily - docs/evolution-archive-readme.md: 归档说明(旧模块功能+替代方案) - server.py: 新增 /api/research/effectiveness + effectiveness/summary + recommendation_log + execution_log - static/effectiveness.html: 新评估页面(概览/详细评估/推荐记录/执行记录) - 策略进化改为人驱动闭环(评估→用户决策→调整)
193 lines
8.4 KiB
HTML
193 lines
8.4 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>策略评估 - MoFin</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0a0a0f; color: #e0e0e0; padding: 20px; }
|
|
h1 { color: #fff; font-size: 1.5em; margin-bottom: 20px; }
|
|
.tab-bar { display: flex; gap: 8px; margin-bottom: 20px; }
|
|
.tab { padding: 8px 16px; border-radius: 6px; cursor: pointer; background: #1a1a2e; color: #888; border: 1px solid #333; }
|
|
.tab.active { background: #16213e; color: #4fc3f7; border-color: #4fc3f7; }
|
|
.card { background: #1a1a2e; border-radius: 8px; padding: 16px; margin-bottom: 12px; border: 1px solid #333; }
|
|
.card h3 { color: #4fc3f7; font-size: 1.1em; margin-bottom: 8px; }
|
|
.metric { display: inline-block; margin-right: 16px; margin-bottom: 8px; }
|
|
.metric .label { color: #888; font-size: 0.85em; }
|
|
.metric .value { color: #fff; font-size: 1.2em; font-weight: bold; }
|
|
.metric .value.good { color: #4caf50; }
|
|
.metric .value.warn { color: #ff9800; }
|
|
.metric .value.bad { color: #f44336; }
|
|
table { width: 100%; border-collapse: collapse; margin-top: 12px; }
|
|
th, td { padding: 8px 12px; text-align: left; border-bottom: 1px solid #333; }
|
|
th { color: #888; font-size: 0.85em; }
|
|
.tag { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.8em; }
|
|
.tag-effective { background: #1b5e20; color: #4caf50; }
|
|
.tag-ineffective { background: #b71c1c; color: #f44336; }
|
|
.tag-neutral { background: #37474f; color: #90a4ae; }
|
|
.tag-not_triggered { background: #263238; color: #607d8b; }
|
|
.filter { margin-bottom: 16px; }
|
|
.filter select, .filter input { background: #1a1a2e; color: #e0e0e0; border: 1px solid #333; padding: 6px 12px; border-radius: 4px; }
|
|
#loading { text-align: center; padding: 40px; color: #888; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>📊 策略评估</h1>
|
|
|
|
<div class="tab-bar">
|
|
<div class="tab active" onclick="showTab('overview')">概览</div>
|
|
<div class="tab" onclick="showTab('details')">详细评估</div>
|
|
<div class="tab" onclick="showTab('recommendations')">推荐记录</div>
|
|
<div class="tab" onclick="showTab('executions')">执行记录</div>
|
|
</div>
|
|
|
|
<div class="filter">
|
|
<select id="sourceFilter" onchange="loadData()">
|
|
<option value="">全部策略</option>
|
|
</select>
|
|
<select id="codeFilter" onchange="loadData()">
|
|
<option value="">全部股票</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div id="loading">加载中...</div>
|
|
<div id="content"></div>
|
|
|
|
<script>
|
|
const API = '/api/research';
|
|
let currentTab = 'overview';
|
|
|
|
function showTab(tab) {
|
|
currentTab = tab;
|
|
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
event.target.classList.add('active');
|
|
loadData();
|
|
}
|
|
|
|
async function loadData() {
|
|
const source = document.getElementById('sourceFilter').value;
|
|
const code = document.getElementById('codeFilter').value;
|
|
const content = document.getElementById('content');
|
|
content.innerHTML = '<div id="loading">加载中...</div>';
|
|
|
|
try {
|
|
if (currentTab === 'overview') {
|
|
const res = await fetch(`${API}/effectiveness/summary`);
|
|
const data = await res.json();
|
|
renderOverview(data);
|
|
} else if (currentTab === 'details') {
|
|
const params = new URLSearchParams();
|
|
if (source) params.set('source', source);
|
|
if (code) params.set('code', code);
|
|
const res = await fetch(`${API}/effectiveness?${params}`);
|
|
const data = await res.json();
|
|
renderDetails(data);
|
|
} else if (currentTab === 'recommendations') {
|
|
const params = new URLSearchParams();
|
|
if (code) params.set('code', code);
|
|
const res = await fetch(`${API}/recommendation_log?${params}`);
|
|
const data = await res.json();
|
|
renderRecommendations(data);
|
|
} else if (currentTab === 'executions') {
|
|
const params = new URLSearchParams();
|
|
if (code) params.set('code', code);
|
|
const res = await fetch(`${API}/execution_log?${params}`);
|
|
const data = await res.json();
|
|
renderExecutions(data);
|
|
}
|
|
} catch (e) {
|
|
content.innerHTML = `<div class="card"><h3>加载失败</h3><p>${e.message}</p></div>`;
|
|
}
|
|
}
|
|
|
|
function renderOverview(data) {
|
|
const content = document.getElementById('content');
|
|
if (!data.length) { content.innerHTML = '<div class="card"><h3>暂无评估数据</h3><p>策略到期后会自动生成评估结果</p></div>'; return; }
|
|
|
|
let html = '';
|
|
data.forEach(row => {
|
|
const total = row.total || 0;
|
|
const buyPct = total ? Math.round(row.buy_effective/total*100) : 0;
|
|
const slPct = total ? Math.round(row.sl_effective/total*100) : 0;
|
|
const tpPct = total ? Math.round(row.tp_effective/total*100) : 0;
|
|
const timePct = Math.round((row.time_score||0)*100);
|
|
|
|
html += `<div class="card">
|
|
<h3>${row.strategy_source || '未知策略'}</h3>
|
|
<div class="metric"><div class="label">评估次数</div><div class="value">${total}</div></div>
|
|
<div class="metric"><div class="label">买入区有效</div><div class="value ${buyPct>=60?'good':buyPct>=40?'warn':'bad'}">${buyPct}%</div></div>
|
|
<div class="metric"><div class="label">止损有效</div><div class="value ${slPct>=60?'good':slPct>=40?'warn':'bad'}">${slPct}%</div></div>
|
|
<div class="metric"><div class="label">止盈有效</div><div class="value ${tpPct>=60?'good':tpPct>=40?'warn':'bad'}">${tpPct}%</div></div>
|
|
<div class="metric"><div class="label">时间准确</div><div class="value ${timePct>=60?'good':timePct>=40?'warn':'bad'}">${timePct}%</div></div>
|
|
</div>`;
|
|
});
|
|
content.innerHTML = html;
|
|
}
|
|
|
|
function renderDetails(data) {
|
|
const content = document.getElementById('content');
|
|
if (!data.length) { content.innerHTML = '<div class="card"><h3>暂无数据</h3></div>'; return; }
|
|
|
|
let html = '<table><tr><th>股票</th><th>策略</th><th>版本</th><th>周期</th><th>买入区</th><th>止损</th><th>止盈</th><th>时间</th><th>评价</th></tr>';
|
|
data.forEach(r => {
|
|
html += `<tr>
|
|
<td>${r.code}</td>
|
|
<td>${r.strategy_source||'-'}</td>
|
|
<td>v${r.version||'?'}</td>
|
|
<td>${(r.period_start||'').slice(0,10)} ~ ${(r.period_end||'').slice(0,10)}</td>
|
|
<td><span class="tag tag-${r.buy_zone_accuracy==='effective'?'effective':r.buy_zone_accuracy==='ineffective'?'ineffective':'neutral'}">${r.buy_zone_accuracy||'-'}</span></td>
|
|
<td><span class="tag tag-${r.stop_loss_accuracy==='effective'?'effective':r.stop_loss_accuracy==='ineffective'?'ineffective':'neutral'}">${r.stop_loss_accuracy||'-'}</span></td>
|
|
<td><span class="tag tag-${r.take_profit_accuracy==='effective'?'effective':r.take_profit_accuracy==='ineffective'?'ineffective':'neutral'}">${r.take_profit_accuracy||'-'}</span></td>
|
|
<td><span class="tag tag-${r.time_accuracy==='on_time'?'effective':r.time_accuracy==='late'?'ineffective':'neutral'}">${r.time_accuracy||'-'}</span></td>
|
|
<td title="${r.overall_assessment||''}">${(r.overall_assessment||'').slice(0,30)}${(r.overall_assessment||'').length>30?'...':''}</td>
|
|
</tr>`;
|
|
});
|
|
html += '</table>';
|
|
content.innerHTML = html;
|
|
}
|
|
|
|
function renderRecommendations(data) {
|
|
const content = document.getElementById('content');
|
|
if (!data.length) { content.innerHTML = '<div class="card"><h3>暂无推荐记录</h3></div>'; return; }
|
|
let html = '<table><tr><th>时间</th><th>股票</th><th>策略</th><th>版本</th><th>动作</th><th>买入区</th><th>止损</th><th>止盈</th></tr>';
|
|
data.forEach(r => {
|
|
html += `<tr>
|
|
<td>${(r.recommend_time||'').slice(0,16)}</td>
|
|
<td>${r.code}</td>
|
|
<td>${r.strategy_source||'-'}</td>
|
|
<td>v${r.version||'?'}</td>
|
|
<td>${r.action||'-'}</td>
|
|
<td>${r.entry_low||0}~${r.entry_high||0}</td>
|
|
<td>${r.stop_loss||'-'}</td>
|
|
<td>${r.take_profit||'-'}</td>
|
|
</tr>`;
|
|
});
|
|
html += '</table>';
|
|
content.innerHTML = html;
|
|
}
|
|
|
|
function renderExecutions(data) {
|
|
const content = document.getElementById('content');
|
|
if (!data.length) { content.innerHTML = '<div class="card"><h3>暂无执行记录</h3></div>'; return; }
|
|
let html = '<table><tr><th>时间</th><th>股票</th><th>动作</th><th>数量</th><th>价格</th><th>来源</th></tr>';
|
|
data.forEach(r => {
|
|
html += `<tr>
|
|
<td>${(r.execute_time||'').slice(0,16)}</td>
|
|
<td>${r.code}</td>
|
|
<td>${r.action}</td>
|
|
<td>${r.shares}</td>
|
|
<td>${r.price}</td>
|
|
<td>${r.source||'-'}</td>
|
|
</tr>`;
|
|
});
|
|
html += '</table>';
|
|
content.innerHTML = html;
|
|
}
|
|
|
|
// 初始加载
|
|
loadData();
|
|
</script>
|
|
</body>
|
|
</html>
|