Bot auto-update: src/web_dashboard.py

This commit is contained in:
Marc Blatter 2026-07-29 11:05:01 +02:00
parent df2d0b4883
commit 0ba7aaded3
1 changed files with 309 additions and 38 deletions

View File

@ -1,9 +1,11 @@
#!/usr/bin/env python3
"""Trading Bot Dashboard v0.5.1 (RSI + Bollinger Bands HYBRID) - Auto-load 1-Day chart on page load"""
import sqlite3
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
from binance.client import Client
from datetime import datetime
import json, os, time, sqlite3
import json, os, time
app = FastAPI()
@ -16,49 +18,318 @@ with open('/home/marc/bot-deploy/.env') as f:
binance = Client(env.get('BINANCE_API_KEY_LIVE'), env.get('BINANCE_API_SECRET_LIVE'))
DB = '/home/marc/bot-deploy/pnl_charts.db'
def get_chart_data(days=1):
conn = sqlite3.connect(DB)
rows = conn.execute(f"""SELECT ts, pv, pp FROM history WHERE ts > {int(time.time()) - days*86400} ORDER BY ts""").fetchall()
conn.close()
return [(datetime.fromtimestamp(r[0]).strftime('%H:%M'), r[2]) for r in rows]
def init_db():
c = sqlite3.connect(DB).cursor()
c.execute("""CREATE TABLE IF NOT EXISTS history (ts INTEGER PRIMARY KEY, pv REAL, pu REAL, pp REAL, uf REAL, ap INTEGER)""")
sqlite3.connect(DB).commit()
@app.get('/')
async def root():
chart_1d = get_chart_data(1)
chart_7d = get_chart_data(7)
chart_30d = get_chart_data(30)
html = f'''<!DOCTYPE html>
<html><head><title>Trading Bot v0.6</title>
<style>body{{font-family:Arial;background:#1e1e1e;color:#d0d0d0;margin:0;padding:20px}}.container{{max-width:1200px;margin:auto}}.header{{margin-bottom:30px}}.header h1{{margin:0;color:#00ff88}}.section{{background:#2d2d2d;padding:15px;margin:15px 0;border-radius:5px}}.metrics{{display:flex;gap:15px}}.metric{{flex:1;background:#1e1e1e;padding:15px;border-left:3px solid #00ff88;border-radius:3px}}.metric-label{{font-size:12px;color:#888}}.metric-value{{font-size:20px;font-weight:bold;color:#00ff88;margin-top:5px}}</style>
</head><body><div class="container">
<div class="header"><h1>🤖 Trading Bot v0.6</h1><p>Contrarian Mean Reversion Strategy</p></div>
<div class="section"><h2>Analytics</h2>
<p>1-Day Chart: {len(chart_1d)} points</p>
<p>7-Day Chart: {len(chart_7d)} points</p>
<p>30-Day Chart: {len(chart_30d)} points</p>
</div>
<div class="section">
<div class="metrics">
<div class="metric"><div class="metric-label">1D Performance</div><div class="metric-value">+{chart_1d[-1][1] if chart_1d else 0:.2f}%</div></div>
<div class="metric"><div class="metric-label">7D Performance</div><div class="metric-value">+{chart_7d[-1][1] if chart_7d else 0:.2f}%</div></div>
<div class="metric"><div class="metric-label">30D Performance</div><div class="metric-value">+{chart_30d[-1][1] if chart_30d else 0:.2f}%</div></div>
</div>
</div>
</div></body></html>'''
return HTMLResponse(html)
init_db()
@app.get('/api/state')
async def state():
try:
acc = binance.get_account()
portfolio = sum(float(b['free']) * (1.0 if b['asset']=='USDT' else 0) for b in acc['balances'])
return {'portfolio': portfolio, 'status': 'ok'}
except:
return {'error': 'failed'}
bal = {}
for a in acc['balances']:
ast, free, locked = a['asset'], float(a['free']), float(a['locked'])
if free + locked > 1e-5:
bal[ast] = {'free': free, 'locked': locked, 'total': free + locked}
prices = {'USDT': 1.0}
for p in ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT']:
try:
t = binance.get_ticker(symbol=p)
prices[p.replace('USDT', '')] = float(t['lastPrice'])
except: pass
pv = sum(bal.get(a, {}).get('total', 0) * prices.get(a, 0) for a in ['BTC', 'ETH', 'SOL', 'BNB', 'XRP', 'USDT'])
uf = bal.get('USDT', {}).get('free', 0)
# Get latest P&L from database
try:
conn = sqlite3.connect('/home/marc/bot-deploy/pnl_charts.db')
row = conn.execute('SELECT pu, pp FROM history ORDER BY ts DESC LIMIT 1').fetchone()
conn.close()
if row:
pu, pp = row[0], row[1]
else:
pu, pp = 0.0, 0.0
except:
pu, pp = 0.0, 0.0
ap = 0
try:
with open('/home/marc/bot-deploy/active_trades.json') as f:
ap = json.load(f).get('count', 0)
except: pass
conn = sqlite3.connect(DB)
conn.execute("INSERT OR REPLACE INTO history VALUES (?, ?, ?, ?, ?, ?)", (int(time.time()), pv, pu, pp, uf, ap))
conn.commit()
conn.close()
return {'portfolio_value': round(pv, 2), 'pnl_usdt': round(pu, 2), 'pnl_pct': round(pp, 2), 'usdt_free': round(uf, 2), 'active_positions': ap, 'balance': bal, 'prices': prices}
except Exception as e:
return {'error': str(e)}
@app.get('/api/pnl-history')
async def history(hours: int = 24):
conn = sqlite3.connect(DB)
cutoff = int(time.time()) - hours * 3600
rows = conn.execute("SELECT ts, pp, pu FROM history WHERE ts > ? ORDER BY ts", (cutoff,)).fetchall()
conn.close()
ts_list, pcts, usdts = [], [], []
seen_ts = set()
for t, p, u in rows:
dt = datetime.fromtimestamp(t)
if hours <= 24:
ts = dt.strftime('%H:00')
else:
ts = dt.strftime('%d.%m.%y')
if ts in seen_ts:
continue
seen_ts.add(ts)
ts_list.append(ts)
pcts.append(round(p, 2))
usdts.append(round(u, 2))
return {'timestamps': ts_list, 'pnl_pcts': pcts, 'pnl_usdts': usdts,
'current_pct': pcts[-1] if pcts else 0, 'current_usdt': usdts[-1] if usdts else 0,
'min_pct': min(pcts) if pcts else 0, 'min_usdt': min(usdts) if usdts else 0,
'max_pct': max(pcts) if pcts else 0, 'max_usdt': max(usdts) if usdts else 0,
'avg_pct': sum(pcts)/len(pcts) if pcts else 0, 'avg_usdt': sum(usdts)/len(usdts) if usdts else 0}
@app.get('/')
async def dashboard():
html = """<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Trading Bot v0.5.1</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:Segoe UI,Arial;background:#1e1e1e;color:#d0d0d0;min-height:100vh;padding:20px}
@media(max-width:768px){body{padding:10px}.container{max-width:100%}}
.container{max-width:1400px;margin:0 auto}
.header{display:flex;justify-content:space-between;align-items:center;margin-bottom:30px;padding:20px;background:rgba(0,255,136,.05);border:1px solid rgba(0,255,136,.2);border-radius:10px}
@media(max-width:768px){.header{flex-direction:column;gap:15px;padding:15px}}
.header h1{font-size:28px;color:#00ff88}
@media(max-width:768px){.header h1{font-size:20px}}
.status{padding:8px 16px;background:rgba(0,255,136,.1);border:2px solid #00ff88;border-radius:20px;font-weight:bold}
.tabs{display:flex;gap:10px;margin-bottom:20px}
.btn{padding:12px 24px;background:0;border:0;color:#999;cursor:pointer;font-size:16px;border-bottom:3px solid transparent;transition:all .3s}
@media(max-width:768px){.btn{padding:10px 16px;font-size:14px}}
.btn:hover{color:#00ff88}
.btn.active{color:#00ff88;border-bottom-color:#00ff88}
.tab{display:none}
.tab.active{display:block}
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:20px;margin-bottom:30px}
@media(max-width:768px){.grid{grid-template-columns:1fr}}
.card{background:rgba(255,255,255,.03);border:1px solid rgba(0,255,136,.2);border-radius:10px;padding:20px;transition:all .3s}
@media(max-width:768px){.card{padding:15px}}
.card:hover{border-color:rgba(0,255,136,.5)}
.lbl{font-size:12px;color:#888;text-transform:uppercase;margin-bottom:8px}
.val{font-size:24px;color:#00ff88;font-weight:bold}
@media(max-width:768px){.val{font-size:20px}}
.sub{font-size:14px;color:#999}
.collapse-header{display:flex;justify-content:space-between;align-items:center;padding:15px 20px;background:transparent;border:1px solid rgba(0,255,136,.2);border-radius:10px;cursor:pointer;margin:20px 0 15px 0}
@media(max-width:768px){.collapse-header{padding:12px 15px}}
.collapse-header h3{color:#00ff88;font-size:16px;margin:0}
@media(max-width:768px){.collapse-header h3{font-size:14px}}
.collapse-toggle{color:#00ff88;font-size:20px}
.holdings{display:none;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:20px;margin-bottom:20px}
@media(max-width:768px){.holdings{grid-template-columns:1fr}}
.holdings.open{display:grid}
.chart-box{background:rgba(255,255,255,.03);border:1px solid rgba(0,255,136,.2);border-radius:10px;padding:20px}
@media(max-width:768px){.chart-box{padding:15px}}
.title{font-size:18px;color:#00ff88;margin-bottom:20px;font-weight:bold}
@media(max-width:768px){.title{font-size:14px}}
.times{display:flex;gap:10px;margin-bottom:20px;flex-wrap:wrap}
.time{padding:8px 16px;background:rgba(0,255,136,.1);border:1px solid rgba(0,255,136,.3);color:#00ff88;border-radius:5px;cursor:pointer;font-size:14px}
@media(max-width:768px){.time{padding:6px 12px;font-size:12px}}
.time:hover{background:rgba(0,255,136,.2)}
.time.active{background:rgba(0,255,136,.3)}
.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:15px;margin-top:20px}
@media(max-width:768px){.stats{grid-template-columns:repeat(2,1fr);gap:10px}}
.stat{background:rgba(0,255,136,.05);border:1px solid rgba(0,255,136,.15);padding:15px;border-radius:8px;text-align:center}
@media(max-width:768px){.stat{padding:12px}}
.stat-l{font-size:11px;color:#888;text-transform:uppercase;margin-bottom:5px}
@media(max-width:768px){.stat-l{font-size:9px}}
.stat-v{font-size:18px;color:#00ff88;font-weight:bold;display:block}
@media(max-width:768px){.stat-v{font-size:14px}}
.stat-sub{font-size:11px;color:#666;margin-top:3px;display:block}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div><h1>🤖 Trading Bot v0.5.1</h1><p>P&L Analytics</p></div>
<div class="status" id="st"> LOADING</div>
</div>
<div class="tabs">
<button class="btn active" onclick="switchTab(event, 'portfolio')">📊 Portfolio</button>
<button class="btn" onclick="switchTab(event, 'analytics')">📈 Analytics</button>
</div>
<div id="portfolio" class="tab active">
<div class="grid">
<div class="card"><div class="lbl">Portfolio</div><div class="val" id="pv">-</div></div>
<div class="card"><div class="lbl">P&L</div><div class="val" id="pl">-</div><div class="sub" id="pp">-</div></div>
<div class="card"><div class="lbl">USDT</div><div class="val" id="uf">-</div></div>
<div class="card"><div class="lbl">Trades</div><div class="val" id="tr">-</div></div>
</div>
<div class="collapse-header" onclick="toggleHoldings()">
<h3>Holdings</h3>
<span class="collapse-toggle" id="toggle-icon"></span>
</div>
<div class="grid holdings" id="holdings"></div>
</div>
<div id="analytics" class="tab">
<div class="chart-box">
<div class="title">📈 P&L Performance (Live)</div>
<div class="times">
<button class="time active" onclick="loadChart(24, event)">1 Day</button>
<button class="time" onclick="loadChart(168, event)">1 Week</button>
<button class="time" onclick="loadChart(720, event)">1 Month</button>
</div>
<canvas id="chart" height="100"></canvas>
<div class="stats">
<div class="stat">
<div class="stat-l">Current</div>
<span class="stat-v" id="cur-pct">-</span>
<span class="stat-sub" id="cur-usd">-</span>
</div>
<div class="stat">
<div class="stat-l">Min</div>
<span class="stat-v" id="min-pct">-</span>
<span class="stat-sub" id="min-usd">-</span>
</div>
<div class="stat">
<div class="stat-l">Max</div>
<span class="stat-v" id="max-pct">-</span>
<span class="stat-sub" id="max-usd">-</span>
</div>
<div class="stat">
<div class="stat-l">Avg</div>
<span class="stat-v" id="avg-pct">-</span>
<span class="stat-sub" id="avg-usd">-</span>
</div>
</div>
</div>
</div>
</div>
<script>
let chartObj = null;
function switchTab(e, tabName) {
document.querySelectorAll('.tab').forEach(el => el.classList.remove('active'));
document.querySelectorAll('.btn').forEach(el => el.classList.remove('active'));
document.getElementById(tabName).classList.add('active');
e.target.classList.add('active');
}
function toggleHoldings() {
const h = document.getElementById('holdings');
const i = document.getElementById('toggle-icon');
h.classList.toggle('open');
i.textContent = h.classList.contains('open') ? '' : '';
}
async function updatePortfolio() {
const res = await fetch('/api/state');
const data = await res.json();
if (data.error) return;
document.getElementById('pv').textContent = '$' + data.portfolio_value.toFixed(2);
document.getElementById('pl').textContent = '$' + data.pnl_usdt.toFixed(2);
document.getElementById('pp').textContent = data.pnl_pct.toFixed(2) + '%';
document.getElementById('uf').textContent = '$' + data.usdt_free.toFixed(2);
document.getElementById('tr').textContent = data.active_positions;
document.getElementById('st').textContent = '● LIVE';
const hh = document.getElementById('holdings');
hh.innerHTML = '';
for (const [asset, info] of Object.entries(data.balance)) {
if (asset !== 'USDT' && info.total > 1e-4) {
const price = data.prices[asset] || 0;
const usdValue = info.total * price;
hh.innerHTML += '<div class="card"><div class="lbl">' + asset + '</div><div class="val">' + info.total.toFixed(4) + '</div><div class="sub">≈ $' + usdValue.toFixed(2) + '</div></div>';
}
}
}
async function loadChart(hours, e) {
if (e) {
document.querySelectorAll('.time').forEach(b => b.classList.remove('active'));
e.target.classList.add('active');
}
const res = await fetch('/api/pnl-history?hours=' + hours);
const data = await res.json();
const ctx = document.getElementById('chart').getContext('2d');
if (chartObj) chartObj.destroy();
const col = data.current_pct >= 0 ? '#00ff88' : '#ff4444';
const bg = data.current_pct >= 0 ? 'rgba(0,255,136,0.1)' : 'rgba(255,68,68,0.1)';
chartObj = new Chart(ctx, {
type: 'line',
data: {
labels: data.timestamps,
datasets: [{
label: 'P&L %',
data: data.pnl_pcts,
borderColor: col,
backgroundColor: bg,
fill: true,
tension: 0.4,
pointRadius: 2,
pointBackgroundColor: col,
borderWidth: 2
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
plugins: { legend: { labels: { color: '#888' } } },
scales: {
y: { grid: { color: 'rgba(0,255,136,0.1)' }, ticks: { color: '#888' } },
x: { grid: { color: 'rgba(0,255,136,0.1)' }, ticks: { color: '#888' } }
}
}
});
const fmt = v => (v >= 0 ? '+' : '') + v.toFixed(2);
document.getElementById('cur-pct').textContent = data.current_pct.toFixed(2) + '%';
document.getElementById('cur-usd').textContent = '$' + fmt(data.current_usdt);
document.getElementById('min-pct').textContent = data.min_pct.toFixed(2) + '%';
document.getElementById('min-usd').textContent = '$' + fmt(data.min_usdt);
document.getElementById('max-pct').textContent = data.max_pct.toFixed(2) + '%';
document.getElementById('max-usd').textContent = '$' + fmt(data.max_usdt);
document.getElementById('avg-pct').textContent = data.avg_pct.toFixed(2) + '%';
document.getElementById('avg-usd').textContent = '$' + fmt(data.avg_usdt);
}
setInterval(updatePortfolio, 10000);
updatePortfolio();
loadChart(24, null);
</script>
</body>
</html>"""
return HTMLResponse(content=html)
if __name__ == '__main__':
import uvicorn