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 #!/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 asyncio
import json
import logging import logging
import os
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from fastapi import FastAPI 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) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
STATE_DIR = Path('/home/marc/bot-deploy/data')
TRADES_FILE = STATE_DIR / 'trades_persistent.json'
app = FastAPI() app = FastAPI()
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
class StateManager: # Binance client
def __init__(self): API_KEY = os.getenv('BINANCE_API_KEY')
self.state_dir = STATE_DIR API_SECRET = os.getenv('BINANCE_API_SECRET')
self.state_dir.mkdir(parents=True, exist_ok=True) client = Client(API_KEY, API_SECRET)
self.trades = {}
self.completed_trades = []
self.swaps = []
self.last_sync = None
self.load_from_disk()
def load_from_disk(self): # Persistence
"""Load state from persistent storage""" DATA_DIR = Path('/home/marc/bot-deploy/data')
if TRADES_FILE.exists(): 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
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: try:
with open(TRADES_FILE) as f: orders = client.get_open_orders(symbol=symbol)
data = json.load(f) if orders:
self.trades = data.get('current_trades', {}) # Get the first order (BUY order)
self.completed_trades = data.get('completed_trades', []) order = orders[0]
self.swaps = data.get('swaps', [])
logger.info(f"✅ Loaded {len(self.trades)} trades from disk") # 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: except Exception as e:
logger.error(f"Failed to load trades: {e}") logger.debug(f"No open orders for {symbol}: {e}")
continue
def save_to_disk(self): state['current_trades'] = open_positions
"""Persist state to JSON""" logger.info(f"✅ Found {len(open_positions)} open positions")
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): # 3. Calculate portfolio value
"""Load current state from bot API""" usdt_balance = balances.get('USDT', 0)
try: portfolio_value = usdt_balance
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 for symbol, trade in open_positions.items():
if 'current_trades' in bot_state and bot_state['current_trades']: portfolio_value += trade['current_value']
self.trades.update(bot_state['current_trades'])
if 'completed_trades' in bot_state: state['portfolio_value_usd'] = portfolio_value
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() # 4. Calculate P&L
logger.info(f"🔄 Synced: {len(self.trades)} open trades") daily_pnl = sum(t.get('profit', 0) for t in open_positions.values())
self.save_to_disk() state['daily_pnl'] = daily_pnl
return True state['total_pnl'] = daily_pnl
except Exception as e:
logger.warning(f"Bot API unavailable: {e}")
# 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 return False
async def recover_from_binance(self): def save_state():
"""Load open positions directly from Binance API on startup""" """Save state to disk"""
logger.info("📊 Attempting to recover positions from Binance...") 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: try:
async with httpx.AsyncClient(timeout=10.0) as client: load_from_binance()
# Get account info from Binance await asyncio.sleep(10) # Sync every 10 seconds
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: except Exception as e:
logger.warning(f"Binance recovery failed: {e}") logger.error(f"Sync loop error: {e}")
await asyncio.sleep(10)
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()
@app.on_event("startup") @app.on_event("startup")
async def startup(): async def startup():
"""On startup: Load from disk, sync bot, recover from Binance if needed""" logger.info("🚀 State Manager starting...")
logger.info("🚀 State Manager startup sequence...") load_from_binance()
# 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
asyncio.create_task(background_sync()) asyncio.create_task(background_sync())
logger.info("✅ State Manager ready!") logger.info("✅ Background sync active")
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)
@app.get("/state") @app.get("/state")
async def get_state(): async def get_state():
"""Get current state (used by dashboard)""" """Return current state (loaded from Binance)"""
return state_mgr.get_state() return state
@app.get("/health") @app.get("/health")
async def health(): async def health():
"""Health check""" """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__': @app.post("/sync")
import uvicorn async def manual_sync():
uvicorn.run(app, host='127.0.0.1', port=8001, log_level='info') """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: finally:
active_connections.remove(websocket) 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(): async def get_state():
"""Get current trading state""" """Get current trading state"""
return trading_state return trading_state
@ -199,6 +208,15 @@ async def trigger_liquidation():
@app.get("/") @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(): async def get_dashboard():
"""Serve web dashboard HTML""" """Serve web dashboard HTML"""
return HTMLResponse(html_content) return HTMLResponse(html_content)