diff --git a/src/__pycache__/state_manager.cpython-310.pyc b/src/__pycache__/state_manager.cpython-310.pyc index fc38ae3..01f26db 100644 Binary files a/src/__pycache__/state_manager.cpython-310.pyc and b/src/__pycache__/state_manager.cpython-310.pyc differ diff --git a/src/state_manager.py b/src/state_manager.py index 3fccade..301986b 100644 --- a/src/state_manager.py +++ b/src/state_manager.py @@ -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") diff --git a/src/web_dashboard.py b/src/web_dashboard.py index e60481e..d8cadd7 100644 --- a/src/web_dashboard.py +++ b/src/web_dashboard.py @@ -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)