From 42f47074615cda9e0c3216531e6ab420729a1e5d Mon Sep 17 00:00:00 2001 From: Marc Blatter Date: Sat, 4 Jul 2026 13:39:25 +0200 Subject: [PATCH] Bot auto-update: src/__pycache__/state_manager.cpython-310.pyc,src/__pycache__/web_dashboard.cpython-310.pyc,src/state_manager.py --- src/__pycache__/state_manager.cpython-310.pyc | Bin 0 -> 5793 bytes src/__pycache__/web_dashboard.cpython-310.pyc | Bin 19623 -> 19623 bytes src/state_manager.py | 191 ++++++++++++++++++ 3 files changed, 191 insertions(+) create mode 100644 src/__pycache__/state_manager.cpython-310.pyc create mode 100644 src/state_manager.py diff --git a/src/__pycache__/state_manager.cpython-310.pyc b/src/__pycache__/state_manager.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fc38ae3f1ddc8ad6a94404561a52c695a2c70b86 GIT binary patch literal 5793 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 delta 21 bcmZ2JlX3Y>My`BbUM>b8IPi8O*IXX}MXCmI 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')