This commit is contained in:
Dobromir Popov
2025-07-28 11:12:42 +03:00
parent 1084b7f5b5
commit 7c508ab536
3 changed files with 251 additions and 108 deletions

View File

@ -163,7 +163,7 @@ class COBIntegration:
if symbol: if symbol:
self.websocket_status[symbol] = status self.websocket_status[symbol] = status
logger.info(f"🔌 WebSocket status for {symbol}: {status} - {message}") logger.info(f"WebSocket status for {symbol}: {status} - {message}")
# Notify dashboard callbacks about status change # Notify dashboard callbacks about status change
status_update = { status_update = {

View File

@ -154,11 +154,11 @@ class EnhancedCOBWebSocket:
# Timezone configuration # Timezone configuration
if self.timezone_offset == '+08:00': if self.timezone_offset == '+08:00':
logger.info("🕐 Configured for UTC+8 timezone (Asian markets)") logger.info("Configured for UTC+8 timezone (Asian markets)")
elif self.timezone_offset: elif self.timezone_offset:
logger.info(f"🕐 Configured for {self.timezone_offset} timezone") logger.info(f"Configured for {self.timezone_offset} timezone")
else: else:
logger.info("🕐 Configured for UTC timezone (default)") logger.info("Configured for UTC timezone (default)")
logger.info(f"Enhanced COB WebSocket initialized for symbols: {self.symbols}") logger.info(f"Enhanced COB WebSocket initialized for symbols: {self.symbols}")
if not WEBSOCKETS_AVAILABLE: if not WEBSOCKETS_AVAILABLE:
@ -222,79 +222,39 @@ class EnhancedCOBWebSocket:
logger.info("Enhanced COB WebSocket system stopped") logger.info("Enhanced COB WebSocket system stopped")
async def _init_rest_session(self): async def _init_rest_session(self):
"""Initialize REST API session for fallback and snapshots - Windows compatible""" """Initialize REST API session - Windows uses requests-only mode"""
import platform import platform
# On Windows, completely skip aiohttp to avoid aiodns issues
is_windows = platform.system().lower() == 'windows'
if is_windows:
logger.info("Windows detected - using WebSocket-only mode (skipping REST API due to rate limits)")
self.rest_session = None # Force requests fallback
return
# Non-Windows: try aiohttp
try: try:
# Detect Windows and use specific configuration connector = aiohttp.TCPConnector(
is_windows = platform.system().lower() == 'windows' limit=100,
limit_per_host=20,
enable_cleanup_closed=True,
use_dns_cache=True
)
timeout = aiohttp.ClientTimeout(total=10, connect=5)
self.rest_session = aiohttp.ClientSession(
timeout=timeout,
connector=connector,
headers={'User-Agent': 'Enhanced-COB-WebSocket/1.0'}
)
logger.info("REST API session initialized (Unix/Linux)")
if is_windows:
# Windows-specific configuration to avoid aiodns issues
import asyncio
# Check if we're using ProactorEventLoop (Windows default)
loop = asyncio.get_event_loop()
loop_type = type(loop).__name__
logger.debug(f"Event loop type: {loop_type}")
# Use basic connector without DNS caching for Windows
connector = aiohttp.TCPConnector(
limit=50,
limit_per_host=10,
enable_cleanup_closed=True,
use_dns_cache=False, # Critical: disable DNS cache
resolver=None, # Use default resolver, not aiodns
family=0, # Use default address family
ssl=False # Disable SSL verification for simplicity
)
timeout = aiohttp.ClientTimeout(total=15, connect=10)
self.rest_session = aiohttp.ClientSession(
timeout=timeout,
connector=connector,
headers={
'User-Agent': 'Enhanced-COB-WebSocket/1.0',
'Accept': 'application/json'
}
)
logger.info("✅ REST API session initialized (Windows ProactorEventLoop compatible)")
else:
# Unix/Linux configuration (can use more advanced features)
connector = aiohttp.TCPConnector(
limit=100,
limit_per_host=20,
enable_cleanup_closed=True,
use_dns_cache=True
)
timeout = aiohttp.ClientTimeout(total=10, connect=5)
self.rest_session = aiohttp.ClientSession(
timeout=timeout,
connector=connector,
headers={'User-Agent': 'Enhanced-COB-WebSocket/1.0'}
)
logger.info("✅ REST API session initialized (Unix/Linux)")
except Exception as e: except Exception as e:
logger.warning(f"⚠️ Failed to initialize REST session: {e}") logger.warning(f"Failed to initialize aiohttp session: {e}")
logger.info("Falling back to requests-only mode")
# Fallback: Ultra-minimal configuration self.rest_session = None
try:
# Most basic configuration possible
self.rest_session = aiohttp.ClientSession(
timeout=aiohttp.ClientTimeout(total=20),
headers={'User-Agent': 'COB-WebSocket/1.0'}
)
logger.info("✅ REST API session initialized with ultra-minimal config")
except Exception as e2:
logger.error(f"❌ Failed to initialize any REST session: {e2}")
logger.info("🔄 Continuing without REST session - WebSocket-only mode")
self.rest_session = None
async def _get_order_book_snapshot(self, symbol: str): async def _get_order_book_snapshot(self, symbol: str):
"""Get initial order book snapshot from REST API """Get initial order book snapshot from REST API
@ -308,7 +268,7 @@ class EnhancedCOBWebSocket:
await self._init_rest_session() await self._init_rest_session()
if not self.rest_session: if not self.rest_session:
logger.warning(f"⚠️ Cannot get order book snapshot for {symbol} - REST session not available, will use WebSocket data only") logger.warning(f"Cannot get order book snapshot for {symbol} - REST session not available, will use WebSocket data only")
return return
# Convert symbol format for Binance API # Convert symbol format for Binance API
@ -317,7 +277,7 @@ class EnhancedCOBWebSocket:
# Get order book snapshot with maximum depth # Get order book snapshot with maximum depth
url = f"https://api.binance.com/api/v3/depth?symbol={binance_symbol}&limit=1000" url = f"https://api.binance.com/api/v3/depth?symbol={binance_symbol}&limit=1000"
logger.debug(f"🔍 Getting order book snapshot for {symbol} from {url}") logger.debug(f"Getting order book snapshot for {symbol} from {url}")
async with self.rest_session.get(url) as response: async with self.rest_session.get(url) as response:
if response.status == 200: if response.status == 200:
@ -325,7 +285,7 @@ class EnhancedCOBWebSocket:
# Validate response structure # Validate response structure
if not isinstance(data, dict) or 'bids' not in data or 'asks' not in data: if not isinstance(data, dict) or 'bids' not in data or 'asks' not in data:
logger.error(f"Invalid order book snapshot response for {symbol}: missing bids/asks") logger.error(f"Invalid order book snapshot response for {symbol}: missing bids/asks")
return return
# Initialize order book state for proper WebSocket synchronization # Initialize order book state for proper WebSocket synchronization
@ -344,7 +304,7 @@ class EnhancedCOBWebSocket:
# Mark as initialized # Mark as initialized
self.order_book_initialized[symbol] = True self.order_book_initialized[symbol] = True
logger.info(f"Got order book snapshot for {symbol}: {len(data['bids'])} bids, {len(data['asks'])} asks") logger.info(f"Got order book snapshot for {symbol}: {len(data['bids'])} bids, {len(data['asks'])} asks")
# Create initial COB data from snapshot # Create initial COB data from snapshot
bids = [{'price': float(price), 'size': float(qty)} for price, qty in data['bids'] if float(qty) > 0] bids = [{'price': float(price), 'size': float(qty)} for price, qty in data['bids'] if float(qty) > 0]
@ -401,21 +361,21 @@ class EnhancedCOBWebSocket:
except Exception as e: except Exception as e:
logger.error(f"❌ Error in COB callback: {e}") logger.error(f"❌ Error in COB callback: {e}")
logger.debug(f"📊 Initial snapshot for {symbol}: ${mid_price:.2f}, spread: {spread_bps:.1f} bps") logger.debug(f"Initial snapshot for {symbol}: ${mid_price:.2f}, spread: {spread_bps:.1f} bps")
else: else:
logger.warning(f"⚠️ No valid bid/ask data in snapshot for {symbol}") logger.warning(f"No valid bid/ask data in snapshot for {symbol}")
elif response.status == 429: elif response.status == 429:
logger.warning(f"⚠️ Rate limited getting snapshot for {symbol}, will continue with WebSocket only") logger.warning(f"Rate limited getting snapshot for {symbol}, will continue with WebSocket only")
else: else:
logger.error(f"Failed to get order book snapshot for {symbol}: HTTP {response.status}") logger.error(f"Failed to get order book snapshot for {symbol}: HTTP {response.status}")
response_text = await response.text() response_text = await response.text()
logger.debug(f"Response: {response_text}") logger.debug(f"Response: {response_text}")
except asyncio.TimeoutError: except asyncio.TimeoutError:
logger.warning(f"⚠️ Timeout getting order book snapshot for {symbol}, will continue with WebSocket only") logger.warning(f"Timeout getting order book snapshot for {symbol}, will continue with WebSocket only")
except Exception as e: except Exception as e:
logger.warning(f"⚠️ Error getting order book snapshot for {symbol}: {e}, will continue with WebSocket only") logger.warning(f"Error getting order book snapshot for {symbol}: {e}, will continue with WebSocket only")
logger.debug(f"Snapshot error details: {e}") logger.debug(f"Snapshot error details: {e}")
# Don't fail the entire connection due to snapshot issues # Don't fail the entire connection due to snapshot issues
@ -796,7 +756,24 @@ class EnhancedCOBWebSocket:
return return
# Order book is initialized, apply update directly # Order book is initialized, apply update directly
await self._apply_depth_update(symbol, data) # In WebSocket-only mode (no REST snapshot), be more lenient with update IDs
last_update_id = self.last_update_ids.get(symbol, 0)
if last_update_id == 0:
# WebSocket-only mode: accept any update as starting point
logger.debug(f"WebSocket-only mode for {symbol}: accepting update as starting point (U={first_update_id}, u={final_update_id})")
await self._apply_depth_update(symbol, data)
elif final_update_id <= last_update_id:
# Event is older than our current state, ignore
logger.debug(f"Ignoring old update for {symbol}: {final_update_id} <= {last_update_id}")
return
elif first_update_id > last_update_id + 1:
# Gap detected - in WebSocket-only mode, just continue (less strict)
logger.warning(f"Gap detected for {symbol}: {first_update_id} > {last_update_id + 1}, continuing anyway (WebSocket-only mode)")
await self._apply_depth_update(symbol, data)
else:
# Normal update
await self._apply_depth_update(symbol, data)
except Exception as e: except Exception as e:
logger.error(f"Error handling depth update for {symbol}: {e}") logger.error(f"Error handling depth update for {symbol}: {e}")
@ -804,6 +781,13 @@ class EnhancedCOBWebSocket:
async def _initialize_order_book_with_buffering(self, symbol: str): async def _initialize_order_book_with_buffering(self, symbol: str):
"""Initialize order book following Binance recommendations with event buffering""" """Initialize order book following Binance recommendations with event buffering"""
try: try:
# On Windows, skip REST API and go directly to simplified mode to avoid rate limits
import platform
if platform.system().lower() == 'windows':
logger.info(f"Windows detected - using WebSocket-only mode for {symbol} (avoiding REST API rate limits)")
await self._init_simplified_mode(symbol)
return
max_attempts = 3 max_attempts = 3
attempt = 0 attempt = 0
@ -839,38 +823,185 @@ class EnhancedCOBWebSocket:
self.order_book_initialized[symbol] = True self.order_book_initialized[symbol] = True
self.snapshot_in_progress[symbol] = False self.snapshot_in_progress[symbol] = False
logger.info(f"Order book initialized for {symbol} with {len(self.event_buffers[symbol])} buffered events") logger.info(f"Order book initialized for {symbol} with {len(self.event_buffers[symbol])} buffered events")
return return
logger.error(f"Failed to initialize order book for {symbol} after {max_attempts} attempts") logger.error(f"Failed to initialize order book for {symbol} after {max_attempts} attempts")
self.snapshot_in_progress[symbol] = False logger.info(f"Switching to simplified mode for {symbol} (no order book sync)")
# Use simplified mode that doesn't maintain order book
await self._init_simplified_mode(symbol)
except Exception as e: except Exception as e:
logger.error(f"Error initializing order book with buffering for {symbol}: {e}") logger.error(f"Error initializing order book with buffering for {symbol}: {e}")
self.snapshot_in_progress[symbol] = False self.snapshot_in_progress[symbol] = False
async def _get_order_book_snapshot_data(self, symbol: str) -> Optional[Dict]: async def _init_simplified_mode(self, symbol: str):
"""Get order book snapshot data from REST API""" """Initialize simplified mode that processes WebSocket data without order book sync"""
try: try:
# Mark as using simplified mode
if not hasattr(self, 'simplified_mode'):
self.simplified_mode = {}
self.simplified_mode[symbol] = True
# Clear any existing state
if symbol in self.event_buffers:
self.event_buffers[symbol] = []
if symbol in self.order_books:
del self.order_books[symbol]
# Mark as initialized
self.order_book_initialized[symbol] = True
self.snapshot_in_progress[symbol] = False
logger.info(f"Simplified mode initialized for {symbol} - will process raw WebSocket data")
except Exception as e:
logger.error(f"Error initializing simplified mode for {symbol}: {e}")
async def _process_simplified_depth_data(self, symbol: str, data: Dict):
"""Process depth data in simplified mode without order book synchronization"""
try:
# Extract bids and asks directly from the update
bids_data = data.get('b', [])
asks_data = data.get('a', [])
if not bids_data and not asks_data:
return
# Convert to simple format
bids = []
asks = []
for bid in bids_data:
if len(bid) >= 2:
price = float(bid[0])
size = float(bid[1])
if size > 0: # Only include non-zero quantities
bids.append({'price': price, 'size': size})
for ask in asks_data:
if len(ask) >= 2:
price = float(ask[0])
size = float(ask[1])
if size > 0: # Only include non-zero quantities
asks.append({'price': price, 'size': size})
if not bids and not asks:
return
# Sort for best bid/ask calculation
if bids:
bids.sort(key=lambda x: x['price'], reverse=True)
if asks:
asks.sort(key=lambda x: x['price'])
# Create simplified COB data
cob_data = {
'symbol': symbol,
'timestamp': datetime.now(),
'bids': bids,
'asks': asks,
'source': 'simplified_depth_stream',
'exchange': 'binance'
}
# Calculate basic stats if we have data
if bids and asks:
best_bid = bids[0]['price']
best_ask = asks[0]['price']
mid_price = (best_bid + best_ask) / 2
spread = best_ask - best_bid
spread_bps = (spread / mid_price) * 10000 if mid_price > 0 else 0
cob_data['stats'] = {
'best_bid': best_bid,
'best_ask': best_ask,
'mid_price': mid_price,
'spread': spread,
'spread_bps': spread_bps,
'bid_levels': len(bids),
'ask_levels': len(asks),
'timestamp': datetime.now().isoformat(),
'mode': 'simplified'
}
# 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"{symbol}: Simplified depth - {len(bids)} bids, {len(asks)} asks")
except Exception as e:
logger.error(f"Error processing simplified depth data for {symbol}: {e}")
async def _get_order_book_snapshot_data(self, symbol: str) -> Optional[Dict]:
"""Get order book snapshot data from REST API with Windows fallback"""
try:
# Try aiohttp first
if not self.rest_session: if not self.rest_session:
await self._init_rest_session() await self._init_rest_session()
if not self.rest_session: if self.rest_session:
# Convert symbol format for Binance API
binance_symbol = symbol.replace('/', '')
url = f"https://api.binance.com/api/v3/depth?symbol={binance_symbol}&limit=5000"
async with self.rest_session.get(url) as response:
if response.status == 200:
return await response.json()
else:
logger.error(f"Failed to get snapshot for {symbol}: HTTP {response.status}")
return None
else:
# Fallback to synchronous requests (Windows compatible)
logger.info(f"Using synchronous HTTP fallback for {symbol} snapshot")
return await self._get_snapshot_with_requests(symbol)
except Exception as e:
logger.error(f"Error getting snapshot data for {symbol}: {e}")
# Try synchronous fallback
try:
return await self._get_snapshot_with_requests(symbol)
except Exception as e2:
logger.error(f"Fallback also failed for {symbol}: {e2}")
return None return None
async def _get_snapshot_with_requests(self, symbol: str) -> Optional[Dict]:
"""Fallback method using synchronous requests library (Windows compatible)"""
try:
import requests
import asyncio
# Convert symbol format for Binance API # Convert symbol format for Binance API
binance_symbol = symbol.replace('/', '') binance_symbol = symbol.replace('/', '')
url = f"https://api.binance.com/api/v3/depth?symbol={binance_symbol}&limit=5000" url = f"https://api.binance.com/api/v3/depth?symbol={binance_symbol}&limit=5000"
async with self.rest_session.get(url) as response: # Run in thread pool to avoid blocking
if response.status == 200: loop = asyncio.get_event_loop()
return await response.json() response = await loop.run_in_executor(
else: None,
logger.error(f"Failed to get snapshot for {symbol}: HTTP {response.status}") lambda: requests.get(url, timeout=10, headers={'User-Agent': 'COB-WebSocket/1.0'})
return None )
if response.status_code == 200:
logger.info(f"Got snapshot for {symbol} using requests fallback")
return response.json()
else:
logger.error(f"Requests fallback failed for {symbol}: HTTP {response.status_code}")
return None
except ImportError:
logger.error("requests library not available for fallback")
return None
except Exception as e: except Exception as e:
logger.error(f"Error getting snapshot data for {symbol}: {e}") logger.error(f"Error in requests fallback for {symbol}: {e}")
return None return None
async def _setup_order_book_from_snapshot(self, symbol: str, snapshot_data: Dict): async def _setup_order_book_from_snapshot(self, symbol: str, snapshot_data: Dict):
@ -917,13 +1048,13 @@ class EnhancedCOBWebSocket:
first_final_u = first_event.get('u', 0) first_final_u = first_event.get('u', 0)
if not (first_u <= snapshot_last_update_id <= first_final_u): if not (first_u <= snapshot_last_update_id <= first_final_u):
logger.error(f"Synchronization error for {symbol}: snapshot lastUpdateId {snapshot_last_update_id} not in range [{first_u}, {first_final_u}]") logger.error(f"Synchronization error for {symbol}: snapshot lastUpdateId {snapshot_last_update_id} not in range [{first_u}, {first_final_u}]")
# Reset and try again # Reset and try again
self.order_book_initialized[symbol] = False self.order_book_initialized[symbol] = False
self.event_buffers[symbol] = [] self.event_buffers[symbol] = []
return return
logger.debug(f"First buffered event valid for {symbol}: snapshot lastUpdateId {snapshot_last_update_id} in range [{first_u}, {first_final_u}]") logger.debug(f"First buffered event valid for {symbol}: snapshot lastUpdateId {snapshot_last_update_id} in range [{first_u}, {first_final_u}]")
# Step 6-7: Apply all valid buffered events # Step 6-7: Apply all valid buffered events
for event in valid_events: for event in valid_events:
@ -1126,7 +1257,13 @@ class EnhancedCOBWebSocket:
async def _handle_depth_stream(self, symbol: str, data: Dict): async def _handle_depth_stream(self, symbol: str, data: Dict):
"""Handle depth stream data (order book updates)""" """Handle depth stream data (order book updates)"""
try: try:
# Check if this is a depth update event # Check if we're in simplified mode
if hasattr(self, 'simplified_mode') and self.simplified_mode.get(symbol, False):
# Simplified mode: process raw data without order book sync
await self._process_simplified_depth_data(symbol, data)
return
# Normal mode: try to maintain order book
if data.get('e') == 'depthUpdate': if data.get('e') == 'depthUpdate':
await self._handle_depth_update(symbol, data) await self._handle_depth_update(symbol, data)
else: else:
@ -1134,6 +1271,12 @@ class EnhancedCOBWebSocket:
await self._process_websocket_message(symbol, data) await self._process_websocket_message(symbol, data)
except Exception as e: except Exception as e:
logger.error(f"Error handling depth stream for {symbol}: {e}") logger.error(f"Error handling depth stream for {symbol}: {e}")
# Fall back to simplified mode on repeated errors
if not hasattr(self, 'simplified_mode'):
self.simplified_mode = {}
if not self.simplified_mode.get(symbol, False):
logger.info(f"Switching {symbol} to simplified mode due to errors")
await self._init_simplified_mode(symbol)
async def _handle_kline_stream(self, symbol: str, data: Dict): async def _handle_kline_stream(self, symbol: str, data: Dict):
"""Handle 1-second kline/candlestick data with timezone support""" """Handle 1-second kline/candlestick data with timezone support"""
@ -1397,9 +1540,9 @@ class EnhancedCOBWebSocket:
try: try:
await callback(symbol, cob_data) await callback(symbol, cob_data)
except Exception as e: except Exception as e:
logger.error(f"Error in COB callback: {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") logger.debug(f"Fetched REST COB data for {symbol}: {len(cob_data['bids'])} bids, {len(cob_data['asks'])} asks")
else: else:
logger.warning(f"REST API error for {symbol}: HTTP {response.status}") logger.warning(f"REST API error for {symbol}: HTTP {response.status}")
@ -1427,7 +1570,7 @@ class EnhancedCOBWebSocket:
# Log connection status and order book sync status # Log connection status and order book sync status
if status.connected: if status.connected:
ob_status = "Synced" if self.order_book_initialized.get(symbol, False) else "🔄 Syncing" ob_status = "Synced" if self.order_book_initialized.get(symbol, False) else "Syncing"
buffered_count = len(self.event_buffers.get(symbol, [])) buffered_count = len(self.event_buffers.get(symbol, []))
logger.debug(f"{symbol}: Connected, {status.messages_received} messages, OB: {ob_status}, Buffered: {buffered_count}") logger.debug(f"{symbol}: Connected, {status.messages_received} messages, OB: {ob_status}, Buffered: {buffered_count}")
else: else:

View File

@ -369,7 +369,7 @@ class CleanTradingDashboard:
else: else:
self.cob_cache[symbol]['update_rate'] = 0.0 self.cob_cache[symbol]['update_rate'] = 0.0
logger.info(f"📊 Updated COB cache for {symbol} from data provider (updates: {self.cob_cache[symbol]['updates_count']})") logger.debug(f"Updated COB cache for {symbol} from data provider (updates: {self.cob_cache[symbol]['updates_count']})")
# Continue with existing logic # Continue with existing logic
# Update latest COB data cache # Update latest COB data cache
@ -6634,10 +6634,10 @@ class CleanTradingDashboard:
self.cob_cache[symbol]['websocket_status'] = 'connected' self.cob_cache[symbol]['websocket_status'] = 'connected'
self.cob_cache[symbol]['source'] = 'orchestrator' self.cob_cache[symbol]['source'] = 'orchestrator'
logger.info(f"📊 Updated COB cache for {symbol} from orchestrator: {self.cob_cache[symbol]['websocket_status']} (updates: {self.cob_cache[symbol]['updates_count']})") logger.debug(f"Updated COB cache for {symbol} from orchestrator: {self.cob_cache[symbol]['websocket_status']} (updates: {self.cob_cache[symbol]['updates_count']})")
except Exception as e: except Exception as e:
logger.error(f"Error updating COB cache from orchestrator for {symbol}: {e}") logger.error(f"Error updating COB cache from orchestrator for {symbol}: {e}")
def _on_enhanced_cob_update(self, symbol: str, data: Dict): def _on_enhanced_cob_update(self, symbol: str, data: Dict):
"""Handle enhanced COB updates with WebSocket status""" """Handle enhanced COB updates with WebSocket status"""