BrainDock/src/web_dashboard.py

637 lines
20 KiB
Python

import httpx
"""
Trading Bot Web Dashboard
Real-time tracking of trades, swaps, and performance
"""
from fastapi import FastAPI, WebSocket
from fastapi.staticfiles import StaticFiles
from fastapi.responses import HTMLResponse, JSONResponse
import asyncio
import json
import logging
from datetime import datetime
from typing import Dict, List
import os
app = FastAPI(title="Trading Bot Dashboard")
# Logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Shared state (will be updated by main_ml.py)
# RESET: Start with CLEAN state (no old test data)
trading_state = {
'current_trades': {}, # {pair: {qty, price, entry_time, ...}} - EMPTY
'completed_trades': [], # History of closed trades - EMPTY
'swaps': [], # Swap history - EMPTY
'balance': {'USDT': 0.0},
'daily_pnl': 0.0,
'total_pnl': 0.0,
'trades_today': 0,
'wins_today': 0,
'losses_today': 0,
'portfolio_value_usd': 0.0,
'portfolio_value_chf': 0.0,
'last_update': datetime.now().isoformat()
}
# WebSocket connections for live updates
active_connections: List[WebSocket] = []
async def broadcast_update():
"""Broadcast state update to all connected WebSocket clients"""
for connection in active_connections:
try:
await connection.send_json(trading_state)
except:
pass
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
"""WebSocket endpoint for live updates"""
await websocket.accept()
active_connections.append(websocket)
try:
# Send initial state
await websocket.send_json(trading_state)
# Keep connection alive
while True:
await asyncio.sleep(1)
await websocket.send_json(trading_state)
except:
pass
finally:
active_connections.remove(websocket)
@app.get("/api/state")
async def get_state():
"""Get current trading state"""
return trading_state
@app.post("/api/clear")
async def clear_state():
"""RESET: Clear all historical data, start fresh"""
global trading_state
logger.info('🗑️ Dashboard state cleared')
trading_state = {
'current_trades': {},
'completed_trades': [],
'swaps': [],
'balance': {'USDT': 0.0},
'daily_pnl': 0.0,
'total_pnl': 0.0,
'trades_today': 0,
'wins_today': 0,
'losses_today': 0,
'portfolio_value_usd': 0.0,
'portfolio_value_chf': 0.0,
'last_update': datetime.now().isoformat()
}
await broadcast_update()
return {"status": "cleared"}
@app.post("/api/update")
async def update_state(data: dict):
"""Update trading state (called by main_ml.py)"""
global trading_state
# Explicitly set current_trades if provided (don't merge!)
if 'current_trades' in data:
trading_state['current_trades'] = data['current_trades']
data.pop('current_trades') # Remove so update() doesn't override
# Update rest of state
trading_state.update(data)
trading_state['last_update'] = datetime.now().isoformat()
logger.info(f'📊 Dashboard updated: USDT={trading_state["balance"].get("USDT", 0):.2f}, trades={len(trading_state.get("current_trades", {}))}')
# Broadcast to WebSocket clients
await broadcast_update()
return {"status": "updated"}
@app.post("/api/trade/buy")
async def record_buy(pair: str, qty: float, price: float, entry_time: str = None):
"""Record a buy trade"""
if entry_time is None:
entry_time = datetime.now().isoformat()
trading_state['current_trades'][pair] = {
'qty': qty,
'price': price,
'entry_time': entry_time,
'type': 'BUY'
}
trading_state['trades_today'] += 1
await broadcast_update()
return {"status": "recorded"}
@app.post("/api/trade/sell")
async def record_sell(pair: str, qty: float, price: float, profit_usd: float, profit_pct: float, hold_time_min: float):
"""Record a sell trade"""
entry = trading_state['current_trades'].pop(pair, {})
completed = {
'pair': pair,
'qty': qty,
'entry_price': entry.get('entry_price', 0),
'exit_price': price,
'profit_usd': profit_usd,
'profit_pct': profit_pct,
'hold_time_min': hold_time_min,
'entry_time': entry.get('entry_time', ''),
'exit_time': datetime.now().isoformat()
}
trading_state['completed_trades'].append(completed)
trading_state['daily_pnl'] += profit_usd
trading_state['total_pnl'] += profit_usd
if profit_pct >= 0:
trading_state['wins_today'] += 1
else:
trading_state['losses_today'] += 1
# Keep last 100 trades in history
if len(trading_state['completed_trades']) > 100:
trading_state['completed_trades'] = trading_state['completed_trades'][-100:]
await broadcast_update()
return {"status": "recorded"}
@app.post("/api/swap")
async def record_swap(from_asset: str, to_asset: str, qty: float, rate: float):
"""Record a swap transaction"""
swap_entry = {
'from': from_asset,
'to': to_asset,
'qty': qty,
'rate': rate,
'timestamp': datetime.now().isoformat()
}
trading_state['swaps'].append(swap_entry)
# Keep last 50 swaps in history
if len(trading_state['swaps']) > 50:
trading_state['swaps'] = trading_state['swaps'][-50:]
await broadcast_update()
return {"status": "recorded"}
@app.post("/api/liquidate")
async def trigger_liquidation():
"""FORCE LIQUIDATE: Marc calls this to sell ALL holdings immediately"""
if bot_instance is None:
return {"status": "error", "message": "Bot not running"}
logger.warning("🔥 MANUAL LIQUIDATION TRIGGERED")
result = await bot_instance.force_liquidate_all()
# Update dashboard with result
await broadcast_update()
return result
@app.get("/")
async def get_dashboard():
"""Serve web dashboard HTML"""
return HTMLResponse(html_content)
# HTML Dashboard
html_content = """
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Trading Bot Dashboard</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #0f0c29 0%, #302b63 100%);
color: #e0e0e0;
padding: 20px;
min-height: 100vh;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
.header {
text-align: center;
margin-bottom: 30px;
border-bottom: 2px solid #6c63ff;
padding-bottom: 20px;
}
.header h1 {
font-size: 2.5em;
color: #6c63ff;
margin-bottom: 10px;
}
.status {
display: inline-block;
padding: 8px 16px;
background: #00c853;
color: white;
border-radius: 20px;
font-weight: bold;
font-size: 0.9em;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.card {
background: rgba(255, 255, 255, 0.1);
border: 1px solid #6c63ff;
border-radius: 10px;
padding: 20px;
backdrop-filter: blur(10px);
transition: all 0.3s ease;
}
.card:hover {
background: rgba(255, 255, 255, 0.15);
border-color: #00c853;
transform: translateY(-5px);
}
.card-title {
color: #6c63ff;
font-size: 0.9em;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 10px;
}
.card-value {
font-size: 2em;
font-weight: bold;
color: #e0e0e0;
margin-bottom: 5px;
}
.card-sub {
color: #a0a0a0;
font-size: 0.85em;
}
.positive {
color: #00c853;
}
.negative {
color: #ff3d00;
}
.section {
margin-bottom: 30px;
}
.section-title {
color: #6c63ff;
font-size: 1.5em;
margin-bottom: 15px;
border-bottom: 1px solid #6c63ff;
padding-bottom: 10px;
}
.trades-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 15px;
}
.trade-card {
background: rgba(255, 255, 255, 0.08);
border-left: 4px solid #6c63ff;
border-radius: 5px;
padding: 15px;
font-size: 0.9em;
}
.trade-card.open {
border-left-color: #6c63ff;
}
.trade-card.closed {
border-left-color: #00c853;
}
.trade-pair {
font-weight: bold;
color: #6c63ff;
margin-bottom: 8px;
}
.trade-info {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
font-size: 0.85em;
color: #a0a0a0;
}
.trade-info strong {
color: #e0e0e0;
}
.swap-item {
background: rgba(255, 255, 255, 0.05);
padding: 10px;
border-radius: 5px;
margin-bottom: 8px;
font-size: 0.85em;
}
.performance-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
}
.stat-card {
background: rgba(108, 99, 255, 0.2);
border: 1px solid #6c63ff;
border-radius: 8px;
padding: 15px;
text-align: center;
}
.stat-label {
color: #a0a0a0;
font-size: 0.8em;
text-transform: uppercase;
margin-bottom: 8px;
}
.stat-value {
font-size: 1.8em;
font-weight: bold;
color: #00c853;
}
.stat-value.loss {
color: #ff3d00;
}
.update-time {
text-align: right;
color: #666;
font-size: 0.8em;
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #333;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.fade-in {
animation: fadeIn 0.3s ease-in;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🤖 Trading Bot Dashboard</h1>
<span class="status">● LIVE</span>
</div>
<!-- Key Metrics -->
<div class="grid">
<div class="card">
<div class="card-title">Liquid USDT</div>
<div class="card-value" id="usdt">0.00</div>
<div class="card-sub">Available Balance</div>
</div>
<div class="card">
<div class="card-title">Portfolio Value</div>
<div class="card-value" id="portfolio">$0.00</div>
<div class="card-sub">All Assets USD</div>
</div>
<div class="card">
<div class="card-title">Daily P&L</div>
<div class="card-value" id="daily-pnl">$0.00</div>
<div class="card-sub">Today's Profit/Loss</div>
</div>
<div class="card">
<div class="card-title">Total P&L</div>
<div class="card-value" id="total-pnl">$0.00</div>
<div class="card-sub">Lifetime Profit/Loss</div>
</div>
</div>
<!-- Performance Stats -->
<div class="section">
<div class="section-title">📊 Performance</div>
<div class="performance-grid">
<div class="stat-card">
<div class="stat-label">Trades Today</div>
<div class="stat-value" id="trades-count">0</div>
</div>
<div class="stat-card">
<div class="stat-label">Win Rate</div>
<div class="stat-value" id="win-rate">0%</div>
</div>
<div class="stat-card">
<div class="stat-label">Wins</div>
<div class="stat-value" id="wins">0</div>
</div>
<div class="stat-card">
<div class="stat-label">Losses</div>
<div class="stat-value loss" id="losses">0</div>
</div>
</div>
</div>
<!-- Open Trades -->
<div class="section">
<div class="section-title">📈 Open Trades</div>
<div class="trades-list" id="open-trades">
<div class="trade-card open">
<div class="trade-pair">No open trades</div>
</div>
</div>
</div>
<!-- Recent Closed Trades -->
<div class="section">
<div class="section-title">✅ Recent Closed Trades</div>
<div class="trades-list" id="closed-trades">
<div class="trade-card closed">
<div class="trade-pair">No closed trades yet</div>
</div>
</div>
</div>
<!-- Swaps -->
<div class="section">
<div class="section-title">🔄 Recent Swaps</div>
<div id="swaps-list">
<div class="swap-item">No swaps yet</div>
</div>
</div>
<div class="update-time">
Last update: <span id="update-time">--:--:--</span>
</div>
</div>
<script>
function formatCurrency(value) {
return new Intl.NumberFormat('de-CH', {
style: 'currency',
currency: 'USD'
}).format(value);
}
function formatPercent(value) {
return value.toFixed(1) + '%';
}
function updateDashboard(data) {
// Update key metrics - CHF PRIMARY, USD secondary
const usdt = data.balance.USDT || 0;
const usdt_chf = usdt * 0.84;
document.getElementById('usdt').textContent = `$${usdt.toFixed(2)}`;
const portfolio_usd = data.portfolio_value_usd || 0;
const portfolio_chf = data.portfolio_value_chf || (portfolio_usd * 0.84);
document.getElementById('portfolio').textContent = `$${portfolio_usd.toFixed(2)}`;
// Update P&L with color - CHF primary
const dailyPnl = data.daily_pnl || 0;
const dailyPnl_chf = dailyPnl * 0.84;
const dailyPnlEl = document.getElementById('daily-pnl');
dailyPnlEl.textContent = `$${dailyPnl >= 0 ? '+' : ''}${dailyPnl.toFixed(2)}`;
dailyPnlEl.className = 'card-value ' + (dailyPnl >= 0 ? 'positive' : 'negative');
const totalPnl = data.total_pnl || 0;
const totalPnl_chf = totalPnl * 0.84;
const totalPnlEl = document.getElementById('total-pnl');
totalPnlEl.textContent = `$${totalPnl >= 0 ? '+' : ''}${totalPnl.toFixed(2)}`;
totalPnlEl.className = 'card-value ' + (totalPnl >= 0 ? 'positive' : 'negative');
// Update performance
document.getElementById('trades-count').textContent = data.trades_today || 0;
document.getElementById('wins').textContent = data.wins_today || 0;
document.getElementById('losses').textContent = data.losses_today || 0;
const total_trades = (data.wins_today || 0) + (data.losses_today || 0);
const win_rate = total_trades > 0 ? ((data.wins_today || 0) / total_trades * 100) : 0;
document.getElementById('win-rate').textContent = formatPercent(win_rate);
// Update open trades
const openTradesHtml = Object.entries(data.current_trades || {})
.map(([pair, trade]) => `
<div class="trade-card open fade-in">
<div class="trade-pair">${pair}</div>
<div class="trade-info">
<div><strong>Qty:</strong> ${trade.qty?.toFixed(8)}</div>
<div><strong>Price:</strong> ${formatCurrency(trade.entry_price)}</div>
<div><strong>Entry:</strong> ${new Date(trade.entry_time).toLocaleTimeString('de-CH')}</div>
</div>
</div>
`)
.join('');
const openTradesEl = document.getElementById('open-trades');
openTradesEl.innerHTML = openTradesHtml || '<div class="trade-card open"><div class="trade-pair">No open trades</div></div>';
// Update closed trades (last 10)
const closedTradesHtml = (data.completed_trades || []).slice(-10).reverse()
.map(trade => `
<div class="trade-card closed fade-in">
<div class="trade-pair">${trade.pair}</div>
<div class="trade-info">
<div><strong>Entry:</strong> ${formatCurrency(trade.entry_price)}</div>
<div><strong>Exit:</strong> ${formatCurrency(trade.exit_price)}</div>
<div><strong>Profit:</strong> <span class="${trade.profit_usd >= 0 ? 'positive' : 'negative'}">${formatCurrency(trade.profit_usd)} (${trade.profit_pct >= 0 ? '+' : ''}${trade.profit_pct.toFixed(2)}%)</span></div>
<div><strong>Hold:</strong> ${trade.hold_time_min?.toFixed(0)} min</div>
</div>
</div>
`)
.join('');
const closedTradesEl = document.getElementById('closed-trades');
closedTradesEl.innerHTML = closedTradesHtml || '<div class="trade-card closed"><div class="trade-pair">No closed trades yet</div></div>';
// Update swaps (last 10)
const swapsHtml = (data.swaps || []).slice(-10).reverse()
.map(swap => `
<div class="swap-item">
<strong>${swap.from} → ${swap.to}:</strong> ${swap.qty?.toFixed(8)} @ ${swap.rate?.toFixed(8)}
<br><span style="color: #666;">${new Date(swap.timestamp).toLocaleTimeString('de-CH')}</span>
</div>
`)
.join('');
const swapsEl = document.getElementById('swaps-list');
swapsEl.innerHTML = swapsHtml || '<div class="swap-item">No swaps yet</div>';
// Update time
document.getElementById('update-time').textContent = new Date().toLocaleTimeString('de-CH');
}
// WebSocket connection
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const ws = new WebSocket(protocol + '//' + window.location.host + '/ws');
ws.onmessage = function(event) {
const data = JSON.parse(event.data);
updateDashboard(data);
};
ws.onerror = function(error) {
console.error('WebSocket error:', error);
// Fallback to polling
setInterval(async () => {
const response = await fetch('/api/state');
const data = await response.json();
updateDashboard(data);
}, 1000);
};
// Initial load
fetch('/api/state')
.then(r => r.json())
.then(data => updateDashboard(data));
</script>
</body>
</html>
"""
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=7000)