COB WS fix
This commit is contained in:
@ -26,6 +26,7 @@ from collections import defaultdict
|
|||||||
|
|
||||||
from .multi_exchange_cob_provider import MultiExchangeCOBProvider, COBSnapshot, ConsolidatedOrderBookLevel
|
from .multi_exchange_cob_provider import MultiExchangeCOBProvider, COBSnapshot, ConsolidatedOrderBookLevel
|
||||||
from .data_provider import DataProvider, MarketTick
|
from .data_provider import DataProvider, MarketTick
|
||||||
|
from .enhanced_cob_websocket import EnhancedCOBWebSocket
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -48,6 +49,9 @@ class COBIntegration:
|
|||||||
# Initialize COB provider to None, will be set in start()
|
# Initialize COB provider to None, will be set in start()
|
||||||
self.cob_provider = None
|
self.cob_provider = None
|
||||||
|
|
||||||
|
# Enhanced WebSocket integration
|
||||||
|
self.enhanced_websocket: Optional[EnhancedCOBWebSocket] = None
|
||||||
|
|
||||||
# CNN/DQN integration
|
# CNN/DQN integration
|
||||||
self.cnn_callbacks: List[Callable] = []
|
self.cnn_callbacks: List[Callable] = []
|
||||||
self.dqn_callbacks: List[Callable] = []
|
self.dqn_callbacks: List[Callable] = []
|
||||||
@ -62,43 +66,187 @@ class COBIntegration:
|
|||||||
self.cob_feature_cache: Dict[str, np.ndarray] = {}
|
self.cob_feature_cache: Dict[str, np.ndarray] = {}
|
||||||
self.last_cob_features_update: Dict[str, datetime] = {}
|
self.last_cob_features_update: Dict[str, datetime] = {}
|
||||||
|
|
||||||
|
# WebSocket status for dashboard
|
||||||
|
self.websocket_status: Dict[str, str] = {symbol: 'disconnected' for symbol in self.symbols}
|
||||||
|
|
||||||
# Initialize signal tracking
|
# Initialize signal tracking
|
||||||
for symbol in self.symbols:
|
for symbol in self.symbols:
|
||||||
self.cob_signals[symbol] = []
|
self.cob_signals[symbol] = []
|
||||||
self.liquidity_alerts[symbol] = []
|
self.liquidity_alerts[symbol] = []
|
||||||
self.arbitrage_opportunities[symbol] = []
|
self.arbitrage_opportunities[symbol] = []
|
||||||
|
|
||||||
logger.info("COB Integration initialized (provider will be started in async)")
|
logger.info("COB Integration initialized with Enhanced WebSocket support")
|
||||||
logger.info(f"Symbols: {self.symbols}")
|
logger.info(f"Symbols: {self.symbols}")
|
||||||
|
|
||||||
async def start(self):
|
async def start(self):
|
||||||
"""Start COB integration"""
|
"""Start COB integration with Enhanced WebSocket"""
|
||||||
logger.info("Starting COB Integration")
|
logger.info("🚀 Starting COB Integration with Enhanced WebSocket")
|
||||||
|
|
||||||
# Initialize COB provider here, within the async context
|
# Initialize Enhanced WebSocket first
|
||||||
self.cob_provider = MultiExchangeCOBProvider(
|
|
||||||
symbols=self.symbols,
|
|
||||||
bucket_size_bps=1.0 # 1 basis point granularity
|
|
||||||
)
|
|
||||||
|
|
||||||
# Register callbacks
|
|
||||||
self.cob_provider.subscribe_to_cob_updates(self._on_cob_update)
|
|
||||||
self.cob_provider.subscribe_to_bucket_updates(self._on_bucket_update)
|
|
||||||
|
|
||||||
# Start COB provider streaming
|
|
||||||
try:
|
try:
|
||||||
logger.info("Starting COB provider streaming...")
|
self.enhanced_websocket = EnhancedCOBWebSocket(
|
||||||
await self.cob_provider.start_streaming()
|
symbols=self.symbols,
|
||||||
|
dashboard_callback=self._on_websocket_status_update
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add COB data callback
|
||||||
|
self.enhanced_websocket.add_cob_callback(self._on_enhanced_cob_update)
|
||||||
|
|
||||||
|
# Start enhanced WebSocket
|
||||||
|
await self.enhanced_websocket.start()
|
||||||
|
logger.info("✅ Enhanced WebSocket started successfully")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error starting COB provider streaming: {e}")
|
logger.error(f"❌ Error starting Enhanced WebSocket: {e}")
|
||||||
# Start a background task instead
|
|
||||||
|
# Initialize COB provider as fallback
|
||||||
|
try:
|
||||||
|
self.cob_provider = MultiExchangeCOBProvider(
|
||||||
|
symbols=self.symbols,
|
||||||
|
bucket_size_bps=1.0 # 1 basis point granularity
|
||||||
|
)
|
||||||
|
|
||||||
|
# Register callbacks
|
||||||
|
self.cob_provider.subscribe_to_cob_updates(self._on_cob_update)
|
||||||
|
self.cob_provider.subscribe_to_bucket_updates(self._on_bucket_update)
|
||||||
|
|
||||||
|
# Start COB provider streaming as backup
|
||||||
|
logger.info("Starting COB provider as backup...")
|
||||||
asyncio.create_task(self._start_cob_provider_background())
|
asyncio.create_task(self._start_cob_provider_background())
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Error initializing COB provider: {e}")
|
||||||
|
|
||||||
# Start analysis threads
|
# Start analysis threads
|
||||||
asyncio.create_task(self._continuous_cob_analysis())
|
asyncio.create_task(self._continuous_cob_analysis())
|
||||||
asyncio.create_task(self._continuous_signal_generation())
|
asyncio.create_task(self._continuous_signal_generation())
|
||||||
|
|
||||||
logger.info("COB Integration started successfully")
|
logger.info("✅ COB Integration started successfully with Enhanced WebSocket")
|
||||||
|
|
||||||
|
async def _on_enhanced_cob_update(self, symbol: str, cob_data: Dict):
|
||||||
|
"""Handle COB updates from Enhanced WebSocket"""
|
||||||
|
try:
|
||||||
|
logger.debug(f"📊 Enhanced WebSocket COB update for {symbol}")
|
||||||
|
|
||||||
|
# Convert enhanced WebSocket data to COB format for existing callbacks
|
||||||
|
# Notify CNN callbacks
|
||||||
|
for callback in self.cnn_callbacks:
|
||||||
|
try:
|
||||||
|
callback(symbol, {
|
||||||
|
'features': cob_data,
|
||||||
|
'timestamp': cob_data.get('timestamp', datetime.now()),
|
||||||
|
'type': 'enhanced_cob_features'
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Error in CNN callback: {e}")
|
||||||
|
|
||||||
|
# Notify DQN callbacks
|
||||||
|
for callback in self.dqn_callbacks:
|
||||||
|
try:
|
||||||
|
callback(symbol, {
|
||||||
|
'state': cob_data,
|
||||||
|
'timestamp': cob_data.get('timestamp', datetime.now()),
|
||||||
|
'type': 'enhanced_cob_state'
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Error in DQN callback: {e}")
|
||||||
|
|
||||||
|
# Notify dashboard callbacks
|
||||||
|
dashboard_data = self._format_enhanced_cob_for_dashboard(symbol, cob_data)
|
||||||
|
for callback in self.dashboard_callbacks:
|
||||||
|
try:
|
||||||
|
if asyncio.iscoroutinefunction(callback):
|
||||||
|
asyncio.create_task(callback(symbol, dashboard_data))
|
||||||
|
else:
|
||||||
|
callback(symbol, dashboard_data)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Error in dashboard callback: {e}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error processing Enhanced WebSocket COB update for {symbol}: {e}")
|
||||||
|
|
||||||
|
async def _on_websocket_status_update(self, status_data: Dict):
|
||||||
|
"""Handle WebSocket status updates for dashboard"""
|
||||||
|
try:
|
||||||
|
symbol = status_data.get('symbol')
|
||||||
|
status = status_data.get('status')
|
||||||
|
message = status_data.get('message', '')
|
||||||
|
|
||||||
|
if symbol:
|
||||||
|
self.websocket_status[symbol] = status
|
||||||
|
logger.info(f"🔌 WebSocket status for {symbol}: {status} - {message}")
|
||||||
|
|
||||||
|
# Notify dashboard callbacks about status change
|
||||||
|
status_update = {
|
||||||
|
'type': 'websocket_status',
|
||||||
|
'data': {
|
||||||
|
'symbol': symbol,
|
||||||
|
'status': status,
|
||||||
|
'message': message,
|
||||||
|
'timestamp': status_data.get('timestamp', datetime.now().isoformat())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for callback in self.dashboard_callbacks:
|
||||||
|
try:
|
||||||
|
if asyncio.iscoroutinefunction(callback):
|
||||||
|
asyncio.create_task(callback(symbol, status_update))
|
||||||
|
else:
|
||||||
|
callback(symbol, status_update)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Error in dashboard status callback: {e}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error processing WebSocket status update: {e}")
|
||||||
|
|
||||||
|
def _format_enhanced_cob_for_dashboard(self, symbol: str, cob_data: Dict) -> Dict:
|
||||||
|
"""Format Enhanced WebSocket COB data for dashboard"""
|
||||||
|
try:
|
||||||
|
# Extract data from enhanced WebSocket format
|
||||||
|
bids = cob_data.get('bids', [])
|
||||||
|
asks = cob_data.get('asks', [])
|
||||||
|
stats = cob_data.get('stats', {})
|
||||||
|
|
||||||
|
# Format for dashboard
|
||||||
|
dashboard_data = {
|
||||||
|
'type': 'cob_update',
|
||||||
|
'data': {
|
||||||
|
'bids': [{'price': bid['price'], 'volume': bid['size'] * bid['price'], 'side': 'bid'} for bid in bids[:100]],
|
||||||
|
'asks': [{'price': ask['price'], 'volume': ask['size'] * ask['price'], 'side': 'ask'} for ask in asks[:100]],
|
||||||
|
'svp': [], # SVP data not available from WebSocket
|
||||||
|
'stats': {
|
||||||
|
'symbol': symbol,
|
||||||
|
'timestamp': cob_data.get('timestamp', datetime.now()).isoformat() if isinstance(cob_data.get('timestamp'), datetime) else cob_data.get('timestamp', datetime.now().isoformat()),
|
||||||
|
'mid_price': stats.get('mid_price', 0),
|
||||||
|
'spread_bps': (stats.get('spread', 0) / stats.get('mid_price', 1)) * 10000 if stats.get('mid_price', 0) > 0 else 0,
|
||||||
|
'bid_liquidity': stats.get('bid_volume', 0) * stats.get('best_bid', 0),
|
||||||
|
'ask_liquidity': stats.get('ask_volume', 0) * stats.get('best_ask', 0),
|
||||||
|
'total_bid_liquidity': stats.get('bid_volume', 0) * stats.get('best_bid', 0),
|
||||||
|
'total_ask_liquidity': stats.get('ask_volume', 0) * stats.get('best_ask', 0),
|
||||||
|
'imbalance': (stats.get('bid_volume', 0) - stats.get('ask_volume', 0)) / (stats.get('bid_volume', 0) + stats.get('ask_volume', 0)) if (stats.get('bid_volume', 0) + stats.get('ask_volume', 0)) > 0 else 0,
|
||||||
|
'liquidity_imbalance': (stats.get('bid_volume', 0) - stats.get('ask_volume', 0)) / (stats.get('bid_volume', 0) + stats.get('ask_volume', 0)) if (stats.get('bid_volume', 0) + stats.get('ask_volume', 0)) > 0 else 0,
|
||||||
|
'bid_levels': len(bids),
|
||||||
|
'ask_levels': len(asks),
|
||||||
|
'exchanges_active': [cob_data.get('exchange', 'binance')],
|
||||||
|
'bucket_size': 1.0,
|
||||||
|
'websocket_status': self.websocket_status.get(symbol, 'unknown'),
|
||||||
|
'source': cob_data.get('source', 'enhanced_websocket')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dashboard_data
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error formatting enhanced COB data for dashboard: {e}")
|
||||||
|
return {
|
||||||
|
'type': 'error',
|
||||||
|
'data': {'error': str(e)}
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_websocket_status(self) -> Dict[str, str]:
|
||||||
|
"""Get current WebSocket status for all symbols"""
|
||||||
|
return self.websocket_status.copy()
|
||||||
|
|
||||||
async def _start_cob_provider_background(self):
|
async def _start_cob_provider_background(self):
|
||||||
"""Start COB provider in background task"""
|
"""Start COB provider in background task"""
|
||||||
|
488
core/enhanced_cob_websocket.py
Normal file
488
core/enhanced_cob_websocket.py
Normal file
@ -0,0 +1,488 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Enhanced COB WebSocket Implementation
|
||||||
|
|
||||||
|
Robust WebSocket implementation for Consolidated Order Book data with:
|
||||||
|
- Maximum allowed depth subscription
|
||||||
|
- Clear error handling and warnings
|
||||||
|
- Automatic reconnection with exponential backoff
|
||||||
|
- Fallback to REST API when WebSocket fails
|
||||||
|
- Dashboard integration with status updates
|
||||||
|
|
||||||
|
This replaces the existing COB WebSocket implementation with a more reliable version.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Dict, List, Optional, Any, Callable
|
||||||
|
from collections import deque, defaultdict
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import aiohttp
|
||||||
|
import weakref
|
||||||
|
|
||||||
|
try:
|
||||||
|
import websockets
|
||||||
|
from websockets.client import connect as websockets_connect
|
||||||
|
from websockets.exceptions import ConnectionClosed, WebSocketException
|
||||||
|
WEBSOCKETS_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
websockets = None
|
||||||
|
websockets_connect = None
|
||||||
|
ConnectionClosed = Exception
|
||||||
|
WebSocketException = Exception
|
||||||
|
WEBSOCKETS_AVAILABLE = False
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class COBWebSocketStatus:
|
||||||
|
"""Status tracking for COB WebSocket connections"""
|
||||||
|
connected: bool = False
|
||||||
|
last_message_time: Optional[datetime] = None
|
||||||
|
connection_attempts: int = 0
|
||||||
|
last_error: Optional[str] = None
|
||||||
|
reconnect_delay: float = 1.0
|
||||||
|
max_reconnect_delay: float = 60.0
|
||||||
|
messages_received: int = 0
|
||||||
|
|
||||||
|
def reset_reconnect_delay(self):
|
||||||
|
"""Reset reconnect delay on successful connection"""
|
||||||
|
self.reconnect_delay = 1.0
|
||||||
|
|
||||||
|
def increase_reconnect_delay(self):
|
||||||
|
"""Increase reconnect delay with exponential backoff"""
|
||||||
|
self.reconnect_delay = min(self.max_reconnect_delay, self.reconnect_delay * 2)
|
||||||
|
|
||||||
|
class EnhancedCOBWebSocket:
|
||||||
|
"""Enhanced COB WebSocket with robust error handling and fallback"""
|
||||||
|
|
||||||
|
def __init__(self, symbols: List[str] = None, dashboard_callback: Callable = None):
|
||||||
|
"""
|
||||||
|
Initialize Enhanced COB WebSocket
|
||||||
|
|
||||||
|
Args:
|
||||||
|
symbols: List of symbols to monitor (default: ['BTC/USDT', 'ETH/USDT'])
|
||||||
|
dashboard_callback: Callback function for dashboard status updates
|
||||||
|
"""
|
||||||
|
self.symbols = symbols or ['BTC/USDT', 'ETH/USDT']
|
||||||
|
self.dashboard_callback = dashboard_callback
|
||||||
|
|
||||||
|
# Connection status tracking
|
||||||
|
self.status: Dict[str, COBWebSocketStatus] = {
|
||||||
|
symbol: COBWebSocketStatus() for symbol in self.symbols
|
||||||
|
}
|
||||||
|
|
||||||
|
# Data callbacks
|
||||||
|
self.cob_callbacks: List[Callable] = []
|
||||||
|
self.error_callbacks: List[Callable] = []
|
||||||
|
|
||||||
|
# Latest data cache
|
||||||
|
self.latest_cob_data: Dict[str, Dict] = {}
|
||||||
|
|
||||||
|
# WebSocket connections
|
||||||
|
self.websocket_tasks: Dict[str, asyncio.Task] = {}
|
||||||
|
|
||||||
|
# REST API fallback
|
||||||
|
self.rest_session: Optional[aiohttp.ClientSession] = None
|
||||||
|
self.rest_fallback_active: Dict[str, bool] = {symbol: False for symbol in self.symbols}
|
||||||
|
self.rest_tasks: Dict[str, asyncio.Task] = {}
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
self.max_depth = 1000 # Maximum depth for order book
|
||||||
|
self.update_speed = '100ms' # Binance update speed
|
||||||
|
|
||||||
|
logger.info(f"Enhanced COB WebSocket initialized for symbols: {self.symbols}")
|
||||||
|
if not WEBSOCKETS_AVAILABLE:
|
||||||
|
logger.error("⚠️ WebSockets module not available - COB data will be limited to REST API")
|
||||||
|
|
||||||
|
def add_cob_callback(self, callback: Callable):
|
||||||
|
"""Add callback for COB data updates"""
|
||||||
|
self.cob_callbacks.append(callback)
|
||||||
|
|
||||||
|
def add_error_callback(self, callback: Callable):
|
||||||
|
"""Add callback for error notifications"""
|
||||||
|
self.error_callbacks.append(callback)
|
||||||
|
|
||||||
|
async def start(self):
|
||||||
|
"""Start COB WebSocket connections"""
|
||||||
|
logger.info("🚀 Starting Enhanced COB WebSocket system")
|
||||||
|
|
||||||
|
# Initialize REST session for fallback
|
||||||
|
await self._init_rest_session()
|
||||||
|
|
||||||
|
# Start WebSocket connections for each symbol
|
||||||
|
for symbol in self.symbols:
|
||||||
|
await self._start_symbol_websocket(symbol)
|
||||||
|
|
||||||
|
# Start monitoring task
|
||||||
|
asyncio.create_task(self._monitor_connections())
|
||||||
|
|
||||||
|
logger.info("✅ Enhanced COB WebSocket system started")
|
||||||
|
|
||||||
|
async def stop(self):
|
||||||
|
"""Stop all WebSocket connections"""
|
||||||
|
logger.info("🛑 Stopping Enhanced COB WebSocket system")
|
||||||
|
|
||||||
|
# Cancel all WebSocket tasks
|
||||||
|
for symbol, task in self.websocket_tasks.items():
|
||||||
|
if task and not task.done():
|
||||||
|
task.cancel()
|
||||||
|
try:
|
||||||
|
await task
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Cancel all REST tasks
|
||||||
|
for symbol, task in self.rest_tasks.items():
|
||||||
|
if task and not task.done():
|
||||||
|
task.cancel()
|
||||||
|
try:
|
||||||
|
await task
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Close REST session
|
||||||
|
if self.rest_session:
|
||||||
|
await self.rest_session.close()
|
||||||
|
|
||||||
|
logger.info("✅ Enhanced COB WebSocket system stopped")
|
||||||
|
|
||||||
|
async def _init_rest_session(self):
|
||||||
|
"""Initialize REST API session for fallback"""
|
||||||
|
try:
|
||||||
|
timeout = aiohttp.ClientTimeout(total=10)
|
||||||
|
self.rest_session = aiohttp.ClientSession(timeout=timeout)
|
||||||
|
logger.info("✅ REST API session initialized for fallback")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Failed to initialize REST session: {e}")
|
||||||
|
|
||||||
|
async def _start_symbol_websocket(self, symbol: str):
|
||||||
|
"""Start WebSocket connection for a specific symbol"""
|
||||||
|
if not WEBSOCKETS_AVAILABLE:
|
||||||
|
logger.warning(f"⚠️ WebSockets not available for {symbol}, starting REST fallback")
|
||||||
|
await self._start_rest_fallback(symbol)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Cancel existing task if running
|
||||||
|
if symbol in self.websocket_tasks and not self.websocket_tasks[symbol].done():
|
||||||
|
self.websocket_tasks[symbol].cancel()
|
||||||
|
|
||||||
|
# Start new WebSocket task
|
||||||
|
self.websocket_tasks[symbol] = asyncio.create_task(
|
||||||
|
self._websocket_connection_loop(symbol)
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"🔌 Started WebSocket task for {symbol}")
|
||||||
|
|
||||||
|
async def _websocket_connection_loop(self, symbol: str):
|
||||||
|
"""Main WebSocket connection loop with reconnection logic"""
|
||||||
|
status = self.status[symbol]
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
logger.info(f"🔌 Attempting WebSocket connection for {symbol} (attempt {status.connection_attempts + 1})")
|
||||||
|
status.connection_attempts += 1
|
||||||
|
|
||||||
|
# Create WebSocket URL with maximum depth
|
||||||
|
ws_symbol = symbol.replace('/', '').lower() # BTCUSDT, ETHUSDT
|
||||||
|
ws_url = f"wss://stream.binance.com:9443/ws/{ws_symbol}@depth@{self.update_speed}"
|
||||||
|
|
||||||
|
logger.info(f"🔗 Connecting to: {ws_url}")
|
||||||
|
|
||||||
|
async with websockets_connect(ws_url) as websocket:
|
||||||
|
# Connection successful
|
||||||
|
status.connected = True
|
||||||
|
status.last_error = None
|
||||||
|
status.reset_reconnect_delay()
|
||||||
|
|
||||||
|
logger.info(f"✅ WebSocket connected for {symbol}")
|
||||||
|
await self._notify_dashboard_status(symbol, "connected", "WebSocket connected")
|
||||||
|
|
||||||
|
# Deactivate REST fallback
|
||||||
|
if self.rest_fallback_active[symbol]:
|
||||||
|
await self._stop_rest_fallback(symbol)
|
||||||
|
|
||||||
|
# Message receiving loop
|
||||||
|
async for message in websocket:
|
||||||
|
try:
|
||||||
|
data = json.loads(message)
|
||||||
|
await self._process_websocket_message(symbol, data)
|
||||||
|
|
||||||
|
status.last_message_time = datetime.now()
|
||||||
|
status.messages_received += 1
|
||||||
|
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
logger.warning(f"⚠️ Invalid JSON from {symbol} WebSocket: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Error processing WebSocket message for {symbol}: {e}")
|
||||||
|
|
||||||
|
except ConnectionClosed as e:
|
||||||
|
status.connected = False
|
||||||
|
status.last_error = f"Connection closed: {e}"
|
||||||
|
logger.warning(f"🔌 WebSocket connection closed for {symbol}: {e}")
|
||||||
|
|
||||||
|
except WebSocketException as e:
|
||||||
|
status.connected = False
|
||||||
|
status.last_error = f"WebSocket error: {e}"
|
||||||
|
logger.error(f"❌ WebSocket error for {symbol}: {e}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
status.connected = False
|
||||||
|
status.last_error = f"Unexpected error: {e}"
|
||||||
|
logger.error(f"❌ Unexpected WebSocket error for {symbol}: {e}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
# Connection failed or closed - start REST fallback
|
||||||
|
await self._notify_dashboard_status(symbol, "disconnected", status.last_error)
|
||||||
|
await self._start_rest_fallback(symbol)
|
||||||
|
|
||||||
|
# Wait before reconnecting
|
||||||
|
status.increase_reconnect_delay()
|
||||||
|
logger.info(f"⏳ Waiting {status.reconnect_delay:.1f}s before reconnecting {symbol}")
|
||||||
|
await asyncio.sleep(status.reconnect_delay)
|
||||||
|
|
||||||
|
async def _process_websocket_message(self, symbol: str, data: Dict):
|
||||||
|
"""Process WebSocket message and convert to COB format"""
|
||||||
|
try:
|
||||||
|
# Binance depth stream format
|
||||||
|
if 'b' in data and 'a' in data: # bids and asks
|
||||||
|
cob_data = {
|
||||||
|
'symbol': symbol,
|
||||||
|
'timestamp': datetime.now(),
|
||||||
|
'bids': [{'price': float(bid[0]), 'size': float(bid[1])} for bid in data['b']],
|
||||||
|
'asks': [{'price': float(ask[0]), 'size': float(ask[1])} for ask in data['a']],
|
||||||
|
'source': 'websocket',
|
||||||
|
'exchange': 'binance'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Calculate stats
|
||||||
|
if cob_data['bids'] and cob_data['asks']:
|
||||||
|
best_bid = max(cob_data['bids'], key=lambda x: x['price'])
|
||||||
|
best_ask = min(cob_data['asks'], key=lambda x: x['price'])
|
||||||
|
|
||||||
|
cob_data['stats'] = {
|
||||||
|
'best_bid': best_bid['price'],
|
||||||
|
'best_ask': best_ask['price'],
|
||||||
|
'spread': best_ask['price'] - best_bid['price'],
|
||||||
|
'mid_price': (best_bid['price'] + best_ask['price']) / 2,
|
||||||
|
'bid_volume': sum(bid['size'] for bid in cob_data['bids']),
|
||||||
|
'ask_volume': sum(ask['size'] for ask in cob_data['asks'])
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update cache
|
||||||
|
self.latest_cob_data[symbol] = cob_data
|
||||||
|
|
||||||
|
# Notify callbacks
|
||||||
|
for callback in self.cob_callbacks:
|
||||||
|
try:
|
||||||
|
await callback(symbol, cob_data)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Error in COB callback: {e}")
|
||||||
|
|
||||||
|
logger.debug(f"📊 Processed WebSocket COB data for {symbol}: {len(cob_data['bids'])} bids, {len(cob_data['asks'])} asks")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Error processing WebSocket message for {symbol}: {e}")
|
||||||
|
|
||||||
|
async def _start_rest_fallback(self, symbol: str):
|
||||||
|
"""Start REST API fallback for a symbol"""
|
||||||
|
if self.rest_fallback_active[symbol]:
|
||||||
|
return # Already active
|
||||||
|
|
||||||
|
self.rest_fallback_active[symbol] = True
|
||||||
|
|
||||||
|
# Cancel existing REST task
|
||||||
|
if symbol in self.rest_tasks and not self.rest_tasks[symbol].done():
|
||||||
|
self.rest_tasks[symbol].cancel()
|
||||||
|
|
||||||
|
# Start new REST task
|
||||||
|
self.rest_tasks[symbol] = asyncio.create_task(
|
||||||
|
self._rest_fallback_loop(symbol)
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.warning(f"⚠️ Started REST API fallback for {symbol}")
|
||||||
|
await self._notify_dashboard_status(symbol, "fallback", "Using REST API fallback")
|
||||||
|
|
||||||
|
async def _stop_rest_fallback(self, symbol: str):
|
||||||
|
"""Stop REST API fallback for a symbol"""
|
||||||
|
if not self.rest_fallback_active[symbol]:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.rest_fallback_active[symbol] = False
|
||||||
|
|
||||||
|
if symbol in self.rest_tasks and not self.rest_tasks[symbol].done():
|
||||||
|
self.rest_tasks[symbol].cancel()
|
||||||
|
|
||||||
|
logger.info(f"✅ Stopped REST API fallback for {symbol}")
|
||||||
|
|
||||||
|
async def _rest_fallback_loop(self, symbol: str):
|
||||||
|
"""REST API fallback loop"""
|
||||||
|
while self.rest_fallback_active[symbol]:
|
||||||
|
try:
|
||||||
|
await self._fetch_rest_orderbook(symbol)
|
||||||
|
await asyncio.sleep(1) # Update every second
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ REST fallback error for {symbol}: {e}")
|
||||||
|
await asyncio.sleep(5) # Wait longer on error
|
||||||
|
|
||||||
|
async def _fetch_rest_orderbook(self, symbol: str):
|
||||||
|
"""Fetch order book data via REST API"""
|
||||||
|
try:
|
||||||
|
if not self.rest_session:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Binance REST API
|
||||||
|
rest_symbol = symbol.replace('/', '') # BTCUSDT, ETHUSDT
|
||||||
|
url = f"https://api.binance.com/api/v3/depth?symbol={rest_symbol}&limit=1000"
|
||||||
|
|
||||||
|
async with self.rest_session.get(url) as response:
|
||||||
|
if response.status == 200:
|
||||||
|
data = await response.json()
|
||||||
|
|
||||||
|
cob_data = {
|
||||||
|
'symbol': symbol,
|
||||||
|
'timestamp': datetime.now(),
|
||||||
|
'bids': [{'price': float(bid[0]), 'size': float(bid[1])} for bid in data['bids']],
|
||||||
|
'asks': [{'price': float(ask[0]), 'size': float(ask[1])} for ask in data['asks']],
|
||||||
|
'source': 'rest_fallback',
|
||||||
|
'exchange': 'binance'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Calculate stats
|
||||||
|
if cob_data['bids'] and cob_data['asks']:
|
||||||
|
best_bid = max(cob_data['bids'], key=lambda x: x['price'])
|
||||||
|
best_ask = min(cob_data['asks'], key=lambda x: x['price'])
|
||||||
|
|
||||||
|
cob_data['stats'] = {
|
||||||
|
'best_bid': best_bid['price'],
|
||||||
|
'best_ask': best_ask['price'],
|
||||||
|
'spread': best_ask['price'] - best_bid['price'],
|
||||||
|
'mid_price': (best_bid['price'] + best_ask['price']) / 2,
|
||||||
|
'bid_volume': sum(bid['size'] for bid in cob_data['bids']),
|
||||||
|
'ask_volume': sum(ask['size'] for ask in cob_data['asks'])
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update cache
|
||||||
|
self.latest_cob_data[symbol] = cob_data
|
||||||
|
|
||||||
|
# Notify callbacks
|
||||||
|
for callback in self.cob_callbacks:
|
||||||
|
try:
|
||||||
|
await callback(symbol, cob_data)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Error in COB callback: {e}")
|
||||||
|
|
||||||
|
logger.debug(f"📊 Fetched REST COB data for {symbol}: {len(cob_data['bids'])} bids, {len(cob_data['asks'])} asks")
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.warning(f"⚠️ REST API error for {symbol}: HTTP {response.status}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Error fetching REST order book for {symbol}: {e}")
|
||||||
|
|
||||||
|
async def _monitor_connections(self):
|
||||||
|
"""Monitor WebSocket connections and provide status updates"""
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
await asyncio.sleep(10) # Check every 10 seconds
|
||||||
|
|
||||||
|
for symbol in self.symbols:
|
||||||
|
status = self.status[symbol]
|
||||||
|
|
||||||
|
# Check for stale connections
|
||||||
|
if status.connected and status.last_message_time:
|
||||||
|
time_since_last = datetime.now() - status.last_message_time
|
||||||
|
if time_since_last > timedelta(seconds=30):
|
||||||
|
logger.warning(f"⚠️ No messages from {symbol} WebSocket for {time_since_last.total_seconds():.0f}s")
|
||||||
|
await self._notify_dashboard_status(symbol, "stale", "No recent messages")
|
||||||
|
|
||||||
|
# Log status
|
||||||
|
if status.connected:
|
||||||
|
logger.debug(f"✅ {symbol}: Connected, {status.messages_received} messages received")
|
||||||
|
elif self.rest_fallback_active[symbol]:
|
||||||
|
logger.debug(f"⚠️ {symbol}: Using REST fallback")
|
||||||
|
else:
|
||||||
|
logger.debug(f"❌ {symbol}: Disconnected, last error: {status.last_error}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Error in connection monitor: {e}")
|
||||||
|
|
||||||
|
async def _notify_dashboard_status(self, symbol: str, status: str, message: str):
|
||||||
|
"""Notify dashboard of status changes"""
|
||||||
|
try:
|
||||||
|
if self.dashboard_callback:
|
||||||
|
await self.dashboard_callback({
|
||||||
|
'type': 'cob_status',
|
||||||
|
'symbol': symbol,
|
||||||
|
'status': status,
|
||||||
|
'message': message,
|
||||||
|
'timestamp': datetime.now().isoformat()
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Error notifying dashboard: {e}")
|
||||||
|
|
||||||
|
def get_status_summary(self) -> Dict[str, Any]:
|
||||||
|
"""Get status summary for all symbols"""
|
||||||
|
summary = {
|
||||||
|
'websockets_available': WEBSOCKETS_AVAILABLE,
|
||||||
|
'symbols': {},
|
||||||
|
'overall_status': 'unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
connected_count = 0
|
||||||
|
fallback_count = 0
|
||||||
|
|
||||||
|
for symbol in self.symbols:
|
||||||
|
status = self.status[symbol]
|
||||||
|
symbol_status = {
|
||||||
|
'connected': status.connected,
|
||||||
|
'last_message_time': status.last_message_time.isoformat() if status.last_message_time else None,
|
||||||
|
'connection_attempts': status.connection_attempts,
|
||||||
|
'last_error': status.last_error,
|
||||||
|
'messages_received': status.messages_received,
|
||||||
|
'rest_fallback_active': self.rest_fallback_active[symbol]
|
||||||
|
}
|
||||||
|
|
||||||
|
if status.connected:
|
||||||
|
connected_count += 1
|
||||||
|
elif self.rest_fallback_active[symbol]:
|
||||||
|
fallback_count += 1
|
||||||
|
|
||||||
|
summary['symbols'][symbol] = symbol_status
|
||||||
|
|
||||||
|
# Determine overall status
|
||||||
|
if connected_count == len(self.symbols):
|
||||||
|
summary['overall_status'] = 'all_connected'
|
||||||
|
elif connected_count + fallback_count == len(self.symbols):
|
||||||
|
summary['overall_status'] = 'partial_fallback'
|
||||||
|
else:
|
||||||
|
summary['overall_status'] = 'degraded'
|
||||||
|
|
||||||
|
return summary
|
||||||
|
|
||||||
|
# Global instance for easy access
|
||||||
|
enhanced_cob_websocket: Optional[EnhancedCOBWebSocket] = None
|
||||||
|
|
||||||
|
async def get_enhanced_cob_websocket(symbols: List[str] = None, dashboard_callback: Callable = None) -> EnhancedCOBWebSocket:
|
||||||
|
"""Get or create the global enhanced COB WebSocket instance"""
|
||||||
|
global enhanced_cob_websocket
|
||||||
|
|
||||||
|
if enhanced_cob_websocket is None:
|
||||||
|
enhanced_cob_websocket = EnhancedCOBWebSocket(symbols, dashboard_callback)
|
||||||
|
await enhanced_cob_websocket.start()
|
||||||
|
|
||||||
|
return enhanced_cob_websocket
|
||||||
|
|
||||||
|
async def stop_enhanced_cob_websocket():
|
||||||
|
"""Stop the global enhanced COB WebSocket instance"""
|
||||||
|
global enhanced_cob_websocket
|
||||||
|
|
||||||
|
if enhanced_cob_websocket:
|
||||||
|
await enhanced_cob_websocket.stop()
|
||||||
|
enhanced_cob_websocket = None
|
@ -1369,15 +1369,31 @@ class TradingOrchestrator:
|
|||||||
reasoning['models_aggregated'] = [pred.model_name for pred in predictions]
|
reasoning['models_aggregated'] = [pred.model_name for pred in predictions]
|
||||||
reasoning['aggregated_confidence'] = best_confidence
|
reasoning['aggregated_confidence'] = best_confidence
|
||||||
|
|
||||||
# Apply confidence thresholds for signal confirmation
|
# Calculate dynamic aggressiveness based on recent performance
|
||||||
|
entry_aggressiveness = self._calculate_dynamic_entry_aggressiveness(symbol)
|
||||||
|
|
||||||
|
# Adjust confidence threshold based on entry aggressiveness
|
||||||
|
# Higher aggressiveness = lower threshold (more trades)
|
||||||
|
# entry_aggressiveness: 0.0 = very conservative, 1.0 = very aggressive
|
||||||
|
base_threshold = self.confidence_threshold
|
||||||
|
aggressiveness_factor = 1.0 - entry_aggressiveness # Invert: high agg = low factor
|
||||||
|
dynamic_threshold = base_threshold * aggressiveness_factor
|
||||||
|
|
||||||
|
# Ensure minimum threshold for safety (don't go below 1% confidence)
|
||||||
|
dynamic_threshold = max(0.01, dynamic_threshold)
|
||||||
|
|
||||||
|
# Apply dynamic confidence threshold for signal confirmation
|
||||||
if best_action != 'HOLD':
|
if best_action != 'HOLD':
|
||||||
if best_confidence < self.confidence_threshold:
|
if best_confidence < dynamic_threshold:
|
||||||
logger.debug(f"Signal below confidence threshold: {best_action} {symbol} "
|
logger.debug(f"Signal below dynamic confidence threshold: {best_action} {symbol} "
|
||||||
f"(confidence: {best_confidence:.3f} < {self.confidence_threshold})")
|
f"(confidence: {best_confidence:.3f} < {dynamic_threshold:.3f}, "
|
||||||
|
f"base: {base_threshold:.3f}, aggressiveness: {entry_aggressiveness:.2f})")
|
||||||
best_action = 'HOLD'
|
best_action = 'HOLD'
|
||||||
best_confidence = 0.0
|
best_confidence = 0.0
|
||||||
reasoning['rejected_reason'] = 'low_confidence'
|
|
||||||
else:
|
else:
|
||||||
|
logger.info(f"SIGNAL ACCEPTED: {best_action} {symbol} "
|
||||||
|
f"(confidence: {best_confidence:.3f} >= {dynamic_threshold:.3f}, "
|
||||||
|
f"aggressiveness: {entry_aggressiveness:.2f})")
|
||||||
# Add signal to accumulator for trend confirmation
|
# Add signal to accumulator for trend confirmation
|
||||||
signal_data = {
|
signal_data = {
|
||||||
'action': best_action,
|
'action': best_action,
|
||||||
@ -1418,8 +1434,7 @@ class TradingOrchestrator:
|
|||||||
except Exception:
|
except Exception:
|
||||||
memory_usage = {}
|
memory_usage = {}
|
||||||
|
|
||||||
# Calculate dynamic aggressiveness based on recent performance
|
# Get exit aggressiveness (entry aggressiveness already calculated above)
|
||||||
entry_aggressiveness = self._calculate_dynamic_entry_aggressiveness(symbol)
|
|
||||||
exit_aggressiveness = self._calculate_dynamic_exit_aggressiveness(symbol, current_position_pnl)
|
exit_aggressiveness = self._calculate_dynamic_exit_aggressiveness(symbol, current_position_pnl)
|
||||||
|
|
||||||
# Create final decision
|
# Create final decision
|
||||||
@ -1440,6 +1455,9 @@ class TradingOrchestrator:
|
|||||||
f"entry_agg: {entry_aggressiveness:.2f}, exit_agg: {exit_aggressiveness:.2f}, "
|
f"entry_agg: {entry_aggressiveness:.2f}, exit_agg: {exit_aggressiveness:.2f}, "
|
||||||
f"pnl: ${current_position_pnl:.2f})")
|
f"pnl: ${current_position_pnl:.2f})")
|
||||||
|
|
||||||
|
# Trigger training on each decision (especially for executed trades)
|
||||||
|
self._trigger_training_on_decision(decision, price)
|
||||||
|
|
||||||
return decision
|
return decision
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -2032,6 +2050,253 @@ class TradingOrchestrator:
|
|||||||
logger.error(f"Error calculating enhanced reward: {e}")
|
logger.error(f"Error calculating enhanced reward: {e}")
|
||||||
return base_pnl
|
return base_pnl
|
||||||
|
|
||||||
|
def _trigger_training_on_decision(self, decision: TradingDecision, current_price: float):
|
||||||
|
"""Trigger training on each decision, especially executed trades
|
||||||
|
|
||||||
|
This ensures models learn from every signal outcome, giving more weight
|
||||||
|
to executed trades as they have real market feedback.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Only train if training is enabled and we have the enhanced training system
|
||||||
|
if not self.training_enabled or not self.enhanced_training_system:
|
||||||
|
return
|
||||||
|
|
||||||
|
symbol = decision.symbol
|
||||||
|
action = decision.action
|
||||||
|
confidence = decision.confidence
|
||||||
|
|
||||||
|
# Create training data from the decision
|
||||||
|
training_data = {
|
||||||
|
'symbol': symbol,
|
||||||
|
'action': action,
|
||||||
|
'confidence': confidence,
|
||||||
|
'price': current_price,
|
||||||
|
'timestamp': decision.timestamp,
|
||||||
|
'executed': action != 'HOLD', # Assume non-HOLD actions are executed
|
||||||
|
'entry_aggressiveness': decision.entry_aggressiveness,
|
||||||
|
'exit_aggressiveness': decision.exit_aggressiveness,
|
||||||
|
'reasoning': decision.reasoning
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add to enhanced training system for immediate learning
|
||||||
|
if hasattr(self.enhanced_training_system, 'add_decision_for_training'):
|
||||||
|
self.enhanced_training_system.add_decision_for_training(training_data)
|
||||||
|
logger.debug(f"🎓 Added decision to training queue: {action} {symbol} (conf: {confidence:.3f})")
|
||||||
|
|
||||||
|
# Trigger immediate training for executed trades (higher priority)
|
||||||
|
if action != 'HOLD':
|
||||||
|
if hasattr(self.enhanced_training_system, 'trigger_immediate_training'):
|
||||||
|
self.enhanced_training_system.trigger_immediate_training(
|
||||||
|
symbol=symbol,
|
||||||
|
priority='high' if confidence > 0.7 else 'medium'
|
||||||
|
)
|
||||||
|
logger.info(f"🚀 Triggered immediate training for executed trade: {action} {symbol}")
|
||||||
|
|
||||||
|
# Train all models on the decision outcome
|
||||||
|
self._train_models_on_decision(decision, current_price)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error triggering training on decision: {e}")
|
||||||
|
|
||||||
|
def _train_models_on_decision(self, decision: TradingDecision, current_price: float):
|
||||||
|
"""Train all models on the decision outcome
|
||||||
|
|
||||||
|
This provides immediate feedback to models about their predictions,
|
||||||
|
allowing them to learn from each signal they generate.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
symbol = decision.symbol
|
||||||
|
action = decision.action
|
||||||
|
confidence = decision.confidence
|
||||||
|
|
||||||
|
# Get current market data for training context
|
||||||
|
market_data = self._get_current_market_data(symbol)
|
||||||
|
if not market_data:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Train DQN agent if available
|
||||||
|
if self.rl_agent and hasattr(self.rl_agent, 'add_experience'):
|
||||||
|
try:
|
||||||
|
# Create state representation
|
||||||
|
state = self._create_state_for_training(symbol, market_data)
|
||||||
|
|
||||||
|
# Map action to DQN action space
|
||||||
|
action_mapping = {'BUY': 0, 'SELL': 1, 'HOLD': 2}
|
||||||
|
dqn_action = action_mapping.get(action, 2)
|
||||||
|
|
||||||
|
# Calculate immediate reward based on confidence and execution
|
||||||
|
immediate_reward = confidence if action != 'HOLD' else 0.0
|
||||||
|
|
||||||
|
# Add experience to DQN
|
||||||
|
self.rl_agent.add_experience(
|
||||||
|
state=state,
|
||||||
|
action=dqn_action,
|
||||||
|
reward=immediate_reward,
|
||||||
|
next_state=state, # Will be updated with actual outcome later
|
||||||
|
done=False
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.debug(f"🧠 Added DQN experience: {action} {symbol} (reward: {immediate_reward:.3f})")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Error training DQN on decision: {e}")
|
||||||
|
|
||||||
|
# Train CNN model if available
|
||||||
|
if self.cnn_model and hasattr(self.cnn_model, 'add_training_sample'):
|
||||||
|
try:
|
||||||
|
# Create CNN input features
|
||||||
|
cnn_features = self._create_cnn_features_for_training(symbol, market_data)
|
||||||
|
|
||||||
|
# Create target based on action
|
||||||
|
target_mapping = {'BUY': [1, 0, 0], 'SELL': [0, 1, 0], 'HOLD': [0, 0, 1]}
|
||||||
|
target = target_mapping.get(action, [0, 0, 1])
|
||||||
|
|
||||||
|
# Add training sample
|
||||||
|
self.cnn_model.add_training_sample(cnn_features, target, weight=confidence)
|
||||||
|
|
||||||
|
logger.debug(f"🔍 Added CNN training sample: {action} {symbol}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Error training CNN on decision: {e}")
|
||||||
|
|
||||||
|
# Train COB RL model if available and we have COB data
|
||||||
|
if self.cob_rl_agent and symbol in self.latest_cob_data:
|
||||||
|
try:
|
||||||
|
cob_data = self.latest_cob_data[symbol]
|
||||||
|
if hasattr(self.cob_rl_agent, 'add_experience'):
|
||||||
|
# Create COB state representation
|
||||||
|
cob_state = self._create_cob_state_for_training(symbol, cob_data)
|
||||||
|
|
||||||
|
# Add COB experience
|
||||||
|
self.cob_rl_agent.add_experience(
|
||||||
|
state=cob_state,
|
||||||
|
action=action,
|
||||||
|
reward=confidence,
|
||||||
|
symbol=symbol
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.debug(f"📊 Added COB RL experience: {action} {symbol}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Error training COB RL on decision: {e}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error training models on decision: {e}")
|
||||||
|
|
||||||
|
def _get_current_market_data(self, symbol: str) -> Optional[Dict]:
|
||||||
|
"""Get current market data for training context"""
|
||||||
|
try:
|
||||||
|
if self.data_provider:
|
||||||
|
# Get recent data for training
|
||||||
|
df = self.data_provider.get_historical_data(symbol, '1m', limit=100)
|
||||||
|
if df is not None and not df.empty:
|
||||||
|
return {
|
||||||
|
'ohlcv': df.tail(50).to_dict('records'), # Last 50 candles
|
||||||
|
'current_price': float(df['close'].iloc[-1]),
|
||||||
|
'volume': float(df['volume'].iloc[-1]),
|
||||||
|
'timestamp': df.index[-1]
|
||||||
|
}
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Error getting market data for training: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _create_state_for_training(self, symbol: str, market_data: Dict) -> np.ndarray:
|
||||||
|
"""Create state representation for DQN training"""
|
||||||
|
try:
|
||||||
|
# Create a basic state representation
|
||||||
|
ohlcv_data = market_data.get('ohlcv', [])
|
||||||
|
if not ohlcv_data:
|
||||||
|
return np.zeros(100) # Default state size
|
||||||
|
|
||||||
|
# Extract features from recent candles
|
||||||
|
features = []
|
||||||
|
for candle in ohlcv_data[-20:]: # Last 20 candles
|
||||||
|
features.extend([
|
||||||
|
candle.get('open', 0),
|
||||||
|
candle.get('high', 0),
|
||||||
|
candle.get('low', 0),
|
||||||
|
candle.get('close', 0),
|
||||||
|
candle.get('volume', 0)
|
||||||
|
])
|
||||||
|
|
||||||
|
# Pad or truncate to expected size
|
||||||
|
state = np.array(features[:100])
|
||||||
|
if len(state) < 100:
|
||||||
|
state = np.pad(state, (0, 100 - len(state)), 'constant')
|
||||||
|
|
||||||
|
return state
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Error creating state for training: {e}")
|
||||||
|
return np.zeros(100)
|
||||||
|
|
||||||
|
def _create_cnn_features_for_training(self, symbol: str, market_data: Dict) -> np.ndarray:
|
||||||
|
"""Create CNN features for training"""
|
||||||
|
try:
|
||||||
|
# Similar to state creation but formatted for CNN
|
||||||
|
ohlcv_data = market_data.get('ohlcv', [])
|
||||||
|
if not ohlcv_data:
|
||||||
|
return np.zeros((1, 100))
|
||||||
|
|
||||||
|
# Create feature matrix
|
||||||
|
features = []
|
||||||
|
for candle in ohlcv_data[-20:]:
|
||||||
|
features.extend([
|
||||||
|
candle.get('open', 0),
|
||||||
|
candle.get('high', 0),
|
||||||
|
candle.get('low', 0),
|
||||||
|
candle.get('close', 0),
|
||||||
|
candle.get('volume', 0)
|
||||||
|
])
|
||||||
|
|
||||||
|
# Reshape for CNN input
|
||||||
|
cnn_features = np.array(features[:100]).reshape(1, -1)
|
||||||
|
if cnn_features.shape[1] < 100:
|
||||||
|
cnn_features = np.pad(cnn_features, ((0, 0), (0, 100 - cnn_features.shape[1])), 'constant')
|
||||||
|
|
||||||
|
return cnn_features
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Error creating CNN features for training: {e}")
|
||||||
|
return np.zeros((1, 100))
|
||||||
|
|
||||||
|
def _create_cob_state_for_training(self, symbol: str, cob_data: Dict) -> np.ndarray:
|
||||||
|
"""Create COB state representation for training"""
|
||||||
|
try:
|
||||||
|
# Extract COB features for training
|
||||||
|
features = []
|
||||||
|
|
||||||
|
# Add bid/ask data
|
||||||
|
bids = cob_data.get('bids', [])[:10] # Top 10 bids
|
||||||
|
asks = cob_data.get('asks', [])[:10] # Top 10 asks
|
||||||
|
|
||||||
|
for bid in bids:
|
||||||
|
features.extend([bid.get('price', 0), bid.get('size', 0)])
|
||||||
|
for ask in asks:
|
||||||
|
features.extend([ask.get('price', 0), ask.get('size', 0)])
|
||||||
|
|
||||||
|
# Add market stats
|
||||||
|
stats = cob_data.get('stats', {})
|
||||||
|
features.extend([
|
||||||
|
stats.get('spread', 0),
|
||||||
|
stats.get('mid_price', 0),
|
||||||
|
stats.get('bid_volume', 0),
|
||||||
|
stats.get('ask_volume', 0),
|
||||||
|
stats.get('imbalance', 0)
|
||||||
|
])
|
||||||
|
|
||||||
|
# Pad to expected COB state size (2000 features)
|
||||||
|
cob_state = np.array(features[:2000])
|
||||||
|
if len(cob_state) < 2000:
|
||||||
|
cob_state = np.pad(cob_state, (0, 2000 - len(cob_state)), 'constant')
|
||||||
|
|
||||||
|
return cob_state
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Error creating COB state for training: {e}")
|
||||||
|
return np.zeros(2000)
|
||||||
|
|
||||||
def _check_signal_confirmation(self, symbol: str, signal_data: Dict) -> Optional[str]:
|
def _check_signal_confirmation(self, symbol: str, signal_data: Dict) -> Optional[str]:
|
||||||
"""Check if we have enough signal confirmations for trend confirmation with rate limiting"""
|
"""Check if we have enough signal confirmations for trend confirmation with rate limiting"""
|
||||||
try:
|
try:
|
||||||
|
148
test_enhanced_cob_websocket.py
Normal file
148
test_enhanced_cob_websocket.py
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test Enhanced COB WebSocket Implementation
|
||||||
|
|
||||||
|
This script tests the enhanced COB WebSocket system to ensure:
|
||||||
|
1. WebSocket connections work properly
|
||||||
|
2. Fallback to REST API when WebSocket fails
|
||||||
|
3. Dashboard status updates are working
|
||||||
|
4. Clear error messages and warnings are displayed
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Setup logging
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Import the enhanced COB WebSocket
|
||||||
|
try:
|
||||||
|
from core.enhanced_cob_websocket import EnhancedCOBWebSocket, get_enhanced_cob_websocket
|
||||||
|
print("✅ Enhanced COB WebSocket imported successfully")
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"❌ Failed to import Enhanced COB WebSocket: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
async def test_dashboard_callback(status_data):
|
||||||
|
"""Test dashboard callback function"""
|
||||||
|
print(f"📊 Dashboard callback received: {status_data}")
|
||||||
|
|
||||||
|
async def test_cob_callback(symbol, cob_data):
|
||||||
|
"""Test COB data callback function"""
|
||||||
|
stats = cob_data.get('stats', {})
|
||||||
|
mid_price = stats.get('mid_price', 0)
|
||||||
|
bid_levels = len(cob_data.get('bids', []))
|
||||||
|
ask_levels = len(cob_data.get('asks', []))
|
||||||
|
source = cob_data.get('source', 'unknown')
|
||||||
|
|
||||||
|
print(f"📈 COB data for {symbol}: ${mid_price:.2f}, {bid_levels} bids, {ask_levels} asks (via {source})")
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
"""Main test function"""
|
||||||
|
print("🚀 Testing Enhanced COB WebSocket System")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Test 1: Initialize Enhanced COB WebSocket
|
||||||
|
print("\n1. Initializing Enhanced COB WebSocket...")
|
||||||
|
try:
|
||||||
|
cob_ws = EnhancedCOBWebSocket(
|
||||||
|
symbols=['BTC/USDT', 'ETH/USDT'],
|
||||||
|
dashboard_callback=test_dashboard_callback
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add callbacks
|
||||||
|
cob_ws.add_cob_callback(test_cob_callback)
|
||||||
|
|
||||||
|
print("✅ Enhanced COB WebSocket initialized")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Failed to initialize: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Test 2: Start WebSocket connections
|
||||||
|
print("\n2. Starting WebSocket connections...")
|
||||||
|
try:
|
||||||
|
await cob_ws.start()
|
||||||
|
print("✅ WebSocket connections started")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Failed to start connections: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Test 3: Monitor connections for 30 seconds
|
||||||
|
print("\n3. Monitoring connections for 30 seconds...")
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
while time.time() - start_time < 30:
|
||||||
|
try:
|
||||||
|
# Get status summary
|
||||||
|
status = cob_ws.get_status_summary()
|
||||||
|
overall_status = status.get('overall_status', 'unknown')
|
||||||
|
|
||||||
|
print(f"⏱️ Status: {overall_status}")
|
||||||
|
|
||||||
|
# Print symbol-specific status
|
||||||
|
for symbol, symbol_status in status.get('symbols', {}).items():
|
||||||
|
connected = symbol_status.get('connected', False)
|
||||||
|
fallback = symbol_status.get('rest_fallback_active', False)
|
||||||
|
messages = symbol_status.get('messages_received', 0)
|
||||||
|
|
||||||
|
if connected:
|
||||||
|
print(f" {symbol}: ✅ Connected ({messages} messages)")
|
||||||
|
elif fallback:
|
||||||
|
print(f" {symbol}: ⚠️ REST fallback active")
|
||||||
|
else:
|
||||||
|
error = symbol_status.get('last_error', 'Unknown error')
|
||||||
|
print(f" {symbol}: ❌ Error - {error}")
|
||||||
|
|
||||||
|
await asyncio.sleep(5) # Check every 5 seconds
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n⏹️ Test interrupted by user")
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error during monitoring: {e}")
|
||||||
|
break
|
||||||
|
|
||||||
|
# Test 4: Final status check
|
||||||
|
print("\n4. Final status check...")
|
||||||
|
try:
|
||||||
|
final_status = cob_ws.get_status_summary()
|
||||||
|
print(f"Final overall status: {final_status.get('overall_status', 'unknown')}")
|
||||||
|
|
||||||
|
for symbol, symbol_status in final_status.get('symbols', {}).items():
|
||||||
|
print(f" {symbol}:")
|
||||||
|
print(f" Connected: {symbol_status.get('connected', False)}")
|
||||||
|
print(f" Messages received: {symbol_status.get('messages_received', 0)}")
|
||||||
|
print(f" REST fallback: {symbol_status.get('rest_fallback_active', False)}")
|
||||||
|
if symbol_status.get('last_error'):
|
||||||
|
print(f" Last error: {symbol_status.get('last_error')}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error getting final status: {e}")
|
||||||
|
|
||||||
|
# Test 5: Stop connections
|
||||||
|
print("\n5. Stopping connections...")
|
||||||
|
try:
|
||||||
|
await cob_ws.stop()
|
||||||
|
print("✅ Connections stopped successfully")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error stopping connections: {e}")
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("🏁 Enhanced COB WebSocket test completed")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
asyncio.run(main())
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n⏹️ Test interrupted")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Test failed: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
@ -223,8 +223,8 @@ class CleanTradingDashboard:
|
|||||||
# Universal Data Adapter is managed by orchestrator
|
# Universal Data Adapter is managed by orchestrator
|
||||||
logger.debug("Universal Data Adapter ready for orchestrator data access")
|
logger.debug("Universal Data Adapter ready for orchestrator data access")
|
||||||
|
|
||||||
# Initialize COB integration with high-frequency data handling
|
# Initialize COB integration with enhanced WebSocket
|
||||||
self._initialize_cob_integration()
|
self._initialize_enhanced_cob_integration()
|
||||||
|
|
||||||
# Start signal generation loop to ensure continuous trading signals
|
# Start signal generation loop to ensure continuous trading signals
|
||||||
self._start_signal_generation_loop()
|
self._start_signal_generation_loop()
|
||||||
@ -497,6 +497,7 @@ class CleanTradingDashboard:
|
|||||||
Output('trade-count', 'children'),
|
Output('trade-count', 'children'),
|
||||||
Output('portfolio-value', 'children'),
|
Output('portfolio-value', 'children'),
|
||||||
Output('profitability-multiplier', 'children'),
|
Output('profitability-multiplier', 'children'),
|
||||||
|
Output('cob-websocket-status', 'children'),
|
||||||
Output('mexc-status', 'children')],
|
Output('mexc-status', 'children')],
|
||||||
[Input('interval-component', 'n_intervals')]
|
[Input('interval-component', 'n_intervals')]
|
||||||
)
|
)
|
||||||
@ -622,11 +623,27 @@ class CleanTradingDashboard:
|
|||||||
if hasattr(self.trading_executor, 'simulation_mode') and not self.trading_executor.simulation_mode:
|
if hasattr(self.trading_executor, 'simulation_mode') and not self.trading_executor.simulation_mode:
|
||||||
mexc_status = "LIVE+SYNC" # Indicate live trading with position sync
|
mexc_status = "LIVE+SYNC" # Indicate live trading with position sync
|
||||||
|
|
||||||
return price_str, session_pnl_str, position_str, trade_str, portfolio_str, multiplier_str, mexc_status
|
# COB WebSocket status
|
||||||
|
cob_status = self.get_cob_websocket_status()
|
||||||
|
overall_status = cob_status.get('overall_status', 'unknown')
|
||||||
|
warning_message = cob_status.get('warning_message')
|
||||||
|
|
||||||
|
if overall_status == 'all_connected':
|
||||||
|
cob_status_str = "Connected"
|
||||||
|
elif overall_status == 'partial_fallback':
|
||||||
|
cob_status_str = "Fallback"
|
||||||
|
elif overall_status == 'degraded':
|
||||||
|
cob_status_str = "Degraded"
|
||||||
|
elif overall_status == 'unavailable':
|
||||||
|
cob_status_str = "N/A"
|
||||||
|
else:
|
||||||
|
cob_status_str = "Error"
|
||||||
|
|
||||||
|
return price_str, session_pnl_str, position_str, trade_str, portfolio_str, multiplier_str, cob_status_str, mexc_status
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error updating metrics: {e}")
|
logger.error(f"Error updating metrics: {e}")
|
||||||
return "Error", "$0.00", "Error", "0", "$100.00", "0.0x", "ERROR"
|
return "Error", "$0.00", "Error", "0", "$100.00", "0.0x", "Error", "ERROR"
|
||||||
|
|
||||||
@self.app.callback(
|
@self.app.callback(
|
||||||
Output('recent-decisions', 'children'),
|
Output('recent-decisions', 'children'),
|
||||||
@ -7296,3 +7313,166 @@ def create_clean_dashboard(data_provider: Optional[DataProvider] = None, orchest
|
|||||||
|
|
||||||
|
|
||||||
# test edit
|
# test edit
|
||||||
|
def _initialize_enhanced_cob_integration(self):
|
||||||
|
"""Initialize enhanced COB integration with WebSocket status monitoring"""
|
||||||
|
try:
|
||||||
|
if not COB_INTEGRATION_AVAILABLE:
|
||||||
|
logger.warning("⚠️ COB integration not available - WebSocket status will show as unavailable")
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info("🚀 Initializing Enhanced COB Integration with WebSocket monitoring")
|
||||||
|
|
||||||
|
# Initialize COB integration
|
||||||
|
self.cob_integration = COBIntegration(
|
||||||
|
data_provider=self.data_provider,
|
||||||
|
symbols=['ETH/USDT', 'BTC/USDT']
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add dashboard callback for COB data
|
||||||
|
self.cob_integration.add_dashboard_callback(self._on_enhanced_cob_update)
|
||||||
|
|
||||||
|
# Start COB integration in background thread
|
||||||
|
def start_cob_integration():
|
||||||
|
try:
|
||||||
|
import asyncio
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
loop.run_until_complete(self.cob_integration.start())
|
||||||
|
loop.run_forever()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Error in COB integration thread: {e}")
|
||||||
|
|
||||||
|
cob_thread = threading.Thread(target=start_cob_integration, daemon=True)
|
||||||
|
cob_thread.start()
|
||||||
|
|
||||||
|
logger.info("✅ Enhanced COB Integration started with WebSocket monitoring")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Error initializing Enhanced COB Integration: {e}")
|
||||||
|
|
||||||
|
def _on_enhanced_cob_update(self, symbol: str, data: Dict):
|
||||||
|
"""Handle enhanced COB updates with WebSocket status"""
|
||||||
|
try:
|
||||||
|
# Update COB data cache
|
||||||
|
self.latest_cob_data[symbol] = data
|
||||||
|
|
||||||
|
# Extract WebSocket status if available
|
||||||
|
if isinstance(data, dict) and 'type' in data:
|
||||||
|
if data['type'] == 'websocket_status':
|
||||||
|
status_data = data.get('data', {})
|
||||||
|
status = status_data.get('status', 'unknown')
|
||||||
|
message = status_data.get('message', '')
|
||||||
|
|
||||||
|
# Update COB cache with status
|
||||||
|
if symbol not in self.cob_cache:
|
||||||
|
self.cob_cache[symbol] = {'last_update': 0, 'data': None, 'updates_count': 0}
|
||||||
|
|
||||||
|
self.cob_cache[symbol]['websocket_status'] = status
|
||||||
|
self.cob_cache[symbol]['websocket_message'] = message
|
||||||
|
self.cob_cache[symbol]['last_status_update'] = time.time()
|
||||||
|
|
||||||
|
logger.info(f"🔌 COB WebSocket status for {symbol}: {status} - {message}")
|
||||||
|
|
||||||
|
elif data['type'] == 'cob_update':
|
||||||
|
# Regular COB data update
|
||||||
|
cob_data = data.get('data', {})
|
||||||
|
stats = cob_data.get('stats', {})
|
||||||
|
|
||||||
|
# Update cache
|
||||||
|
self.cob_cache[symbol]['data'] = cob_data
|
||||||
|
self.cob_cache[symbol]['last_update'] = time.time()
|
||||||
|
self.cob_cache[symbol]['updates_count'] += 1
|
||||||
|
|
||||||
|
# Update WebSocket status from stats
|
||||||
|
websocket_status = stats.get('websocket_status', 'unknown')
|
||||||
|
source = stats.get('source', 'unknown')
|
||||||
|
|
||||||
|
self.cob_cache[symbol]['websocket_status'] = websocket_status
|
||||||
|
self.cob_cache[symbol]['source'] = source
|
||||||
|
|
||||||
|
logger.debug(f"📊 Enhanced COB update for {symbol}: {websocket_status} via {source}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Error handling enhanced COB update for {symbol}: {e}")
|
||||||
|
|
||||||
|
def get_cob_websocket_status(self) -> Dict[str, Any]:
|
||||||
|
"""Get COB WebSocket status for dashboard display"""
|
||||||
|
try:
|
||||||
|
status_summary = {
|
||||||
|
'overall_status': 'unknown',
|
||||||
|
'symbols': {},
|
||||||
|
'last_update': None,
|
||||||
|
'warning_message': None
|
||||||
|
}
|
||||||
|
|
||||||
|
if not COB_INTEGRATION_AVAILABLE:
|
||||||
|
status_summary['overall_status'] = 'unavailable'
|
||||||
|
status_summary['warning_message'] = 'COB integration not available'
|
||||||
|
return status_summary
|
||||||
|
|
||||||
|
connected_count = 0
|
||||||
|
fallback_count = 0
|
||||||
|
error_count = 0
|
||||||
|
|
||||||
|
for symbol in ['ETH/USDT', 'BTC/USDT']:
|
||||||
|
symbol_status = {
|
||||||
|
'status': 'unknown',
|
||||||
|
'message': 'No data',
|
||||||
|
'last_update': None,
|
||||||
|
'source': 'unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
if symbol in self.cob_cache:
|
||||||
|
cache_data = self.cob_cache[symbol]
|
||||||
|
ws_status = cache_data.get('websocket_status', 'unknown')
|
||||||
|
source = cache_data.get('source', 'unknown')
|
||||||
|
last_update = cache_data.get('last_update', 0)
|
||||||
|
|
||||||
|
symbol_status['status'] = ws_status
|
||||||
|
symbol_status['source'] = source
|
||||||
|
symbol_status['last_update'] = datetime.fromtimestamp(last_update).isoformat() if last_update > 0 else None
|
||||||
|
|
||||||
|
# Determine status category
|
||||||
|
if ws_status == 'connected':
|
||||||
|
connected_count += 1
|
||||||
|
symbol_status['message'] = 'WebSocket connected'
|
||||||
|
elif ws_status == 'fallback' or source == 'rest_fallback':
|
||||||
|
fallback_count += 1
|
||||||
|
symbol_status['message'] = 'Using REST API fallback'
|
||||||
|
else:
|
||||||
|
error_count += 1
|
||||||
|
symbol_status['message'] = cache_data.get('websocket_message', 'Connection error')
|
||||||
|
|
||||||
|
status_summary['symbols'][symbol] = symbol_status
|
||||||
|
|
||||||
|
# Determine overall status
|
||||||
|
total_symbols = len(['ETH/USDT', 'BTC/USDT'])
|
||||||
|
|
||||||
|
if connected_count == total_symbols:
|
||||||
|
status_summary['overall_status'] = 'all_connected'
|
||||||
|
status_summary['warning_message'] = None
|
||||||
|
elif connected_count + fallback_count == total_symbols:
|
||||||
|
status_summary['overall_status'] = 'partial_fallback'
|
||||||
|
status_summary['warning_message'] = f'⚠️ {fallback_count} symbol(s) using REST fallback - WebSocket connection failed'
|
||||||
|
elif fallback_count > 0:
|
||||||
|
status_summary['overall_status'] = 'degraded'
|
||||||
|
status_summary['warning_message'] = f'⚠️ COB WebSocket degraded - {error_count} error(s), {fallback_count} fallback(s)'
|
||||||
|
else:
|
||||||
|
status_summary['overall_status'] = 'error'
|
||||||
|
status_summary['warning_message'] = '❌ COB WebSocket failed - All connections down'
|
||||||
|
|
||||||
|
# Set last update time
|
||||||
|
last_updates = [cache.get('last_update', 0) for cache in self.cob_cache.values()]
|
||||||
|
if last_updates and max(last_updates) > 0:
|
||||||
|
status_summary['last_update'] = datetime.fromtimestamp(max(last_updates)).isoformat()
|
||||||
|
|
||||||
|
return status_summary
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Error getting COB WebSocket status: {e}")
|
||||||
|
return {
|
||||||
|
'overall_status': 'error',
|
||||||
|
'warning_message': f'Error getting status: {e}',
|
||||||
|
'symbols': {},
|
||||||
|
'last_update': None
|
||||||
|
}
|
@ -94,6 +94,7 @@ class DashboardLayoutManager:
|
|||||||
("trade-count", "Trades", "text-warning"),
|
("trade-count", "Trades", "text-warning"),
|
||||||
("portfolio-value", "Portfolio", "text-secondary"),
|
("portfolio-value", "Portfolio", "text-secondary"),
|
||||||
("profitability-multiplier", "Profit Boost", "text-primary"),
|
("profitability-multiplier", "Profit Boost", "text-primary"),
|
||||||
|
("cob-websocket-status", "COB WebSocket", "text-warning"),
|
||||||
("mexc-status", f"{exchange_name} API", "text-info")
|
("mexc-status", f"{exchange_name} API", "text-info")
|
||||||
]
|
]
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user