Compare commits

..

1 Commits

5 changed files with 1680 additions and 1455 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,200 +0,0 @@
#!/usr/bin/env python3
"""
Trading Bot V5 CLEAN Minimal, Reliable, Profitable
Architecture: Single trading loop, live dashboard updates
"""
import os
import asyncio
import aiohttp
from datetime import datetime
from binance.client import Client
from dotenv import load_dotenv
import logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
load_dotenv()
class TradingBotClean:
def __init__(self):
self.binance = Client(
os.getenv('BINANCE_API_KEY'),
os.getenv('BINANCE_API_SECRET')
)
self.pairs = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT']
# Trading state - SINGLE SOURCE OF TRUTH
self.current_trades = {}
self.completed_trades = []
self.balance = {}
self.trades_today = 0
self.daily_pnl = 0.0
self.total_pnl = 0.0
self.wins_today = 0
self.losses_today = 0
self.dashboard_url = 'http://localhost:7000/api/update'
self.TP = 1.01
self.SL = 0.97
self.BUY_AMOUNT = 0.5
self.MIN_ORDER = 10
logger.info('🤖 Bot CLEAN initialized')
async def update_balance(self):
"""Get current balance from Binance"""
try:
account = self.binance.get_account()
self.balance = {}
for asset in account['balances']:
free = float(asset['free'])
locked = float(asset['locked'])
if free + locked > 0:
self.balance[asset['asset']] = {
'free': free,
'locked': locked,
'total': free + locked
}
except Exception as e:
logger.error(f'Balance error: {e}')
async def get_ml_signal(self, pair, price):
"""Get ML trading signal"""
import random
return 'BUY' if random.random() > 0.95 else None
async def place_buy_order(self, pair, price):
"""Place BUY order"""
try:
usdt_free = self.balance.get('USDT', {}).get('free', 0)
qty_usdt = usdt_free * self.BUY_AMOUNT
if qty_usdt < self.MIN_ORDER:
return None
qty = qty_usdt / price
order = self.binance.order_market_buy(symbol=pair, quantity=qty)
logger.info(f'🟢 BUY: {pair} x{qty:.4f} @ ${price:.2f}')
self.current_trades[pair] = {
'qty': qty,
'buy_price': price,
'buy_time': datetime.now().isoformat(),
'order_id': order['orderId'],
}
self.trades_today += 1
return order
except Exception as e:
logger.error(f'Buy error {pair}: {e}')
return None
async def check_take_profit(self):
"""Check for +1% take profit"""
pairs_to_remove = []
for pair in list(self.current_trades.keys()):
try:
trade = self.current_trades[pair]
ticker = self.binance.get_symbol_ticker(symbol=pair)
current_price = float(ticker['price'])
profit_pct = (current_price / trade['buy_price']) - 1
if profit_pct >= (self.TP - 1): # +1%
logger.info(f'🎯 TP HIT: {pair} +{profit_pct*100:.2f}%')
sell_order = self.binance.order_market_sell(symbol=pair, quantity=trade['qty'])
sell_price = float(sell_order['fills'][0]['price']) if sell_order.get('fills') else current_price
profit_usd = (sell_price - trade['buy_price']) * trade['qty']
self.completed_trades.append({
'pair': pair,
'buy_price': trade['buy_price'],
'sell_price': sell_price,
'qty': trade['qty'],
'profit_usd': profit_usd,
'profit_pct': profit_pct,
'buy_time': trade['buy_time'],
'sell_time': datetime.now().isoformat()
})
self.daily_pnl += profit_usd
self.total_pnl += profit_usd
self.wins_today += 1
pairs_to_remove.append(pair)
except Exception as e:
logger.warning(f'TP check error {pair}: {e}')
for pair in pairs_to_remove:
del self.current_trades[pair]
async def send_to_dashboard(self):
"""Send state to dashboard"""
try:
state = {
'current_trades': self.current_trades,
'completed_trades': self.completed_trades[-20:],
'balance': self.balance,
'trades_today': self.trades_today,
'daily_pnl': self.daily_pnl,
'total_pnl': self.total_pnl,
'wins_today': self.wins_today,
'losses_today': self.losses_today,
'last_update': datetime.now().isoformat()
}
async with aiohttp.ClientSession() as session:
async with session.post(self.dashboard_url, json=state, timeout=2) as resp:
pass
except Exception as e:
logger.warning(f'Dashboard send error: {e}')
async def run(self):
"""Main trading loop"""
logger.info('🎯 Bot started')
while True:
try:
await self.update_balance()
for pair in self.pairs:
if pair in self.current_trades:
continue
try:
ticker = self.binance.get_symbol_ticker(symbol=pair)
price = float(ticker['price'])
signal = await self.get_ml_signal(pair, price)
if signal == 'BUY':
logger.info(f'🟢 BUY signal: {pair}')
await self.place_buy_order(pair, price)
except Exception as e:
pass
await self.check_take_profit()
await self.send_to_dashboard()
await asyncio.sleep(1)
except Exception as e:
logger.error(f'Loop error: {e}')
await asyncio.sleep(5)
async def main():
bot = TradingBotClean()
await bot.run()
if __name__ == '__main__':
asyncio.run(main())

View File

@ -1,104 +1,657 @@
#!/usr/bin/env python3 import httpx
from fastapi import FastAPI """
from fastapi.responses import HTMLResponse Trading Bot Web Dashboard
from datetime import datetime 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 asyncio
import json
import logging
from datetime import datetime
from typing import Dict, List
import os
app = FastAPI() 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 = { trading_state = {
"current_trades": {}, 'current_trades': {}, # {pair: {qty, price, entry_time, ...}} - EMPTY
"completed_trades": [], 'completed_trades': [], # History of closed trades - EMPTY
"balance": {"USDT": 0.0}, 'swaps': [], # Swap history - EMPTY
"trades_today": 0, 'balance': {'USDT': 0.0},
"daily_pnl": 0.0, 'daily_pnl': 0.0,
"wins_today": 0, 'total_pnl': 0.0,
"last_update": datetime.now().isoformat() 'trades_today': 0,
'wins_today': 0,
'losses_today': 0,
'portfolio_value_usd': 0.0,
'portfolio_value_chf': 0.0,
'last_update': datetime.now().isoformat()
} }
@app.post("/api/update") # WebSocket connections for live updates
async def update(data: dict): 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")
@app.get("/api/state")
async def get_state():
"""Get current trading state from Bot DIRECTLY"""
# Return ONLY what the Bot currently has
# NO caching, NO fallback to stale data
return trading_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 global trading_state
trading_state = data logger.info('🗑️ Dashboard state cleared')
trading_state["last_update"] = datetime.now().isoformat() trading_state = {
return {"status": "ok"} '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('buy_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('buy_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 dashboard HTML"""
return HTMLResponse(html_content)
@app.get("/api/state") @app.get("/api/state")
async def get_state(): async def get_state():
"""Get current trading state"""
return trading_state return trading_state
async def get_state():
"""Proxy to State Manager"""
try:
async with httpx.AsyncClient(timeout=3.0) as client:
resp = await client.get("http://localhost:7001/api/bot-state")
return resp.json()
except:
return trading_state
async def get_dashboard():
"""Serve web dashboard HTML"""
return HTMLResponse(html_content)
@app.get("/") # HTML Dashboard
async def dashboard(): html_content = """
html = """<!DOCTYPE html><html><head><title>Bot</title><style> <!DOCTYPE html>
body{background:#0a0e27;color:#fff;font-family:monospace;margin:0;padding:20px} <html lang="de">
h1{color:#00ff88} <head>
.metrics{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:20px;margin:30px 0} <meta charset="UTF-8">
.metric{background:rgba(255,255,255,0.1);border:1px solid #00ff88;padding:20px;border-radius:5px} <meta name="viewport" content="width=device-width, initial-scale=1.0">
.metric-label{color:#aaa;font-size:12px} <title>Trading Bot Dashboard</title>
.metric-value{font-size:24px;color:#00ff88;margin-top:10px} <style>
.trade-card{background:rgba(255,255,255,0.08);border:1px solid #444;padding:15px;margin:10px 0;border-radius:5px} * {
.trade-pair{font-size:16px;color:#00ff88;font-weight:bold} margin: 0;
.profit-pos{color:#00ff88} padding: 0;
.profit-neg{color:#ff4444} box-sizing: border-box;
</style></head><body><div style="max-width:1200px;margin:0 auto"> }
<h1>Trading Bot Dashboard</h1>
<div class="metrics"> body {
<div class="metric"><div class="metric-label">LIQUID USDT</div><div class="metric-value">$<span id="usdt">0.00</span></div></div> font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
<div class="metric"><div class="metric-label">TRADES TODAY</div><div class="metric-value"><span id="trades">0</span></div></div> background: linear-gradient(135deg, #0f0c29 0%, #302b63 100%);
<div class="metric"><div class="metric-label">DAILY P&L</div><div class="metric-value"><span id="pnl">$0.00</span></div></div> color: #e0e0e0;
<div class="metric"><div class="metric-label">WIN RATE</div><div class="metric-value"><span id="wr">0%</span></div></div> padding: 20px;
<div class="metric"><div class="metric-label">OPEN</div><div class="metric-value"><span id="open">0</span></div></div> min-height: 100vh;
</div> }
<h2>Open Trades</h2><div id="open_trades"><div class="trade-card">No open trades</div></div>
<h2>Closed Trades</h2><div id="closed_trades"><div class="trade-card">No closed trades</div></div> .container {
<div style="margin-top:30px;font-size:12px;color:#666">Last update: <span id="last">-</span></div> 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.25);
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: #ffffff;
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.25);
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: #ffffff;
}
.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: #ffffff;
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> </div>
<script> <script>
async function refresh(){ function formatCurrency(value) {
try{ return new Intl.NumberFormat('de-CH', {
const r = await fetch("/api/state"); style: 'currency',
const d = await r.json(); currency: 'USD'
}).format(value);
const usdt = (d.balance && d.balance.USDT) ? d.balance.USDT.free || 0 : 0;
document.getElementById("usdt").textContent = usdt.toFixed(2);
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;
document.getElementById("wr").textContent = wr + "%";
const pnl = d.daily_pnl || 0;
document.getElementById("pnl").textContent = (pnl >= 0 ? "$" : "-$") + Math.abs(pnl).toFixed(2);
document.getElementById("open").textContent = Object.keys(d.current_trades || {}).length;
document.getElementById("last").textContent = new Date(d.last_update).toLocaleTimeString();
let open_html = "";
if(Object.keys(d.current_trades || {}).length === 0){
open_html = "<div class=\"trade-card\">No open trades</div>";
}else{
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 = ""; function formatPercent(value) {
if(!d.completed_trades || d.completed_trades.length === 0){ return value.toFixed(1) + '%';
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){} function updateDashboard(data) {
setTimeout(refresh, 1000); // Update key metrics - CHF PRIMARY, USD secondary
} const usdt = data.balance.USDT || 0;
refresh(); const usdt_chf = usdt * 0.84;
</script></body></html>"""; document.getElementById('usdt').textContent = `$${usdt.toFixed(2)}`;
return HTMLResponse(html)
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.buy_price || trade.entry_price)}</div>
<div><strong>Entry:</strong> ${new Date(trade.buy_time || 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.buy_price || 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__": if __name__ == "__main__":
import uvicorn import uvicorn