Files
my-polymarket-copytrader/src/dashboard.py
T

162 lines
5.5 KiB
Python

"""FastAPI dashboard for the copy-trader.
Endpoints:
/api/stats — overall counters
/api/wallets — top-priority wallet pool
/api/signals/recent — recent consensus signals
/api/health — liveness check
"""
import logging
from typing import Optional
from fastapi import FastAPI, Query
from fastapi.responses import HTMLResponse
from src.config import get_settings
from src.db.database import CopyTraderDatabase
logger = logging.getLogger(__name__)
app = FastAPI(title="Polymarket Copy Trader Dashboard")
def _get_db() -> CopyTraderDatabase:
settings = get_settings()
return CopyTraderDatabase(settings.db_path)
@app.get("/api/stats")
def api_stats():
db = _get_db()
return db.get_stats()
@app.get("/api/wallets")
def api_wallets(limit: int = Query(50, ge=1, le=200)):
db = _get_db()
wallets = db.get_all_wallet_targets()
return wallets[:limit]
@app.get("/api/signals/recent")
def api_signals(limit: int = Query(20, ge=1, le=200)):
db = _get_db()
return db.get_recent_signals(limit=limit)
@app.get("/api/health")
def api_health():
return {"status": "ok"}
HTML_TEMPLATE = """<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Polymarket Copy Trader Dashboard</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, sans-serif; background: #0f1117; color: #e0e0e0; padding: 20px; }
h1 { color: #fff; margin-bottom: 8px; }
h2 { color: #a0a8c0; margin: 24px 0 12px; font-size: 1.2em; }
.subtitle { color: #666; margin-bottom: 24px; }
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; margin-bottom: 24px; }
.card { background: #1a1d28; border-radius: 8px; padding: 16px; text-align: center; }
.card .v { font-size: 1.8em; font-weight: bold; color: #4fc3f7; }
.card .l { color: #888; font-size: 0.85em; margin-top: 4px; }
table { width: 100%; border-collapse: collapse; margin-bottom: 24px; }
th { background: #1a1d28; color: #a0a8c0; text-align: left; padding: 10px 12px; }
td { padding: 10px 12px; border-bottom: 1px solid #222; font-size: 0.9em; }
tr:hover { background: #1a1d28; }
.badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.8em; font-weight: bold; }
.b-high { background: #66bb6a33; color: #66bb6a; }
.b-med { background: #ffb74d33; color: #ffb74d; }
.b-low { background: #88888833; color: #aaa; }
.mono { font-family: monospace; font-size: 0.85em; color: #888; }
#loading { color: #666; text-align: center; padding: 40px; }
</style>
</head>
<body>
<h1>Polymarket Copy Trader</h1>
<p class="subtitle" id="update-info">Live signal & wallet dashboard · auto-refresh 30s</p>
<div id="loading">Loading...</div>
<div id="content" style="display:none">
<div class="stats" id="stats"></div>
<h2>Top Wallets</h2>
<table id="wallet-table">
<thead><tr><th>Rank</th><th>Address</th><th>PnL Total</th><th>Trades</th><th>Categories</th><th>Health</th><th>Credibility</th></tr></thead>
<tbody></tbody>
</table>
<h2>Recent Signals</h2>
<table id="signal-table">
<thead><tr><th>Time</th><th>Market</th><th>Direction</th><th>Entry</th><th>Strength</th><th>Wallets</th><th>Kelly Size</th></tr></thead>
<tbody></tbody>
</table>
</div>
<script>
async function load() {
try {
const [statsR, walletsR, signalsR] = await Promise.all([
fetch('/api/stats').then(r => r.json()),
fetch('/api/wallets').then(r => r.json()),
fetch('/api/signals/recent?limit=20').then(r => r.json())
]);
document.getElementById('stats').innerHTML = [
['Wallets', statsR.wallet_count],
['Total Signals', statsR.total_signals],
['Executed', statsR.executed_signals]
].map(([l,v]) => `<div class="card"><div class="v">${v}</div><div class="l">${l}</div></div>`).join('');
document.querySelector('#wallet-table tbody').innerHTML = walletsR.map((w,i) =>
`<tr>
<td>${i+1}</td>
<td class="mono">${(w.address||'').slice(0,8)}...${(w.address||'').slice(-4)}</td>
<td>$${Number(w.pnl_total_usd||0).toLocaleString('en-US',{maximumFractionDigits:0})}</td>
<td>${w.trades_count}</td>
<td>${w.categories_count}</td>
<td>${(w.health_score||0).toFixed(1)}</td>
<td>${(w.credibility||0).toFixed(3)}</td>
</tr>`
).join('');
document.querySelector('#signal-table tbody').innerHTML = signalsR.map(s => {
const cls = s.aggregated_strength > 0.7 ? 'b-high' : s.aggregated_strength > 0.4 ? 'b-med' : 'b-low';
return `<tr>
<td>${(s.generated_at||'').slice(0,16)}</td>
<td>${(s.market_question||'').slice(0,60)}</td>
<td>${s.side} ${s.outcome}</td>
<td>${Number(s.entry_price||0).toFixed(4)}</td>
<td><span class="badge ${cls}">${(s.aggregated_strength*100).toFixed(0)}%</span></td>
<td>${s.n_contributors||'?'}</td>
<td>$${Number(s.suggested_size_usd||0).toFixed(0)}</td>
</tr>`;
}).join('');
document.getElementById('loading').style.display = 'none';
document.getElementById('content').style.display = 'block';
document.getElementById('update-info').textContent =
`Last refresh: ${new Date().toLocaleTimeString()} · auto-refresh 30s`;
} catch(e) {
document.getElementById('loading').textContent = 'Error: ' + e.message;
}
}
load();
setInterval(load, 30000);
</script>
</body>
</html>"""
@app.get("/", response_class=HTMLResponse)
def dashboard_page():
return HTML_TEMPLATE