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 = [] # Persistence
self.swaps = [] DATA_DIR = Path('/home/marc/bot-deploy/data')
self.last_sync = None DATA_DIR.mkdir(exist_ok=True)
self.load_from_disk() 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: try:
with open(TRADES_FILE) as f: logger.info('🔄 Syncing with Binance...')
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")
except Exception as e:
logger.error(f"Failed to load trades: {e}")
def save_to_disk(self): # 1. Get account balance
"""Persist state to JSON""" account = client.get_account()
data = { balances = {b['asset']: float(b['free']) for b in account['balances'] if float(b['free']) > 0}
'current_trades': self.trades, state['balance'] = balances
'completed_trades': self.completed_trades, logger.info(f"✅ Balances: USDT={balances.get('USDT', 0):.2f}")
'swaps': self.swaps,
'last_update': datetime.utcnow().isoformat() # 2. Load all open orders by symbol
open_positions = {}
symbols = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT', 'XRPUSDT']
for symbol in symbols:
try:
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']
} }
with open(TRADES_FILE, 'w') as f: logger.info(f" {symbol}: {qty:.8f} @ ${buy_price:.2f} → Current: ${current_price:.2f} (P&L: ${profit:.2f} / {profit_pct:.2f}%)")
json.dump(data, f, indent=2)
logger.info(f"💾 Persisted state: {len(self.trades)} open trades")
async def sync_bot_state(self): except Exception as e:
"""Load current state from bot API""" logger.debug(f"No open orders for {symbol}: {e}")
try: continue
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 state['current_trades'] = open_positions
if 'current_trades' in bot_state and bot_state['current_trades']: logger.info(f"✅ Found {len(open_positions)} open positions")
self.trades.update(bot_state['current_trades'])
if 'completed_trades' in bot_state: # 3. Calculate portfolio value
for trade in bot_state['completed_trades']: usdt_balance = balances.get('USDT', 0)
if trade not in self.completed_trades: portfolio_value = usdt_balance
self.completed_trades.append(trade)
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()
self.last_sync = datetime.utcnow().isoformat()
logger.info(f"🔄 Synced: {len(self.trades)} open trades")
self.save_to_disk()
return True return True
except Exception as e:
logger.warning(f"Bot API unavailable: {e}")
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: try:
async with httpx.AsyncClient(timeout=10.0) as client: with open(STATE_FILE, 'w') as f:
# Get account info from Binance json.dump(state, f, indent=2)
resp = await client.get('https://api.binance.com/api/v3/account') logger.info(f"💾 State persisted to {STATE_FILE}")
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"Failed to save state: {e}")
return False async def background_sync():
"""Periodically sync with Binance"""
def get_state(self): while True:
"""Get current state for dashboard""" try:
return { load_from_binance()
'current_trades': self.trades, await asyncio.sleep(10) # Sync every 10 seconds
'completed_trades': self.completed_trades, except Exception as e:
'swaps': self.swaps, logger.error(f"Sync loop error: {e}")
'last_sync': self.last_sync, await asyncio.sleep(10)
'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)