Bot auto-update: src/__pycache__/state_manager.cpython-310.pyc,src/__pycache__/web_dashboard.cpython-310.pyc,src/state_manager.py
This commit is contained in:
parent
de57afabdb
commit
42f4707461
Binary file not shown.
Binary file not shown.
|
|
@ -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')
|
||||||
Loading…
Reference in New Issue