From 4e8d268e354d5d9dd54acd20890d461d3a621999 Mon Sep 17 00:00:00 2001 From: Marc Blatter Date: Sat, 4 Jul 2026 14:04:27 +0200 Subject: [PATCH] Bot auto-update: src/__pycache__/state_manager.cpython-310.pyc,src/state_manager.py,src/web_dashboard.py --- src/__pycache__/state_manager.cpython-310.pyc | Bin 5793 -> 5413 bytes src/state_manager.py | 319 +++++++++--------- src/web_dashboard.py | 20 +- 3 files changed, 180 insertions(+), 159 deletions(-) diff --git a/src/__pycache__/state_manager.cpython-310.pyc b/src/__pycache__/state_manager.cpython-310.pyc index fc38ae3f1ddc8ad6a94404561a52c695a2c70b86..01f26dbc5e597e30ee312c84991313bec3d3f5a5 100644 GIT binary patch literal 5413 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 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)