Bot auto-update: src/__pycache__/main_ml.cpython-310.pyc,src/__pycache__/state_manager.cpython-310.pyc,src/main_ml.py,src/state_manager.py,src/web_dashboard.py

This commit is contained in:
Marc Blatter 2026-07-04 14:07:22 +02:00
parent 4e8d268e35
commit 882e5adaaf
5 changed files with 73 additions and 128 deletions

View File

@ -113,7 +113,7 @@ class MLTradingBot:
self.pairs = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT'] self.pairs = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT']
# Track open positions # Track open positions
self.open_positions = {} # CLEAN START - reset on bot restart self.current_trades = {} # CLEAN START - reset on bot restart
logger.info('🗑️ RESET: Cleared all stored positions (dashboard will show REAL Binance state only)') logger.info('🗑️ RESET: Cleared all stored positions (dashboard will show REAL Binance state only)')
@ -467,11 +467,11 @@ class MLTradingBot:
logger.warning(f'Failed to get price for {pair}: {e}') logger.warning(f'Failed to get price for {pair}: {e}')
continue continue
if pair not in self.open_positions: if pair not in self.current_trades:
logger.debug(f'⏭️ {pair} not in open_positions (keys: {list(self.open_positions.keys())})') logger.debug(f'⏭️ {pair} not in open_positions (keys: {list(self.current_trades.keys())})')
continue continue
pos = self.open_positions[pair] pos = self.current_trades[pair]
buy_price = pos['buy_price'] buy_price = pos['buy_price']
buy_qty = pos['qty'] buy_qty = pos['qty']
buy_time = datetime.fromisoformat(pos['buy_time']) buy_time = datetime.fromisoformat(pos['buy_time'])
@ -553,7 +553,7 @@ class MLTradingBot:
f'{icon} CLOSED {exit_reason}\n' f'{icon} CLOSED {exit_reason}\n'
f'{pair}: {current_qty:.8f} @ ${current_price:.2f}\n' f'{pair}: {current_qty:.8f} @ ${current_price:.2f}\n'
f'Profit: ${profit_usd:+.2f} ({profit_pct:+.2f}%)\n' f'Profit: ${profit_usd:+.2f} ({profit_pct:+.2f}%)\n'
f'Hold: {(datetime.now() - datetime.fromisoformat(self.open_positions[pair]["buy_time"])).total_seconds() / 60:.0f} min' f'Hold: {(datetime.now() - datetime.fromisoformat(self.current_trades[pair]["buy_time"])).total_seconds() / 60:.0f} min'
) )
else: else:
logger.info(f'Loss trade skipped from Telegram (visible on dashboard): {profit_pct:.2f}%') logger.info(f'Loss trade skipped from Telegram (visible on dashboard): {profit_pct:.2f}%')
@ -566,11 +566,11 @@ class MLTradingBot:
self.error_count = 0 self.error_count = 0
# Send to dashboard BEFORE deleting position (ONLY for REAL executed trades!) # Send to dashboard BEFORE deleting position (ONLY for REAL executed trades!)
hold_time_s = (datetime.now() - datetime.fromisoformat(self.open_positions[pair]["buy_time"])).total_seconds() hold_time_s = (datetime.now() - datetime.fromisoformat(self.current_trades[pair]["buy_time"])).total_seconds()
hold_time_min = hold_time_s / 60 hold_time_min = hold_time_s / 60
await self.dashboard.record_sell(pair, current_qty, current_price, profit_usd, profit_pct, hold_time_min) await self.dashboard.record_sell(pair, current_qty, current_price, profit_usd, profit_pct, hold_time_min)
del self.open_positions[pair] del self.current_trades[pair]
logger.info(f'✅ EXIT EXECUTED & RECORDED TO DASHBOARD!') logger.info(f'✅ EXIT EXECUTED & RECORDED TO DASHBOARD!')
else: else:
logger.warning(f'❌ EXIT order FAILED or returned no result for {pair} - NOT recording') logger.warning(f'❌ EXIT order FAILED or returned no result for {pair} - NOT recording')
@ -740,7 +740,7 @@ class MLTradingBot:
if result and result.get('status') in ['FILLED', 'NEW', 'PARTIALLY_FILLED']: if result and result.get('status') in ['FILLED', 'NEW', 'PARTIALLY_FILLED']:
# Ensure current_trades reflects completed trade # Ensure current_trades reflects completed trade
logger.info(f'🔍 DEBUG: Storing BUY position {pair}: qty={qty}, price={price}') logger.info(f'🔍 DEBUG: Storing BUY position {pair}: qty={qty}, price={price}')
self.open_positions[pair] = { self.current_trades[pair] = {
'qty': qty, 'qty': qty,
'buy_price': price, 'buy_price': price,
'buy_time': datetime.now().isoformat(), 'buy_time': datetime.now().isoformat(),
@ -748,7 +748,7 @@ class MLTradingBot:
'trailing_stop': None, 'trailing_stop': None,
'order_id': result.get('orderId', 'unknown') 'order_id': result.get('orderId', 'unknown')
} }
logger.info(f'🔍 DEBUG: After storage, open_positions keys = {list(self.open_positions.keys())}') logger.info(f'🔍 DEBUG: After storage, open_positions keys = {list(self.current_trades.keys())}')
self.trades_today += 1 self.trades_today += 1
self.error_count = 0 self.error_count = 0
@ -816,7 +816,7 @@ class MLTradingBot:
Max Drawdown: ${self.max_drawdown:.2f} (CHF {self.max_drawdown * 0.84:.2f}) Max Drawdown: ${self.max_drawdown:.2f} (CHF {self.max_drawdown * 0.84:.2f})
🤖 BOT STATUS: {'🟢 RUNNING' if time.time() >= self.error_cooldown_until else '🟡 ERROR_COOLDOWN'} 🤖 BOT STATUS: {'🟢 RUNNING' if time.time() >= self.error_cooldown_until else '🟡 ERROR_COOLDOWN'}
Open Positions: {len(self.open_positions)} Open Positions: {len(self.current_trades)}
Error Count: {self.error_count}/{self.error_threshold}''' Error Count: {self.error_count}/{self.error_threshold}'''
logger.info(report) logger.info(report)
@ -968,13 +968,13 @@ class MLTradingBot:
except: except:
pass pass
logger.info(f'✅ Sending to dashboard: USDT={usdt_live:.2f}, portfolio={portfolio_value_usd:.2f}, trades_today={self.trades_today}, open_trades={len(self.open_positions)}') logger.info(f'✅ Sending to dashboard: USDT={usdt_live:.2f}, portfolio={portfolio_value_usd:.2f}, trades_today={self.trades_today}, open_trades={len(self.current_trades)}')
# SYNC open_positions with dashboard # SYNC open_positions with dashboard
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.post('http://localhost:7000/api/update', json={ async with session.post('http://localhost:7000/api/update', json={
'balance': {'USDT': usdt_live}, 'balance': {'USDT': usdt_live},
'current_trades': self.open_positions, # Send ALL open positions! 'current_trades': self.current_trades, # Send ALL open positions!
'daily_pnl': self.daily_pnl, 'daily_pnl': self.daily_pnl,
'total_pnl': self.total_pnl, 'total_pnl': self.total_pnl,
'trades_today': self.trades_today, 'trades_today': self.trades_today,

View File

@ -1,194 +1,139 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
State Manager V2: Direct Binance Integration State Manager V3: Ultra-Simple Binance Direct
- Loads open positions from Binance API (source of truth) - Uses environment variables directly
- Computes portfolio value from real prices - No .env nonsense, uses os.environ
- Persists to JSON
- Provides REST API for dashboard
""" """
import asyncio import asyncio
import json import json
import logging import logging
import os
import sys
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
import uvicorn import uvicorn
from binance.client import Client from binance.client import Client
import os
from dotenv import load_dotenv
import time
# Setup # Read .env directly into os.environ BEFORE importing anything else
load_dotenv('/home/marc/bot-deploy/.env') env_file = '/home/marc/bot-deploy/.env'
for line in open(env_file).readlines():
line = line.strip()
if line and not line.startswith('#') and '=' in line:
k, v = line.split('=', 1)
os.environ[k] = v.strip('"').strip("'")
API_KEY = os.environ.get('BINANCE_API_KEY')
API_SECRET = os.environ.get('BINANCE_API_SECRET')
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
if not API_KEY or not API_SECRET:
logger.error(f"Missing credentials: key={bool(API_KEY)}, secret={bool(API_SECRET)}")
sys.exit(1)
logger.info(f"✅ API credentials loaded")
client = Client(API_KEY, API_SECRET)
app = FastAPI() app = FastAPI()
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]) app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
# Binance client
API_KEY = os.getenv('BINANCE_API_KEY')
API_SECRET = os.getenv('BINANCE_API_SECRET')
client = Client(API_KEY, API_SECRET)
# Persistence
DATA_DIR = Path('/home/marc/bot-deploy/data')
DATA_DIR.mkdir(exist_ok=True)
TRADES_FILE = DATA_DIR / 'trades_persistent.json'
STATE_FILE = DATA_DIR / 'state.json'
# Global state
state = { state = {
'current_trades': {}, 'current_trades': {},
'completed_trades': [], 'completed_trades': [],
'swaps': [], 'swaps': [],
'balance': {'USDT': 0.0}, 'balance': {},
'portfolio_value_usd': 0.0, 'portfolio_value_usd': 0.0,
'daily_pnl': 0.0, 'daily_pnl': 0.0,
'total_pnl': 0.0, 'total_pnl': 0.0,
'trades_today': 0, 'last_sync': datetime.now().isoformat()
'wins_today': 0,
'losses_today': 0,
'last_sync': datetime.now().isoformat(),
'timestamp': datetime.now().isoformat()
} }
def load_from_binance(): def load_from_binance():
"""Load REAL open positions from Binance API""" """Load real data from Binance"""
global state global state
try: try:
logger.info('🔄 Syncing with Binance...') logger.info('🔄 Syncing with Binance...')
# 1. Get account balance
account = client.get_account() account = client.get_account()
balances = {b['asset']: float(b['free']) for b in account['balances'] if float(b['free']) > 0} balances = {b['asset']: float(b['free']) for b in account['balances'] if float(b['free']) > 0.00001}
state['balance'] = balances state['balance'] = balances
logger.info(f"Balances: USDT={balances.get('USDT', 0):.2f}") logger.info(f"Balance: USDT={balances.get('USDT', 0):.2f}")
# 2. Load all open orders by symbol open_trades = {}
open_positions = {} for symbol in ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT']:
symbols = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT']
for symbol in symbols:
try: try:
orders = client.get_open_orders(symbol=symbol) orders = client.get_open_orders(symbol=symbol)
if orders: if orders:
# Get the first order (BUY order) o = orders[0]
order = orders[0] qty = float(o['origQty'])
buy_price = float(o['price'])
# Get current price for profit calculation
ticker = client.get_symbol_info(symbol)
current_price = float(client.get_symbol_ticker(symbol=symbol)['price']) current_price = float(client.get_symbol_ticker(symbol=symbol)['price'])
qty = float(order['origQty']) profit = (current_price - buy_price) * qty
buy_price = float(order['price']) profit_pct = ((current_price - buy_price) / buy_price * 100) if buy_price > 0 else 0
notional = qty * buy_price
current_value = qty * current_price
profit = current_value - notional
profit_pct = (profit / notional * 100) if notional > 0 else 0
open_positions[symbol] = { open_trades[symbol] = {
'qty': qty, 'qty': qty,
'buy_price': buy_price, 'buy_price': buy_price,
'current_price': current_price, 'current_price': current_price,
'buy_time': datetime.fromtimestamp(order['time']/1000).isoformat(), 'buy_time': datetime.fromtimestamp(o['time']/1000).isoformat(),
'current_value': current_value,
'entry_value': notional,
'profit': profit, 'profit': profit,
'profit_pct': profit_pct, 'profit_pct': profit_pct,
'peak_profit': profit_pct, 'order_id': o['orderId']
'trailing_stop': None,
'order_id': order['orderId']
} }
logger.info(f" {symbol}: {qty:.8f} @ ${buy_price:.2f} → Current: ${current_price:.2f} (P&L: ${profit:.2f} / {profit_pct:.2f}%)") logger.info(f" {symbol}: {qty:.8f} → ${current_price:.2f} P&L: {profit_pct:.2f}%")
except Exception as e: except Exception as e:
logger.debug(f"No open orders for {symbol}: {e}") logger.debug(f"Error {symbol}: {e}")
continue
state['current_trades'] = open_positions state['current_trades'] = open_trades
logger.info(f"✅ Found {len(open_positions)} open positions")
# 3. Calculate portfolio value usdt = balances.get('USDT', 0)
usdt_balance = balances.get('USDT', 0) portfolio = usdt + sum(t['qty']*t['current_price'] for t in open_trades.values())
portfolio_value = usdt_balance pnl = sum(t['profit'] for t in open_trades.values())
for symbol, trade in open_positions.items(): state['portfolio_value_usd'] = portfolio
portfolio_value += trade['current_value'] state['daily_pnl'] = pnl
state['total_pnl'] = pnl
state['portfolio_value_usd'] = portfolio_value
# 4. Calculate P&L
daily_pnl = sum(t.get('profit', 0) for t in open_positions.values())
state['daily_pnl'] = daily_pnl
state['total_pnl'] = daily_pnl
# 5. Update metadata
state['last_sync'] = datetime.now().isoformat() state['last_sync'] = datetime.now().isoformat()
state['timestamp'] = datetime.now().isoformat()
logger.info(f"✅ Portfolio Value: USD ${portfolio_value:.2f}")
logger.info(f"✅ Daily P&L: ${daily_pnl:.2f}")
# 6. Persist
save_state()
logger.info(f"✅ Portfolio: ${portfolio:.2f}, Trades: {len(open_trades)}, P&L: ${pnl:.2f}")
return True return True
except Exception as e: except Exception as e:
logger.error(f"❌ Binance sync failed: {e}") logger.error(f"❌ Error: {e}")
import traceback
traceback.print_exc()
return False return False
def save_state():
"""Save state to disk"""
try:
with open(STATE_FILE, 'w') as f:
json.dump(state, f, indent=2)
logger.info(f"💾 State persisted to {STATE_FILE}")
except Exception as e:
logger.error(f"Failed to save state: {e}")
async def background_sync(): async def background_sync():
"""Periodically sync with Binance"""
while True: while True:
try: try:
load_from_binance() load_from_binance()
await asyncio.sleep(10) # Sync every 10 seconds await asyncio.sleep(10)
except Exception as e: except Exception as e:
logger.error(f"Sync loop error: {e}") logger.error(f"Sync loop: {e}")
await asyncio.sleep(10) await asyncio.sleep(10)
@app.on_event("startup") @app.on_event("startup")
async def startup(): async def startup():
logger.info("🚀 State Manager starting...") logger.info("🚀 Starting State Manager...")
load_from_binance() load_from_binance()
asyncio.create_task(background_sync()) asyncio.create_task(background_sync())
logger.info("Background sync active") logger.info("Sync active")
@app.get("/state") @app.get("/state")
async def get_state(): async def get_state():
"""Return current state (loaded from Binance)"""
return state return state
@app.get("/health") @app.get("/health")
async def health(): async def health():
"""Health check""" return {"status": "ok", "trades": len(state['current_trades']), "portfolio": state['portfolio_value_usd']}
return {
"status": "ok",
"trades": len(state['current_trades']),
"portfolio_usd": state['portfolio_value_usd'],
"last_sync": state['last_sync']
}
@app.post("/sync")
async def manual_sync():
"""Force immediate sync with Binance"""
load_from_binance()
return {"status": "synced", "trades": len(state['current_trades'])}
if __name__ == "__main__": if __name__ == "__main__":
logger.info("Starting State Manager on port 8001...") logger.info("Starting on :8001")
uvicorn.run(app, host="0.0.0.0", port=8001, log_level="info") uvicorn.run(app, host="0.0.0.0", port=8001, log_level="error")

View File

@ -73,7 +73,7 @@ async def get_state():
"""Proxy to State Manager""" """Proxy to State Manager"""
try: try:
async with httpx.AsyncClient(timeout=3.0) as client: async with httpx.AsyncClient(timeout=3.0) as client:
resp = await client.get("http://localhost:8001/state") resp = await client.get("http://localhost:7001/api/bot-state")
return resp.json() return resp.json()
except: except:
return trading_state return trading_state
@ -213,7 +213,7 @@ async def get_state():
"""Proxy to State Manager""" """Proxy to State Manager"""
try: try:
async with httpx.AsyncClient(timeout=3.0) as client: async with httpx.AsyncClient(timeout=3.0) as client:
resp = await client.get("http://localhost:8001/state") resp = await client.get("http://localhost:7001/api/bot-state")
return resp.json() return resp.json()
except: except:
return trading_state return trading_state