Bot auto-update: src/__pycache__/main_ml.cpython-310.pyc,src/main_ml.py,src/main_ml.py.bak,src/main_ml_v6.py,src/web_dashboard.py
This commit is contained in:
parent
2dbca544b9
commit
461af80975
Binary file not shown.
1140
src/main_ml.py
1140
src/main_ml.py
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,200 @@
|
|||
#!/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())
|
||||
|
|
@ -1,657 +1,104 @@
|
|||
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
|
||||
#!/usr/bin/env python3
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import HTMLResponse
|
||||
from datetime import datetime
|
||||
from typing import Dict, List
|
||||
import os
|
||||
import asyncio
|
||||
|
||||
app = FastAPI(title="Trading Bot Dashboard")
|
||||
app = FastAPI()
|
||||
|
||||
# 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()
|
||||
"current_trades": {},
|
||||
"completed_trades": [],
|
||||
"balance": {"USDT": 0.0},
|
||||
"trades_today": 0,
|
||||
"daily_pnl": 0.0,
|
||||
"wins_today": 0,
|
||||
"last_update": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
# WebSocket connections for live updates
|
||||
active_connections: List[WebSocket] = []
|
||||
@app.post("/api/update")
|
||||
async def update(data: dict):
|
||||
global trading_state
|
||||
trading_state = data
|
||||
trading_state["last_update"] = datetime.now().isoformat()
|
||||
return {"status": "ok"}
|
||||
|
||||
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
|
||||
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('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")
|
||||
async def get_state():
|
||||
"""Get current 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)
|
||||
|
||||
# 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.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>
|
||||
async def dashboard():
|
||||
html = """<!DOCTYPE html><html><head><title>Bot</title><style>
|
||||
body{background:#0a0e27;color:#fff;font-family:monospace;margin:0;padding:20px}
|
||||
h1{color:#00ff88}
|
||||
.metrics{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:20px;margin:30px 0}
|
||||
.metric{background:rgba(255,255,255,0.1);border:1px solid #00ff88;padding:20px;border-radius:5px}
|
||||
.metric-label{color:#aaa;font-size:12px}
|
||||
.metric-value{font-size:24px;color:#00ff88;margin-top:10px}
|
||||
.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}
|
||||
.profit-pos{color:#00ff88}
|
||||
.profit-neg{color:#ff4444}
|
||||
</style></head><body><div style="max-width:1200px;margin:0 auto">
|
||||
<h1>Trading Bot Dashboard</h1>
|
||||
<div class="metrics">
|
||||
<div class="metric"><div class="metric-label">LIQUID USDT</div><div class="metric-value">$<span id="usdt">0.00</span></div></div>
|
||||
<div class="metric"><div class="metric-label">TRADES TODAY</div><div class="metric-value"><span id="trades">0</span></div></div>
|
||||
<div class="metric"><div class="metric-label">DAILY P&L</div><div class="metric-value"><span id="pnl">$0.00</span></div></div>
|
||||
<div class="metric"><div class="metric-label">WIN RATE</div><div class="metric-value"><span id="wr">0%</span></div></div>
|
||||
<div class="metric"><div class="metric-label">OPEN</div><div class="metric-value"><span id="open">0</span></div></div>
|
||||
</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>
|
||||
<div style="margin-top:30px;font-size:12px;color:#666">Last update: <span id="last">-</span></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function formatCurrency(value) {
|
||||
return new Intl.NumberFormat('de-CH', {
|
||||
style: 'currency',
|
||||
currency: 'USD'
|
||||
}).format(value);
|
||||
async function refresh(){
|
||||
try{
|
||||
const r = await fetch("/api/state");
|
||||
const d = await r.json();
|
||||
|
||||
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;
|
||||
|
||||
function formatPercent(value) {
|
||||
return value.toFixed(1) + '%';
|
||||
let closed_html = "";
|
||||
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>";
|
||||
}
|
||||
}
|
||||
|
||||
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.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>
|
||||
"""
|
||||
document.getElementById("closed_trades").innerHTML = closed_html;
|
||||
}catch(e){}
|
||||
setTimeout(refresh, 1000);
|
||||
}
|
||||
refresh();
|
||||
</script></body></html>""";
|
||||
return HTMLResponse(html)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
|
|
|||
Loading…
Reference in New Issue