feat: 策略追踪评估系统(版本化+理论盈亏+浮盈/浮亏+自动SL/TP检测+30s刷新)
This commit is contained in:
+90
-78
@@ -612,89 +612,101 @@ function renderMarket() {
|
||||
}
|
||||
|
||||
// ── Evaluation Tab ──
|
||||
let evalTimer = null;
|
||||
function renderEvaluation() {
|
||||
const el = document.getElementById('tab-evaluation');
|
||||
Promise.all([
|
||||
fetchJSON('/api/evaluation'),
|
||||
fetchJSON('/api/stats/accuracy'),
|
||||
]).then(([evals, stats]) => {
|
||||
const p1 = stats.phase1 || {};
|
||||
const p2 = stats.phase2 || {};
|
||||
if (evalTimer) clearInterval(evalTimer);
|
||||
fetchJSON('/api/tracking').then(data => {
|
||||
const tracks = data.tracks || [];
|
||||
const stats = data.stats || {};
|
||||
|
||||
const statusBadge = s => {
|
||||
const m = {active:'🟡进行中',hit_tp:'🟢止盈',hit_sl:'🔴止损',expired:'⚫已过期',manual_close:'🔵手动平仓'};
|
||||
return m[s] || s;
|
||||
};
|
||||
|
||||
el.innerHTML = `
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<span class="text-sm font-semibold">策略双维度评估 <span class="spec-btns"><span class="spec-btn help" onclick="showModuleHelp('evaluation','human')">?</span><span class="spec-btn ai" onclick="showModuleHelp('evaluation','ai')">§</span></span></span>
|
||||
<span class="text-xs bg-blue-900/50 text-blue-300 px-2 py-0.5 rounded-full">${evals.length} 只已评估</span>
|
||||
<button class="text-xs px-2 py-1 rounded bg-slate-700 hover:bg-slate-600 ml-auto" onclick="triggerEval()">🔄 重新评估</button>
|
||||
<span class="text-sm font-semibold">📊 策略追踪评估</span>
|
||||
<span class="text-xs bg-blue-900/50 text-blue-300 px-2 py-0.5 rounded-full">${stats.total||0} 条记录</span>
|
||||
<span class="text-xs text-green-400">止盈 ${stats.hit_tp||0}</span>
|
||||
<span class="text-xs text-red-400">止损 ${stats.hit_sl||0}</span>
|
||||
<span class="text-xs text-yellow-400">进行中 ${stats.active||0}</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 lg:grid-cols-4 gap-3 mb-4">
|
||||
<div class="card p-3 text-center">
|
||||
<div class="text-lg font-bold text-green-400">${p1.correct||0}</div>
|
||||
<div class="text-xs text-slate-500">阶段一 达标</div>
|
||||
</div>
|
||||
<div class="card p-3 text-center">
|
||||
<div class="text-lg font-bold text-red-400">${p1.wrong||0}</div>
|
||||
<div class="text-xs text-slate-500">阶段一 止损</div>
|
||||
</div>
|
||||
<div class="card p-3 text-center">
|
||||
<div class="text-lg font-bold text-yellow-400">${p1.pending||0}</div>
|
||||
<div class="text-xs text-slate-500">待验证</div>
|
||||
</div>
|
||||
<div class="card p-3 text-center">
|
||||
<div class="text-lg font-bold text-white">${p1.accuracy_pct||0}%</div>
|
||||
<div class="text-xs text-slate-500">准确率</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-xs">
|
||||
<thead><tr class="text-slate-500 border-b border-slate-800/50">
|
||||
<th class="text-left p-3">股票</th><th class="text-left p-3">策略</th>
|
||||
<th class="text-left p-3">理论盈亏</th><th class="text-left p-3">实际盈亏</th>
|
||||
<th class="text-left p-3">状态</th>
|
||||
</tr></thead>
|
||||
<tbody>${evals.map(x => {
|
||||
const adv = (x.advice_evaluation && x.advice_evaluation[0]) || {};
|
||||
const t = x.theoretical || {};
|
||||
const a = x.actual || {};
|
||||
const status = t.status || 'safe';
|
||||
const icon = status === 'take_profit_hit' ? '🟢' : status === 'stop_loss_hit' ? '🔴' : status === 'in_entry_zone' ? '📥' : '💤';
|
||||
return `<tr class="stock-row border-b border-slate-800/30">
|
||||
<td class="p-3"><div class="font-medium text-white">${x.name}</div><div class="text-slate-500">${x.code}</div></td>
|
||||
<td class="p-3 text-slate-300">${x.current || (x.advice_evaluation && x.advice_evaluation[0] && x.advice_evaluation[0].current_advice || '') || '—'}</td>
|
||||
<td class="p-3"><span class="${(t.theoretical_pnl_pct||0)>=0?'badge-up':'badge-down'}">${(t.theoretical_pnl_pct||0).toFixed(1)}%</span></td>
|
||||
<td class="p-3"><span class="${(a.actual_pnl_pct||0)>=0?'badge-up':'badge-down'}">${(a.actual_pnl_pct||0).toFixed(1)}%</span></td>
|
||||
<td class="p-3">${icon} ${status.replace(/_/g,' ')}</td>
|
||||
</tr>`;
|
||||
}).join('')}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs text-slate-600 mt-2">更新时间: ${stats.updated_at?.slice(0,16)||'—'}</div>
|
||||
`;
|
||||
// Load feedback
|
||||
fetchJSON('/api/feedback').then(fb => {
|
||||
const trend = fb.accuracy_trend || {};
|
||||
const completed = fb.phase1_completed_count || 0;
|
||||
const reassess = fb.reassess_needed_count || 0;
|
||||
if (!completed && !reassess) return;
|
||||
const fbEl = document.createElement('div');
|
||||
fbEl.className = 'card p-4 mt-4';
|
||||
fbEl.innerHTML = `
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<span class="text-sm font-semibold">🔄 反馈闭环</span>
|
||||
<span class="text-xs text-slate-500">趋势: ${trend.trend||'stable'}</span>
|
||||
<span class="text-xs ${completed>0?'text-green-400':'text-slate-500'}">完成 ${completed}</span>
|
||||
<span class="text-xs ${reassess>0?'text-yellow-400':'text-slate-500'}">重评 ${reassess}</span>
|
||||
</div>
|
||||
${(fb.feedback||[]).filter(f => f.adjustments?.length).slice(0,10).map(f => f.adjustments.map(a => `
|
||||
<div class="flex items-start gap-2 p-2 bg-slate-800/30 rounded-lg mb-1 text-xs">
|
||||
<span class="${a.type==='phase1_success'?'text-green-400':a.type==='phase1_failure'?'text-red-400':'text-yellow-400'}">${a.type==='phase1_success'?'✅':a.type==='phase1_failure'?'❌':'🔄'}</span>
|
||||
<span class="text-slate-300">${a.message}</span>
|
||||
</div>
|
||||
`).join('')).join('')}
|
||||
`;
|
||||
el.appendChild(fbEl);
|
||||
});
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-xs">
|
||||
<thead><tr class="text-slate-500 border-b border-slate-800/50">
|
||||
<th class="text-left p-2">股票</th>
|
||||
<th class="text-left p-2">推荐时间</th>
|
||||
<th class="text-right p-2">评分</th>
|
||||
<th class="text-right p-2">RR</th>
|
||||
<th class="text-right p-2">买入区(中值)</th>
|
||||
<th class="text-right p-2">止损/止盈</th>
|
||||
<th class="text-right p-2">推荐价</th>
|
||||
<th class="text-right p-2">推荐仓位</th>
|
||||
<th class="text-right p-2">理论盈亏</th>
|
||||
<th class="text-right p-2">实际盈亏</th>
|
||||
<th class="text-left p-2">状态</th>
|
||||
</tr></thead>
|
||||
<tbody>${tracks.length === 0 ? '<tr><td colspan="12" class="p-4 text-center text-slate-500">暂无追踪记录</td></tr>' : tracks.map(t => {
|
||||
const ccy = (t.code||'').startsWith('0') ? 'HK$' : '¥';
|
||||
const fmt = v => v ? ccy + Number(v).toFixed(2) : '—';
|
||||
const mid = t.entry_mid || 0;
|
||||
const sl = t.stop_loss || 0;
|
||||
const tp = t.take_profit || 0;
|
||||
const cp = t.current_price || 0;
|
||||
const hasActual = !!(t.actual_action);
|
||||
const rowBg = t.status === 'hit_tp' ? 'bg-green-900/10' : t.status === 'hit_sl' ? 'bg-red-900/10' : hasActual ? 'bg-blue-900/5' : '';
|
||||
// ── 理论盈亏 ──
|
||||
const tPnl = t.theoretical_pnl != null ? '<span class="' + (t.theoretical_pnl >= 0 ? 'text-red-400' : 'text-green-400') + '">' + (t.theoretical_pnl >= 0 ? '+' : '') + t.theoretical_pnl + '%</span>' : '—';
|
||||
// ── 实际盈亏 ──
|
||||
let actualCell = '—';
|
||||
if (hasActual) {
|
||||
const fp = t.floating_pnl != null ? (t.floating_pnl >= 0 ? '+' : '') + t.floating_pnl + '%' : '';
|
||||
const fa = t.floating_amount != null ? ccy + (t.floating_amount >= 0 ? '+' : '') + Number(t.floating_amount).toFixed(0) : '';
|
||||
actualCell = '<span class="' + (t.floating_pnl >= 0 ? 'text-red-400' : 'text-green-400') + '">' + fp + '</span><br><span class="text-slate-500" style="font-size:10px">' + fa + '</span>';
|
||||
}
|
||||
// ── 推荐时间 ──
|
||||
let timeCell = (t.tracked_at||'').slice(0,16);
|
||||
if (hasActual && t.actual_action) {
|
||||
const m = t.actual_action.match(/(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2})/);
|
||||
if (m) timeCell += '<br><span class="text-blue-400">实操 ' + m[1] + '</span>';
|
||||
}
|
||||
// ── 买入区:低→高排列(策略区下/中/上 + 实操价)──
|
||||
let zonePrices = [];
|
||||
if (t.entry_low) zonePrices.push({v:t.entry_low, label:'下', cls:'text-slate-300'});
|
||||
if (mid) zonePrices.push({v:mid, label:'中', cls:'text-amber-300'});
|
||||
if (t.entry_high) zonePrices.push({v:t.entry_high, label:'上', cls:'text-slate-300'});
|
||||
if (hasActual && t.actual_entry) zonePrices.push({v:t.actual_entry, label:'实', cls:'text-blue-400'});
|
||||
zonePrices.sort((a,b) => a.v - b.v);
|
||||
let zoneCell = zonePrices.map(p => '<span class="' + p.cls + '">' + fmt(p.v) + '</span>').join('<br>');
|
||||
// ── 止损/止盈:低→高排列 + 当前价 ──
|
||||
let sltpPrices = [];
|
||||
if (sl) sltpPrices.push({v:sl, label:'损', cls:'text-red-400'});
|
||||
if (cp) sltpPrices.push({v:cp, label:'现', cls:'text-white'});
|
||||
if (tp) sltpPrices.push({v:tp, label:'盈', cls:'text-green-400'});
|
||||
sltpPrices.sort((a,b) => a.v - b.v);
|
||||
let sltpCell = sltpPrices.map(p => '<span class="' + p.cls + '">' + p.label + fmt(p.v) + '</span>').join('<br>');
|
||||
// ── 仓位 ──
|
||||
let posCell = t.position_advice ? String(t.position_advice).replace(/(系统按.*/,'') : '—';
|
||||
if (hasActual && t.actual_shares) posCell += '<br><span class="text-blue-400">实操 ' + t.actual_shares + '股</span>';
|
||||
return '<tr class="border-b border-slate-800/30 ' + rowBg + '">' +
|
||||
'<td class="p-2"><span class="font-medium text-white">' + (t.name||'') + '</span><br><span class="text-slate-500">' + t.code + '</span></td>' +
|
||||
'<td class="p-2 text-slate-400">' + timeCell + '</td>' +
|
||||
'<td class="p-2 text-right font-mono ' + (t.rec_score>=70?'text-green-400':'text-amber-300') + '">' + (t.rec_score||'—') + '</td>' +
|
||||
'<td class="p-2 text-right font-mono">' + (t.rr_ratio ? t.rr_ratio.toFixed(2) : '—') + '</td>' +
|
||||
'<td class="p-2 text-right font-mono text-xs">' + zoneCell + '</td>' +
|
||||
'<td class="p-2 text-right font-mono text-xs">' + sltpCell + '</td>' +
|
||||
'<td class="p-2 text-right font-mono text-slate-300">' + fmt(t.price_at_track) + '</td>' +
|
||||
'<td class="p-2 text-right font-mono text-slate-300">' + posCell + '</td>' +
|
||||
'<td class="p-2 text-right font-mono">' + tPnl + '</td>' +
|
||||
'<td class="p-2 text-right font-mono">' + actualCell + '</td>' +
|
||||
'<td class="p-2">' + statusBadge(t.status) + (t.close_reason ? '<br><span class="text-slate-500">' + t.close_reason + '</span>' : '') + '</td>' +
|
||||
'</tr>';
|
||||
}).join('')}</tbody>
|
||||
</table>
|
||||
</div>`;
|
||||
evalTimer = setInterval(renderEvaluation, 30000);
|
||||
});
|
||||
}
|
||||
function triggerEval() {
|
||||
|
||||
Reference in New Issue
Block a user