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

This commit is contained in:
Marc Blatter 2026-07-04 14:04:27 +02:00
parent dceec2cea9
commit 4e8d268e35
3 changed files with 180 additions and 159 deletions

View File

@ -1,191 +1,194 @@
#!/usr/bin/env python3
"""
Enhanced State Manager with Binance Position Recovery
State Manager V2: Direct Binance Integration
- Loads open positions from Binance API (source of truth)
- Computes portfolio value from real prices
- Persists to JSON
- Provides REST API for dashboard
"""
import json
import asyncio
import json
import logging
import os
from datetime import datetime
from pathlib import Path
from fastapi import FastAPI
import httpx
from fastapi.middleware.cors import CORSMiddleware
import uvicorn
from binance.client import Client
import os
from dotenv import load_dotenv
import time
# Setup
load_dotenv('/home/marc/bot-deploy/.env')
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
STATE_DIR = Path('/home/marc/bot-deploy/data')
TRADES_FILE = STATE_DIR / 'trades_persistent.json'
app = FastAPI()
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
class StateManager:
def __init__(self):
self.state_dir = STATE_DIR
self.state_dir.mkdir(parents=True, exist_ok=True)
self.trades = {}
self.completed_trades = []
self.swaps = []
self.last_sync = None
self.load_from_disk()
# 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 = {
'current_trades': {},
'completed_trades': [],
'swaps': [],
'balance': {'USDT': 0.0},
'portfolio_value_usd': 0.0,
'daily_pnl': 0.0,
'total_pnl': 0.0,
'trades_today': 0,
'wins_today': 0,
'losses_today': 0,
'last_sync': datetime.now().isoformat(),
'timestamp': datetime.now().isoformat()
}
def load_from_binance():
"""Load REAL open positions from Binance API"""
global state
def load_from_disk(self):
"""Load state from persistent storage"""
if TRADES_FILE.exists():
try:
logger.info('🔄 Syncing with Binance...')
# 1. Get account balance
account = client.get_account()
balances = {b['asset']: float(b['free']) for b in account['balances'] if float(b['free']) > 0}
state['balance'] = balances
logger.info(f"✅ Balances: USDT={balances.get('USDT', 0):.2f}")
# 2. Load all open orders by symbol
open_positions = {}
symbols = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT']
for symbol in symbols:
try:
with open(TRADES_FILE) as f:
data = json.load(f)
self.trades = data.get('current_trades', {})
self.completed_trades = data.get('completed_trades', [])
self.swaps = data.get('swaps', [])
logger.info(f"✅ Loaded {len(self.trades)} trades from disk")
orders = client.get_open_orders(symbol=symbol)
if orders:
# Get the first order (BUY order)
order = orders[0]
# Get current price for profit calculation
ticker = client.get_symbol_info(symbol)
current_price = float(client.get_symbol_ticker(symbol=symbol)['price'])
qty = float(order['origQty'])
buy_price = float(order['price'])
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] = {
'qty': qty,
'buy_price': buy_price,
'current_price': current_price,
'buy_time': datetime.fromtimestamp(order['time']/1000).isoformat(),
'current_value': current_value,
'entry_value': notional,
'profit': profit,
'profit_pct': profit_pct,
'peak_profit': profit_pct,
'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}%)")
except Exception as e:
logger.error(f"Failed to load trades: {e}")
def save_to_disk(self):
"""Persist state to JSON"""
data = {
'current_trades': self.trades,
'completed_trades': self.completed_trades,
'swaps': self.swaps,
'last_update': datetime.utcnow().isoformat()
}
with open(TRADES_FILE, 'w') as f:
json.dump(data, f, indent=2)
logger.info(f"💾 Persisted state: {len(self.trades)} open trades")
async def sync_bot_state(self):
"""Load current state from bot API"""
try:
async with httpx.AsyncClient(timeout=5.0) as client:
resp = await client.get('http://localhost:7000/api/state')
if resp.status_code == 200:
bot_state = resp.json()
# Merge bot trades with persistent storage
if 'current_trades' in bot_state and bot_state['current_trades']:
self.trades.update(bot_state['current_trades'])
if 'completed_trades' in bot_state:
for trade in bot_state['completed_trades']:
if trade not in self.completed_trades:
self.completed_trades.append(trade)
self.last_sync = datetime.utcnow().isoformat()
logger.info(f"🔄 Synced: {len(self.trades)} open trades")
self.save_to_disk()
return True
except Exception as e:
logger.warning(f"Bot API unavailable: {e}")
logger.debug(f"No open orders for {symbol}: {e}")
continue
state['current_trades'] = open_positions
logger.info(f"✅ Found {len(open_positions)} open positions")
# 3. Calculate portfolio value
usdt_balance = balances.get('USDT', 0)
portfolio_value = usdt_balance
for symbol, trade in open_positions.items():
portfolio_value += trade['current_value']
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['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()
return True
except Exception as e:
logger.error(f"❌ Binance sync failed: {e}")
return False
async def recover_from_binance(self):
"""Load open positions directly from Binance API on startup"""
logger.info("📊 Attempting to recover positions from Binance...")
try:
async with httpx.AsyncClient(timeout=10.0) as client:
# Get account info from Binance
resp = await client.get('https://api.binance.com/api/v3/account')
if resp.status_code == 200:
account = resp.json()
balances = account.get('balances', [])
new_trades = {}
for bal in balances:
symbol = bal['asset']
free = float(bal['free'])
if symbol != 'USDT' and free > 0.00001:
# Get current price
pair = f'{symbol}USDT'
try:
p_resp = await client.get(
f'https://api.binance.com/api/v3/ticker/price?symbol={pair}'
)
if p_resp.status_code == 200:
price = float(p_resp.json()['price'])
new_trades[pair] = {
'qty': free,
'buy_price': price,
'entry_time': 'recovered-on-restart',
'status': 'open',
'recovered': True
}
logger.info(f"✅ Recovered: {pair} {free} @ ${price}")
except:
pass
if new_trades:
# Merge with existing
for pair, trade in new_trades.items():
if pair not in self.trades:
self.trades[pair] = trade
self.save_to_disk()
logger.info(f"✅ Recovered {len(new_trades)} positions from Binance")
return len(new_trades) > 0
except Exception as e:
logger.warning(f"Binance recovery failed: {e}")
return False
def get_state(self):
"""Get current state for dashboard"""
return {
'current_trades': self.trades,
'completed_trades': self.completed_trades,
'swaps': self.swaps,
'last_sync': self.last_sync,
'timestamp': datetime.utcnow().isoformat()
}
# Global instance
state_mgr = StateManager()
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():
"""Periodically sync with Binance"""
while True:
try:
load_from_binance()
await asyncio.sleep(10) # Sync every 10 seconds
except Exception as e:
logger.error(f"Sync loop error: {e}")
await asyncio.sleep(10)
@app.on_event("startup")
async def startup():
"""On startup: Load from disk, sync bot, recover from Binance if needed"""
logger.info("🚀 State Manager startup sequence...")
# 1. Already loaded from disk in __init__
logger.info(f"📁 Loaded {len(state_mgr.trades)} trades from disk")
# 2. Try to sync with bot
await state_mgr.sync_bot_state()
# 3. If still no trades: recover from Binance API
if not state_mgr.trades:
logger.info("⚠️ No trades in storage - attempting Binance recovery...")
await state_mgr.recover_from_binance()
# 4. Start background sync
logger.info("🚀 State Manager starting...")
load_from_binance()
asyncio.create_task(background_sync())
logger.info("✅ State Manager ready!")
async def background_sync():
"""Periodically sync with bot"""
while True:
try:
await asyncio.sleep(30)
await state_mgr.sync_bot_state()
except Exception as e:
logger.error(f"Sync error: {e}")
await asyncio.sleep(5)
logger.info("✅ Background sync active")
@app.get("/state")
async def get_state():
"""Get current state (used by dashboard)"""
return state_mgr.get_state()
"""Return current state (loaded from Binance)"""
return state
@app.get("/health")
async def health():
"""Health check"""
return {"status": "ok", "trades": len(state_mgr.trades)}
return {
"status": "ok",
"trades": len(state['current_trades']),
"portfolio_usd": state['portfolio_value_usd'],
"last_sync": state['last_sync']
}
if __name__ == '__main__':
import uvicorn
uvicorn.run(app, host='127.0.0.1', port=8001, log_level='info')
@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__":
logger.info("Starting State Manager on port 8001...")
uvicorn.run(app, host="0.0.0.0", port=8001, log_level="info")

View File

@ -67,7 +67,16 @@ async def websocket_endpoint(websocket: WebSocket):
finally:
active_connections.remove(websocket)
@app.get("/api/state")
# @app.get("/api/state")
n@app.get("/api/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:8001/state")
return resp.json()
except:
return trading_state
async def get_state():
"""Get current trading state"""
return trading_state
@ -199,6 +208,15 @@ async def trigger_liquidation():
@app.get("/")
n@app.get("/api/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:8001/state")
return resp.json()
except:
return trading_state
async def get_dashboard():
"""Serve web dashboard HTML"""
return HTMLResponse(html_content)