feat(dashboard): wallet panel now shows forward P&L + warns on copy_pnl traps

serve_dashboard.py _serve_wallets():
- Reads portfolio.json per-wallet forward realized (out-of-sample)
- Returns fwd_realized + fwd_bets for each followed + suggested wallet
- Splits suggestions into 'suggestions' (forward positive) and
  'warnings' (forward negative despite high copy_pnl)

bot_dashboard.html renderWallets():
- Added 前瞻30d column to followed wallets table (green/red)
- Suggestions only show forward-positive wallets
- New warnings section: red badges for copy_pnl-high-but-forward-negative
  wallets (e.g. oliman2 #1 copy_pnl but - forward)

Prevents the oliman2/leegunner trap: dashboard used to recommend
copy_pnl #1 and #2, but both lose money in forward testing.
This commit is contained in:
2026-07-21 09:18:49 +08:00
parent 3141c392de
commit 367c4724e8
2 changed files with 77 additions and 28 deletions
+25 -5
View File
@@ -351,7 +351,7 @@ async function renderWallets() {
}
let html = '<table style="width:100%"><thead><tr>' +
'<th>当前跟的</th><th>排名</th><th>copy_pnl</th><th>信念胜率</th><th>状态</th></tr></thead><tbody>';
'<th>当前跟的</th><th>排名</th><th>copy_pnl</th><th>前瞻30d</th><th>信念胜率</th><th>状态</th></tr></thead><tbody>';
for (const f of (w.followed || [])) {
let statusBadge, statusColor;
@@ -362,27 +362,47 @@ async function renderWallets() {
statusBadge = '❌ 已淘汰';
statusColor = 'var(--red)';
}
const fwdStr = f.fwd_realized != null
? (f.fwd_realized >= 0 ? '+$' + f.fwd_realized.toFixed(0) : '-$' + Math.abs(f.fwd_realized).toFixed(0))
: '?';
const fwdColor = f.fwd_realized != null
? (f.fwd_realized >= 0 ? 'var(--green)' : 'var(--red)')
: 'var(--dim)';
html += `<tr>
<td>${f.name}</td>
<td>${f.rank ? '#' + f.rank : '-'}</td>
<td>${f.copy_pnl != null ? '+' + f.copy_pnl : '-'}</td>
<td style="color:${fwdColor}">${fwdStr}</td>
<td>${f.conv_win != null ? f.conv_win + '%' : '-'}</td>
<td style="color:${statusColor}">${statusBadge}</td>
</tr>`;
}
html += '</tbody></table>';
// 建议加的
// 建议加的(前瞻盈利的)
const suggestions = w.suggestions || [];
if (suggestions.length > 0) {
html += '<div style="margin-top:10px;color:var(--dim);font-size:12px">建议加(watch_sharps 排名6 未跟:</div>';
html += '<div style="margin-top:10px;color:var(--dim);font-size:12px">建议加(前瞻盈利 + copy_pnl 8 未跟):</div>';
html += '<div style="margin-top:4px">';
for (const s of suggestions) {
html += `<span class="wallet-badge" style="background:#1a2438;color:var(--blue)">⬆️ ${s.name} · #${s.rank} · +${s.copy_pnl}${s.conv_win != null ? ' · ' + s.conv_win + '%' : ''}</span>`;
const fwdStr = s.fwd_realized != null ? '+$' + s.fwd_realized.toFixed(0) : '?';
html += `<span class="wallet-badge" style="background:#1a2438;color:var(--blue)">⬆️ ${s.name} · #${s.rank} · copy_pnl+${s.copy_pnl} · 前瞻${fwdStr}${s.conv_win != null ? ' · ' + s.conv_win + '%' : ''}</span>`;
}
html += '</div>';
} else {
html += '<div style="margin-top:10px;color:var(--green);font-size:12px">当前跟的钱包都在 watch_sharps 里,无需调整 ✓</div>';
html += '<div style="margin-top:10px;color:var(--green);font-size:12px">无前瞻盈利的未跟钱包,无需调整 ✓</div>';
}
// 警告:前瞻亏损的(copy_pnl 高但前瞻亏)
const warnings = w.warnings || [];
if (warnings.length > 0) {
html += '<div style="margin-top:8px;color:var(--dim);font-size:12px">⚠️ 不推荐(copy_pnl 高但前瞻亏损):</div>';
html += '<div style="margin-top:4px">';
for (const s of warnings) {
const fwdStr = s.fwd_realized != null ? '-$' + Math.abs(s.fwd_realized).toFixed(0) : '?';
html += `<span class="wallet-badge" style="background:#3a1a1a;color:var(--red)">❌ ${s.name} · #${s.rank} · copy_pnl+${s.copy_pnl} · 前瞻-${fwdStr}</span>`;
}
html += '</div>';
}
document.getElementById('wallets-content').innerHTML = html;
+52 -23
View File
@@ -173,8 +173,8 @@ class Handler(http.server.SimpleHTTPRequestHandler):
self._send_json_obj(daily)
def _serve_wallets(self):
"""钱包建议:当前跟的 vs watch_sharps 排名。"""
result = {"followed": [], "suggestions": []}
"""钱包建议:当前跟的 vs watch_sharps 排名 + 前瞻盈亏"""
result = {"followed": [], "suggestions": [], "warnings": []}
# 1. 读当前跟的钱包(config.live.json
live_cfg_path = os.path.join(ROOT, "config.live.json")
@@ -186,7 +186,7 @@ class Handler(http.server.SimpleHTTPRequestHandler):
except Exception:
pass
# 2. 读 watch_sharps.json 排名
# 2. 读 watch_sharps.json 排名copy_pnl = in-sample
sharps_path = os.path.join(HERE, "watch_sharps.json")
sharps = []
if os.path.exists(sharps_path):
@@ -199,47 +199,76 @@ class Handler(http.server.SimpleHTTPRequestHandler):
sharps_sorted = sorted(sharps, key=lambda w: w.get("copy_pnl", 0), reverse=True)
sharps_addrs = {w.get("wallet", "").lower(): i + 1 for i, w in enumerate(sharps_sorted)}
# 3. 标记当前跟的钱包
# 3. 读 portfolio.json 的前瞻 realizedout-of-sample,真实表现)
fwd_pnl = {} # wallet_addr_lower -> {realized, bets, wins}
portfolio_path = os.path.join(HERE, "portfolio.json")
if os.path.exists(portfolio_path):
try:
pf = json.load(open(portfolio_path))
for w in pf.get("wallets", []):
addr = w.get("wallet", "").lower()
if addr:
fwd_pnl[addr] = {
"realized": w.get("realized", 0),
"bets": w.get("bets", 0),
"won": w.get("won", 0),
"lost": w.get("lost", 0),
"sold": w.get("sold", 0),
}
except Exception:
pass
# 4. 标记当前跟的钱包(加前瞻数据)
for w in followed:
addr = w.get("wallet", "").lower()
rank = sharps_addrs.get(addr)
name = w.get("name") or addr[:10]
fwd = fwd_pnl.get(addr, {})
fwd_realized = fwd.get("realized")
entry = {
"name": name,
"rank": rank,
"copy_pnl": None,
"fwd_realized": fwd_realized,
"fwd_bets": fwd.get("bets", 0),
"conv_win": None,
"status": "ok"
}
if rank:
# 在 sharps 里
sharp_data = next((s for s in sharps_sorted
if s.get("wallet", "").lower() == addr), {})
result["followed"].append({
"name": name,
"rank": rank,
"copy_pnl": sharp_data.get("copy_pnl", 0),
"conv_win": sharp_data.get("conv_win"),
"status": "ok"
})
entry["copy_pnl"] = sharp_data.get("copy_pnl", 0)
entry["conv_win"] = sharp_data.get("conv_win")
else:
# 不在 sharps 里 = 被淘汰
result["followed"].append({
"name": name,
"rank": None,
"copy_pnl": None,
"status": "eliminated"
})
entry["status"] = "eliminated"
result["followed"].append(entry)
# 4. 建议加的钱包(在 sharps 里但没跟,排名前 6
# 5. 建议加的钱包(前瞻盈利 + copy_pnl 排名前 8 未跟
followed_addrs = {w.get("wallet", "").lower() for w in followed}
for i, s in enumerate(sharps_sorted):
addr = s.get("wallet", "").lower()
if addr in followed_addrs:
continue
if i >= 6: # 只推荐前 6 名
if i >= 8:
break
name = s.get("name") or addr[:10]
result["suggestions"].append({
fwd = fwd_pnl.get(addr, {})
fwd_realized = fwd.get("realized")
entry = {
"name": name,
"rank": i + 1,
"copy_pnl": s.get("copy_pnl", 0),
"fwd_realized": fwd_realized,
"fwd_bets": fwd.get("bets", 0),
"conv_win": s.get("conv_win"),
"wallet": addr
})
}
# 前瞻盈利 -> 建议;前瞻亏损 -> 警告;无前瞻数据 -> 未知
if fwd_realized is not None and fwd_realized > 0:
result["suggestions"].append(entry)
elif fwd_realized is not None and fwd_realized < 0:
result["warnings"].append(entry)
# 无前瞻数据的不推荐也不警告
self._send_json_obj(result)