Compare commits
2 Commits
9fb45ce565
...
461af80975
| Author | SHA1 | Date |
|---|---|---|
|
|
461af80975 | |
|
|
2dbca544b9 |
Binary file not shown.
1126
src/main_ml.py
1126
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
|
#!/usr/bin/env python3
|
||||||
"""
|
from fastapi import FastAPI
|
||||||
Trading Bot Web Dashboard
|
from fastapi.responses import HTMLResponse
|
||||||
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 datetime import datetime
|
||||||
from typing import Dict, List
|
import asyncio
|
||||||
import os
|
|
||||||
|
|
||||||
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 = {
|
trading_state = {
|
||||||
'current_trades': {}, # {pair: {qty, price, entry_time, ...}} - EMPTY
|
"current_trades": {},
|
||||||
'completed_trades': [], # History of closed trades - EMPTY
|
"completed_trades": [],
|
||||||
'swaps': [], # Swap history - EMPTY
|
"balance": {"USDT": 0.0},
|
||||||
'balance': {'USDT': 0.0},
|
"trades_today": 0,
|
||||||
'daily_pnl': 0.0,
|
"daily_pnl": 0.0,
|
||||||
'total_pnl': 0.0,
|
"wins_today": 0,
|
||||||
'trades_today': 0,
|
"last_update": datetime.now().isoformat()
|
||||||
'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
|
@app.post("/api/update")
|
||||||
active_connections: List[WebSocket] = []
|
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")
|
@app.get("/api/state")
|
||||||
async def get_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
|
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("/")
|
@app.get("/")
|
||||||
async def get_dashboard():
|
async def dashboard():
|
||||||
"""Serve dashboard HTML"""
|
html = """<!DOCTYPE html><html><head><title>Bot</title><style>
|
||||||
return HTMLResponse(html_content)
|
body{background:#0a0e27;color:#fff;font-family:monospace;margin:0;padding:20px}
|
||||||
|
h1{color:#00ff88}
|
||||||
@app.get("/api/state")
|
.metrics{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:20px;margin:30px 0}
|
||||||
async def get_state():
|
.metric{background:rgba(255,255,255,0.1);border:1px solid #00ff88;padding:20px;border-radius:5px}
|
||||||
"""Get current trading state"""
|
.metric-label{color:#aaa;font-size:12px}
|
||||||
return trading_state
|
.metric-value{font-size:24px;color:#00ff88;margin-top:10px}
|
||||||
async def get_state():
|
.trade-card{background:rgba(255,255,255,0.08);border:1px solid #444;padding:15px;margin:10px 0;border-radius:5px}
|
||||||
"""Proxy to State Manager"""
|
.trade-pair{font-size:16px;color:#00ff88;font-weight:bold}
|
||||||
try:
|
.profit-pos{color:#00ff88}
|
||||||
async with httpx.AsyncClient(timeout=3.0) as client:
|
.profit-neg{color:#ff4444}
|
||||||
resp = await client.get("http://localhost:7001/api/bot-state")
|
</style></head><body><div style="max-width:1200px;margin:0 auto">
|
||||||
return resp.json()
|
<h1>Trading Bot Dashboard</h1>
|
||||||
except:
|
<div class="metrics">
|
||||||
return trading_state
|
<div class="metric"><div class="metric-label">LIQUID USDT</div><div class="metric-value">$<span id="usdt">0.00</span></div></div>
|
||||||
async def get_dashboard():
|
<div class="metric"><div class="metric-label">TRADES TODAY</div><div class="metric-value"><span id="trades">0</span></div></div>
|
||||||
"""Serve web dashboard HTML"""
|
<div class="metric"><div class="metric-label">DAILY P&L</div><div class="metric-value"><span id="pnl">$0.00</span></div></div>
|
||||||
return HTMLResponse(html_content)
|
<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>
|
||||||
# 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>
|
</div>
|
||||||
|
<h2>Open Trades</h2><div id="open_trades"><div class="trade-card">No open trades</div></div>
|
||||||
<!-- Key Metrics -->
|
<h2>Closed Trades</h2><div id="closed_trades"><div class="trade-card">No closed trades</div></div>
|
||||||
<div class="grid">
|
<div style="margin-top:30px;font-size:12px;color:#666">Last update: <span id="last">-</span></div>
|
||||||
<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>
|
||||||
|
|
||||||
<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>
|
<script>
|
||||||
function formatCurrency(value) {
|
async function refresh(){
|
||||||
return new Intl.NumberFormat('de-CH', {
|
try{
|
||||||
style: 'currency',
|
const r = await fetch("/api/state");
|
||||||
currency: 'USD'
|
const d = await r.json();
|
||||||
}).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>";
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatPercent(value) {
|
|
||||||
return value.toFixed(1) + '%';
|
|
||||||
}
|
}
|
||||||
|
document.getElementById("open_trades").innerHTML = open_html;
|
||||||
|
|
||||||
function updateDashboard(data) {
|
let closed_html = "";
|
||||||
// Update key metrics - CHF PRIMARY, USD secondary
|
if(!d.completed_trades || d.completed_trades.length === 0){
|
||||||
const usdt = data.balance.USDT || 0;
|
closed_html = "<div class=\"trade-card\">No closed trades</div>";
|
||||||
const usdt_chf = usdt * 0.84;
|
}else{
|
||||||
document.getElementById('usdt').textContent = `$${usdt.toFixed(2)}`;
|
for(const t of (d.completed_trades || []).slice(-10).reverse()){
|
||||||
|
const p_class = t.profit_usd >= 0 ? "profit-pos" : "profit-neg";
|
||||||
const portfolio_usd = data.portfolio_value_usd || 0;
|
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>";
|
||||||
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
|
document.getElementById("closed_trades").innerHTML = closed_html;
|
||||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
}catch(e){}
|
||||||
const ws = new WebSocket(protocol + '//' + window.location.host + '/ws');
|
setTimeout(refresh, 1000);
|
||||||
|
}
|
||||||
ws.onmessage = function(event) {
|
refresh();
|
||||||
const data = JSON.parse(event.data);
|
</script></body></html>""";
|
||||||
updateDashboard(data);
|
return HTMLResponse(html)
|
||||||
};
|
|
||||||
|
|
||||||
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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue