diff --git a/src/__pycache__/state_manager.cpython-310.pyc b/src/__pycache__/state_manager.cpython-310.pyc new file mode 100644 index 0000000..fc38ae3 Binary files /dev/null and b/src/__pycache__/state_manager.cpython-310.pyc differ diff --git a/src/__pycache__/web_dashboard.cpython-310.pyc b/src/__pycache__/web_dashboard.cpython-310.pyc index a763175..43605b6 100644 Binary files a/src/__pycache__/web_dashboard.cpython-310.pyc and b/src/__pycache__/web_dashboard.cpython-310.pyc differ diff --git a/src/state_manager.py b/src/state_manager.py new file mode 100644 index 0000000..3fccade --- /dev/null +++ b/src/state_manager.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +""" +Enhanced State Manager with Binance Position Recovery +""" + +import json +import asyncio +import logging +import os +from datetime import datetime +from pathlib import Path +from fastapi import FastAPI +import httpx + +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() + +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() + + def load_from_disk(self): + """Load state from persistent storage""" + if TRADES_FILE.exists(): + 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") + 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}") + + 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() + +@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 + 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) + +@app.get("/state") +async def get_state(): + """Get current state (used by dashboard)""" + return state_mgr.get_state() + +@app.get("/health") +async def health(): + """Health check""" + return {"status": "ok", "trades": len(state_mgr.trades)} + +if __name__ == '__main__': + import uvicorn + uvicorn.run(app, host='127.0.0.1', port=8001, log_level='info')