337 lines
14 KiB
Python
337 lines
14 KiB
Python
#!/usr/bin/env python3
|
|
"""Trading Bot Dashboard v0.7.2 AGGRESSIVE (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
|
|
|
|
app = FastAPI()
|
|
|
|
env = {}
|
|
with open('/home/marc/bot-deploy/.env') as f:
|
|
for line in f:
|
|
k, _, v = line.partition('=')
|
|
env[k.strip()] = v.strip()
|
|
|
|
binance = Client(env.get('BINANCE_API_KEY_LIVE'), env.get('BINANCE_API_SECRET_LIVE'))
|
|
DB = '/home/marc/bot-deploy/pnl_charts.db'
|
|
|
|
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()
|
|
|
|
init_db()
|
|
|
|
@app.get('/api/state')
|
|
async def state():
|
|
try:
|
|
acc = binance.get_account()
|
|
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.7.2 AGGRESSIVE.2 AGGRESSIVE</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.7.2 AGGRESSIVE.2 AGGRESSIVE</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
|
|
uvicorn.run(app, host='0.0.0.0', port=7000)
|