162 lines
5.5 KiB
Python
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
|