""" 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 = """