Bot auto-update: src/web_dashboard.py

This commit is contained in:
Marc Blatter 2026-07-04 16:45:01 +02:00
parent a8a44cf54a
commit a6a832a4e7
1 changed files with 153 additions and 94 deletions

View File

@ -1,107 +1,166 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse, FileResponse
from datetime import datetime from fastapi.staticfiles import StaticFiles
import asyncio from fastapi.middleware.cors import CORSMiddleware
import json
app = FastAPI() app = FastAPI()
trading_state = { app.add_middleware(
"current_trades": {}, CORSMiddleware,
"completed_trades": [], allow_origins=['*'],
"balance": {"USDT": 0.0}, allow_credentials=True,
"trades_today": 0, allow_methods=['*'],
"daily_pnl": 0.0, allow_headers=['*'],
"wins_today": 0, )
"last_update": datetime.now().isoformat()
# Global state
state = {
'current_trades': {},
'completed_trades': [],
'balance': {'USDT': {'free': 0.0}},
'trades_today': 0,
'daily_pnl': 0.0,
'wins_today': 0,
'losses_today': 0,
'last_update': '',
} }
@app.post("/api/update") @app.post('/api/update')
async def update(data: dict): async def update_state(data: dict):
global trading_state global state
trading_state = data state.update(data)
trading_state["last_update"] = datetime.now().isoformat() return {'status': 'ok'}
return {"status": "ok"}
@app.get("/api/state") @app.get('/api/state')
async def get_state(): async def get_state():
return trading_state return state
@app.get("/") @app.get('/', response_class=HTMLResponse)
async def dashboard(): async def dashboard():
html = """<!DOCTYPE html><html><head><title>Bot</title><style> html = '''<!DOCTYPE html>
body{background:#0a0e27;color:#fff;font-family:monospace;margin:0;padding:20px} <html>
h1{color:#00ff88} <head>
.metrics{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:20px;margin:30px 0} <title>Bot Dashboard</title>
.metric{background:rgba(255,255,255,0.1);border:1px solid #00ff88;padding:20px;border-radius:5px} <style>
.metric-label{color:#aaa;font-size:12px} * { box-sizing: border-box; }
.metric-value{font-size:24px;color:#00ff88;margin-top:10px} body { background: #0a0e27; color: #fff; font-family: monospace; margin: 0; padding: 20px; }
.trade-card{background:rgba(255,255,255,0.08);border:1px solid #444;padding:15px;margin:10px 0;border-radius:5px} h1 { color: #00ff88; margin: 0 0 30px 0; }
.trade-pair{font-size:16px;color:#00ff88;font-weight:bold} .metrics { display: grid; grid-template-columns: repeat(5, 1fr); gap: 20px; margin-bottom: 40px; }
.profit-pos{color:#00ff88} @media (max-width: 1200px) { .metrics { grid-template-columns: repeat(3, 1fr); } }
.profit-neg{color:#ff4444} @media (max-width: 768px) { .metrics { grid-template-columns: repeat(2, 1fr); } }
</style></head><body><div style="max-width:1200px;margin:0 auto"> .metric { background: rgba(255,255,255,0.1); border: 2px solid #00ff88; padding: 20px; border-radius: 8px; text-align: center; }
<h1>Trading Bot Dashboard</h1> .metric-label { color: #aaa; font-size: 11px; text-transform: uppercase; letter-spacing: 1px; }
<div class="metrics"> .metric-value { font-size: 28px; color: #00ff88; margin-top: 12px; font-weight: bold; }
<div class="metric"><div class="metric-label">LIQUID USDT</div><div class="metric-value">$<span id="usdt">0.00</span></div></div> .section { margin: 40px 0; }
<div class="metric"><div class="metric-label">TRADES TODAY</div><div class="metric-value"><span id="trades">0</span></div></div> .section h2 { color: #00ff88; font-size: 18px; margin: 0 0 15px 0; border-bottom: 1px solid #333; padding-bottom: 10px; }
<div class="metric"><div class="metric-label">DAILY P&L</div><div class="metric-value"><span id="pnl">$0.00</span></div></div> .trade-card { background: rgba(255,255,255,0.05); border-left: 3px solid #00ff88; padding: 15px; margin: 10px 0; border-radius: 4px; }
<div class="metric"><div class="metric-label">WIN RATE</div><div class="metric-value"><span id="wr">0%</span></div></div> .trade-pair { color: #00ff88; font-weight: bold; margin-bottom: 5px; }
<div class="metric"><div class="metric-label">OPEN</div><div class="metric-value"><span id="open">0</span></div></div> .trade-detail { color: #aaa; font-size: 12px; }
</div> .empty { color: #666; padding: 20px; text-align: center; }
<h2>Open Trades</h2><div id="open_trades"><div class="trade-card">No open trades</div></div> .footer { margin-top: 50px; padding-top: 20px; border-top: 1px solid #333; color: #666; font-size: 12px; }
<h2>Closed Trades</h2><div id="closed_trades"><div class="trade-card">No closed trades</div></div> </style>
<div style="margin-top:30px;font-size:12px;color:#666">Last update: <span id="last">-</span></div> </head>
</div> <body>
<script> <h1>Trading Bot Dashboard</h1>
async function refresh(){ <div class=metrics>
try{ <div class=metric><div class=metric-label>Liquid USDT</div><div class=metric-value id=usdt>bash.00</div></div>
const r = await fetch("http://172.16.1.168:7000/api/state"); <div class=metric><div class=metric-label>Trades Today</div><div class=metric-value id=trades>0</div></div>
const d = await r.json(); <div class=metric><div class=metric-label>Daily P&L</div><div class=metric-value id=pnl>bash.00</div></div>
<div class=metric><div class=metric-label>Win Rate</div><div class=metric-value id=wr>0%</div></div>
<div class=metric><div class=metric-label>Open Positions</div><div class=metric-value id=open>0</div></div>
</div>
const usdt_data = d.balance && d.balance.USDT ? d.balance.USDT : {free: 0}; <div class=section>
const usdt = parseFloat(usdt_data.free) || 0; <h2>Open Trades</h2>
console.log("USDT from API:", usdt_data, "parsed:", usdt); <div id=open_trades><div class=empty>No open trades</div></div>
document.getElementById("usdt").textContent = usdt.toFixed(2); </div>
document.getElementById("trades").textContent = d.trades_today || 0;
const wr = d.trades_today > 0 ? Math.round((d.wins_today || 0) / d.trades_today * 100) : 0; <div class=section>
document.getElementById("wr").textContent = wr + "%"; <h2>Closed Trades</h2>
<div id=closed_trades><div class=empty>No closed trades</div></div>
</div>
const pnl = d.daily_pnl || 0; <div class=footer>
document.getElementById("pnl").textContent = (pnl >= 0 ? "$" : "-$") + Math.abs(pnl).toFixed(2); Last update: <span id=last>-</span>
</div>
document.getElementById("open").textContent = Object.keys(d.current_trades || {}).length; <script>
document.getElementById("last").textContent = new Date(d.last_update).toLocaleTimeString(); async function refresh() {
try {
const resp = await fetch('/api/state');
const d = await resp.json();
let open_html = ""; // Update metrics
if(Object.keys(d.current_trades || {}).length === 0){ if (d.balance && d.balance.USDT) {
open_html = "<div class=\"trade-card\">No open trades</div>"; const usdt = parseFloat(d.balance.USDT.free) || 0;
}else{ document.getElementById('usdt').textContent = '$' + usdt.toFixed(2);
for(const p in d.current_trades){
const t = d.current_trades[p];
open_html += "<div class=\"trade-card\"><div class=\"trade-pair\">" + p + "</div><div>Qty: " + t.qty.toFixed(4) + " @ $" + t.buy_price.toFixed(2) + "</div></div>";
} }
}
document.getElementById("open_trades").innerHTML = open_html;
let closed_html = ""; document.getElementById('trades').textContent = (d.trades_today || 0);
if(!d.completed_trades || d.completed_trades.length === 0){
closed_html = "<div class=\"trade-card\">No closed trades</div>";
}else{
for(const t of (d.completed_trades || []).slice(-10).reverse()){
const p_class = t.profit_usd >= 0 ? "profit-pos" : "profit-neg";
closed_html += "<div class=\"trade-card\"><div class=\"trade-pair\">" + t.pair + "</div><div><span class=\"" + p_class + "\">" + t.profit_usd.toFixed(2) + "</span> (" + (t.profit_pct*100).toFixed(2) + "%)</div></div>";
}
}
document.getElementById("closed_trades").innerHTML = closed_html;
}catch(e){}
setTimeout(refresh, 1000);
}
refresh();
</script></body></html>""";
return HTMLResponse(html)
if __name__ == "__main__": const pnl = parseFloat(d.daily_pnl) || 0;
const pnlText = (pnl >= 0 ? '$' : '-$') + Math.abs(pnl).toFixed(2);
document.getElementById('pnl').textContent = pnlText;
const wr = d.trades_today > 0 ? Math.round(((d.wins_today || 0) / d.trades_today) * 100) : 0;
document.getElementById('wr').textContent = wr + '%';
const openCount = Object.keys(d.current_trades || {}).length;
document.getElementById('open').textContent = openCount;
if (d.last_update) {
const t = new Date(d.last_update);
document.getElementById('last').textContent = t.toLocaleTimeString();
}
// Open trades
const openDiv = document.getElementById('open_trades');
const trades = d.current_trades || {};
if (Object.keys(trades).length === 0) {
openDiv.innerHTML = '<div class=empty>No open trades</div>';
} else {
let html = '';
for (const pair in trades) {
const t = trades[pair];
html += '<div class=trade-card>';
html += '<div class=trade-pair>' + pair + '</div>';
html += '<div class=trade-detail>Qty: ' + parseFloat(t.qty).toFixed(4) + ' @ $' + parseFloat(t.buy_price).toFixed(2) + '</div>';
html += '</div>';
}
openDiv.innerHTML = html;
}
// Closed trades
const closedDiv = document.getElementById('closed_trades');
const closed = d.completed_trades || [];
if (closed.length === 0) {
closedDiv.innerHTML = '<div class=empty>No closed trades</div>';
} else {
let html = '';
for (const trade of closed.slice(-5)) {
const profit = parseFloat(trade.profit_usd) || 0;
const color = profit > 0 ? '#00ff88' : '#ff0047';
html += '<div class=trade-card style=border-left-color: + color + >';
html += '<div class=trade-pair>' + trade.pair + '</div>';
html += '<div class=trade-detail>' + profit.toFixed(2) + '$ (' + (parseFloat(trade.profit_pct) * 100).toFixed(1) + '%)</div>';
html += '</div>';
}
closedDiv.innerHTML = html;
}
} catch (e) {
console.error('Error:', e);
}
}
refresh();
setInterval(refresh, 1000);
</script>
</body>
</html>'''
return html
if __name__ == '__main__':
import uvicorn import uvicorn
uvicorn.run(app, host="0.0.0.0", port=7000) uvicorn.run(app, host='0.0.0.0', port=7000)