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:
+25
-5
@@ -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
@@ -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 的前瞻 realized(out-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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user