diff --git a/_dev/dev_notes.md b/_dev/dev_notes.md index eed7af4..6d09d21 100644 --- a/_dev/dev_notes.md +++ b/_dev/dev_notes.md @@ -91,3 +91,14 @@ also, adjust our bybit api so we trade with usdt futures - where we can have up +-------------- + + + + +1. on the dash buy/sell buttons do not open/close positions in live mode . +2. we also need to fix our Current Order Book data shown on the dash - it is not consistent ande definitely not fast/low latency. let's store all COB data aggregated to 1S buckets and 0.2s sec ticks. show COB datasource updte rate +3. we don't calculate the COB imbalance correctly - we have MA with 4 time windows. +4. we have some more work on the models statistics and overview but we can focust there later when we fix the other issues + +5. audit and backtest if calculate_williams_pivot_points works correctly. show pivot points on the dash on the 1m candlesticks \ No newline at end of file diff --git a/core/data_provider.py b/core/data_provider.py index 23fed18..73edacb 100644 --- a/core/data_provider.py +++ b/core/data_provider.py @@ -1137,8 +1137,19 @@ class DataProvider: # Extract timestamp and price from tick if isinstance(tick, dict): timestamp = tick.get('timestamp') - price = tick.get('price', tick.get('mid_price', 0)) - volume = tick.get('volume', 1.0) # Default volume if not available + # Prefer explicit price if available, fallback to stats.mid_price + stats = tick.get('stats', {}) if isinstance(tick.get('stats', {}), dict) else {} + price = tick.get('price') + if not price: + price = tick.get('mid_price') or stats.get('mid_price', 0) + # Derive a volume proxy if not provided (use bid+ask volume from stats) + volume = tick.get('volume') + if volume is None: + bid_vol = stats.get('bid_volume', 0) or 0 + ask_vol = stats.get('ask_volume', 0) or 0 + volume = float(bid_vol) + float(ask_vol) + if volume == 0: + volume = 1.0 # Minimal placeholder to avoid zero-volume bars else: continue @@ -2221,14 +2232,40 @@ class DataProvider: # Get latest COB data from cache cob_data = self.get_latest_cob_data(symbol) - if cob_data and 'current_price' in cob_data: + if cob_data: + # Determine current price (prefer explicit field, fallback to stats.mid_price) + stats = cob_data.get('stats', {}) if isinstance(cob_data.get('stats', {}), dict) else {} + current_price = cob_data.get('current_price') or stats.get('mid_price', 0.0) + bucket_size = 1.0 if 'ETH' in symbol else 10.0 + + # Ensure price buckets exist; compute from bids/asks if missing + price_buckets = cob_data.get('price_buckets') or {} + if (not price_buckets) and current_price: + price_buckets = self._compute_price_buckets_from_snapshot( + current_price=current_price, + bucket_size=bucket_size, + bids=cob_data.get('bids', []), + asks=cob_data.get('asks', []) + ) + + # Build imbalance map (price -> imbalance) if not provided + bid_ask_imbalance = cob_data.get('bid_ask_imbalance') or {} + if not bid_ask_imbalance and price_buckets: + tmp = {} + for price, bucket in price_buckets.items(): + bid_vol = float(bucket.get('bid_volume', 0.0) or 0.0) + ask_vol = float(bucket.get('ask_volume', 0.0) or 0.0) + denom = bid_vol + ask_vol + tmp[price] = (bid_vol - ask_vol) / denom if denom > 0 else 0.0 + bid_ask_imbalance = tmp + return COBData( symbol=symbol, timestamp=datetime.now(), - current_price=cob_data['current_price'], - bucket_size=1.0 if 'ETH' in symbol else 10.0, - price_buckets=cob_data.get('price_buckets', {}), - bid_ask_imbalance=cob_data.get('bid_ask_imbalance', {}), + current_price=float(current_price or 0.0), + bucket_size=bucket_size, + price_buckets=price_buckets, + bid_ask_imbalance=bid_ask_imbalance, volume_weighted_prices=cob_data.get('volume_weighted_prices', {}), order_flow_metrics=cob_data.get('order_flow_metrics', {}), ma_1s_imbalance=cob_data.get('ma_1s_imbalance', {}), @@ -2241,6 +2278,67 @@ class DataProvider: logger.error(f"Error getting COB data object for {symbol}: {e}") return None + def _compute_price_buckets_from_snapshot( + self, + current_price: float, + bucket_size: float, + bids: List[List[float]], + asks: List[List[float]] + ) -> Dict[float, Dict[str, float]]: + """Compute ±20 price buckets around current price from raw bids/asks. + + Returns dict: price -> {bid_volume, ask_volume, total_volume, imbalance} + """ + try: + # Initialize bucket map for ±20 buckets + bucket_map: Dict[float, Dict[str, float]] = {} + if not current_price or bucket_size <= 0: + return bucket_map + + # Center-aligned bucket prices + bucket_count = 20 + for i in range(-bucket_count, bucket_count + 1): + price = (round(current_price / bucket_size) * bucket_size) + (i * bucket_size) + bucket_map[price] = { + 'bid_volume': 0.0, + 'ask_volume': 0.0, + 'total_volume': 0.0, + 'imbalance': 0.0, + } + + # Aggregate bids + for level in (bids or [])[:200]: + try: + price, size = float(level[0]), float(level[1]) + except Exception: + continue + bucket_price = round(price / bucket_size) * bucket_size + if bucket_price in bucket_map: + bucket_map[bucket_price]['bid_volume'] += size + + # Aggregate asks + for level in (asks or [])[:200]: + try: + price, size = float(level[0]), float(level[1]) + except Exception: + continue + bucket_price = round(price / bucket_size) * bucket_size + if bucket_price in bucket_map: + bucket_map[bucket_price]['ask_volume'] += size + + # Compute totals and imbalance + for price, bucket in bucket_map.items(): + bid_vol = float(bucket['bid_volume']) + ask_vol = float(bucket['ask_volume']) + total = bid_vol + ask_vol + bucket['total_volume'] = total + bucket['imbalance'] = (bid_vol - ask_vol) / total if total > 0 else 0.0 + + return bucket_map + except Exception as e: + logger.debug(f"Error computing price buckets: {e}") + return {} + def _add_basic_indicators(self, df: pd.DataFrame) -> pd.DataFrame: @@ -4278,13 +4376,46 @@ class DataProvider: if symbol not in self.cob_data_cache: self.cob_data_cache[symbol] = [] - # Convert WebSocket format to standard format + # Convert WebSocket format to standard format and enrich stats if missing + bids_arr = [[bid['price'], bid['size']] for bid in cob_data.get('bids', [])[:50]] + asks_arr = [[ask['price'], ask['size']] for ask in cob_data.get('asks', [])[:50]] + stats_in = cob_data.get('stats', {}) if isinstance(cob_data.get('stats', {}), dict) else {} + + # Derive stats when not provided by source + best_bid = max([b[0] for b in bids_arr], default=0) + best_ask = min([a[0] for a in asks_arr], default=0) + mid = stats_in.get('mid_price') or ((best_bid + best_ask) / 2.0 if best_bid > 0 and best_ask > 0 else 0) + + total_bid_liq = sum([b[0] * b[1] for b in bids_arr]) # price*size USD approx + total_ask_liq = sum([a[0] * a[1] for a in asks_arr]) + spread_bps = 0 + if best_bid > 0 and best_ask > 0 and mid > 0: + spread_bps = ((best_ask - best_bid) / mid) * 10000 + imbalance = 0.0 + denom = (total_bid_liq + total_ask_liq) + if denom > 0: + imbalance = (total_bid_liq - total_ask_liq) / denom + + stats_out = { + 'mid_price': mid, + 'spread_bps': spread_bps, + 'imbalance': imbalance, + 'best_bid': best_bid, + 'best_ask': best_ask, + 'bid_volume': total_bid_liq, + 'ask_volume': total_ask_liq, + 'bid_levels': len(bids_arr), + 'ask_levels': len(asks_arr) + } + # Merge any provided stats atop computed defaults + stats_out.update(stats_in or {}) + standard_cob_data = { 'symbol': symbol, 'timestamp': int(cob_data['timestamp'] * 1000), # Convert to milliseconds - 'bids': [[bid['price'], bid['size']] for bid in cob_data.get('bids', [])[:50]], - 'asks': [[ask['price'], ask['size']] for ask in cob_data.get('asks', [])[:50]], - 'stats': cob_data.get('stats', {}) + 'bids': bids_arr, + 'asks': asks_arr, + 'stats': stats_out } # Add to cache diff --git a/core/multi_exchange_cob_provider.py b/core/multi_exchange_cob_provider.py index f3a7d90..d10fb5b 100644 --- a/core/multi_exchange_cob_provider.py +++ b/core/multi_exchange_cob_provider.py @@ -99,6 +99,7 @@ class ExchangeType(Enum): KRAKEN = "kraken" HUOBI = "huobi" BITFINEX = "bitfinex" + COINAPI = "coinapi" @dataclass class ExchangeOrderBookLevel: diff --git a/core/standardized_data_provider.py b/core/standardized_data_provider.py index 497cbbb..cadbec1 100644 --- a/core/standardized_data_provider.py +++ b/core/standardized_data_provider.py @@ -86,6 +86,15 @@ class StandardizedDataProvider(DataProvider): enabled=True, websocket_url="wss://stream.binance.com:9443/ws/", symbols_mapping={symbol: symbol.replace('/', '').lower() for symbol in self.symbols} + ), + # CoinAPI REST for supplemental depth snapshots (merged with WS streams) + 'coinapi': ExchangeConfig( + exchange_type=ExchangeType.COINAPI, + weight=0.6, + enabled=True, + rest_api_url="https://rest.coinapi.io/v1/", + symbols_mapping={symbol: symbol.replace('/', '_').replace('USDT', 'USD') for symbol in self.symbols}, + rate_limits={"min_interval_ms": 500} ) } @@ -229,69 +238,24 @@ class StandardizedDataProvider(DataProvider): COBData: COB data with price buckets and moving averages """ try: - if not self.cob_provider: + # Use real-time COB snapshot from parent and convert to COBData + cob_obj = self._get_latest_cob_data_object(symbol) + if cob_obj is None: return None - - # Get current price - current_price = self.current_prices.get(symbol.replace('/', '').upper(), 0.0) - if current_price <= 0: - return None - - # Determine bucket size based on symbol - bucket_size = 1.0 if 'ETH' in symbol else 10.0 # $1 for ETH, $10 for BTC - - # Calculate price range (±20 buckets) - price_range = 20 * bucket_size - min_price = current_price - price_range - max_price = current_price + price_range - - # Create price buckets - price_buckets = {} - bid_ask_imbalance = {} - volume_weighted_prices = {} - - # Generate mock COB data for now (will be replaced with real COB provider data) - for i in range(-20, 21): - price = current_price + (i * bucket_size) - if price > 0: - # Mock data - replace with real COB provider data - bid_volume = max(0, 1000 - abs(i) * 50) # More volume near current price - ask_volume = max(0, 1000 - abs(i) * 50) - total_volume = bid_volume + ask_volume - imbalance = (bid_volume - ask_volume) / max(total_volume, 1) - - price_buckets[price] = { - 'bid_volume': bid_volume, - 'ask_volume': ask_volume, - 'total_volume': total_volume, - 'imbalance': imbalance - } - bid_ask_imbalance[price] = imbalance - volume_weighted_prices[price] = price # Simplified VWAP - + # Calculate moving averages of imbalance for ±5 buckets - ma_data = self._calculate_cob_moving_averages(symbol, bid_ask_imbalance, timestamp) - - cob_data = COBData( - symbol=symbol, - timestamp=timestamp, - current_price=current_price, - bucket_size=bucket_size, - price_buckets=price_buckets, - bid_ask_imbalance=bid_ask_imbalance, - volume_weighted_prices=volume_weighted_prices, - order_flow_metrics={}, - ma_1s_imbalance=ma_data.get('1s', {}), - ma_5s_imbalance=ma_data.get('5s', {}), - ma_15s_imbalance=ma_data.get('15s', {}), - ma_60s_imbalance=ma_data.get('60s', {}) - ) - - # Cache the COB data - self.cob_data_cache[symbol] = cob_data - - return cob_data - + ma_data = self._calculate_cob_moving_averages(symbol, cob_obj.bid_ask_imbalance, timestamp) + + # Update MA fields + cob_obj.ma_1s_imbalance = ma_data.get('1s', {}) + cob_obj.ma_5s_imbalance = ma_data.get('5s', {}) + cob_obj.ma_15s_imbalance = ma_data.get('15s', {}) + cob_obj.ma_60s_imbalance = ma_data.get('60s', {}) + + # Cache and return + self.cob_data_cache[symbol] = cob_obj + return cob_obj + except Exception as e: logger.error(f"Error getting COB data for {symbol}: {e}") return None @@ -379,16 +343,40 @@ class StandardizedDataProvider(DataProvider): def _get_pivot_points(self, symbol: str) -> List[PivotPoint]: """Get pivot points for a symbol""" try: - pivot_points = [] - - # Get pivot points from Williams Market Structure if available - if symbol in self.williams_structure: - williams = self.williams_structure[symbol] - # This would need to be implemented based on the actual Williams structure - # For now, return empty list - pass - - return pivot_points + results: List[PivotPoint] = [] + + # Prefer DataProvider's Williams calculation (1s OHLCV based) + try: + levels = self.calculate_williams_pivot_points(symbol) + except Exception: + levels = {} + + # Flatten levels into standardized PivotPoint list + if levels: + for level_idx, trend_level in levels.items(): + # Expect trend_level to have an iterable of pivot points + pivots = getattr(trend_level, 'pivots', None) + if not pivots: + # Some implementations may expose as list directly + pivots = getattr(trend_level, 'points', []) + for p in pivots or []: + # Map fields defensively + ts = getattr(p, 'timestamp', None) + price = float(getattr(p, 'price', 0.0) or 0.0) + ptype = getattr(p, 'pivot_type', getattr(p, 'type', 'low')) + ptype = 'high' if str(ptype).lower() == 'high' else 'low' + lvl = int(getattr(p, 'level', level_idx) or level_idx) + if ts and price > 0: + results.append(PivotPoint( + symbol=symbol, + timestamp=ts, + price=price, + type=ptype, + level=lvl, + confidence=1.0 + )) + + return results except Exception as e: logger.error(f"Error getting pivot points for {symbol}: {e}") diff --git a/main.py b/main.py index bde3748..ff3a43d 100644 --- a/main.py +++ b/main.py @@ -51,12 +51,12 @@ async def run_web_dashboard(): config = get_config() # Initialize core components for streamlined pipeline - from core.data_provider import DataProvider + from core.standardized_data_provider import StandardizedDataProvider from core.orchestrator import TradingOrchestrator from core.trading_executor import TradingExecutor - # Create data provider - data_provider = DataProvider() + # Create standardized data provider (validated BaseDataInput, pivots, COB) + data_provider = StandardizedDataProvider() # Start real-time streaming for BOM caching try: @@ -153,13 +153,13 @@ def start_web_ui(port=8051): # Import and create the Clean Trading Dashboard from web.clean_dashboard import CleanTradingDashboard - from core.data_provider import DataProvider + from core.standardized_data_provider import StandardizedDataProvider from core.orchestrator import TradingOrchestrator from core.trading_executor import TradingExecutor # Initialize components for the dashboard config = get_config() - data_provider = DataProvider() + data_provider = StandardizedDataProvider() # Start real-time streaming for BOM caching (non-blocking) try: diff --git a/web/clean_dashboard.py b/web/clean_dashboard.py index f45b1fc..91ea391 100644 --- a/web/clean_dashboard.py +++ b/web/clean_dashboard.py @@ -1300,7 +1300,7 @@ class CleanTradingDashboard: """Update COB data displays with real order book ladders and cumulative stats""" try: # COB data is critical for trading - keep at 2s interval - + import time eth_snapshot = self._get_cob_snapshot('ETH/USDT') btc_snapshot = self._get_cob_snapshot('BTC/USDT') @@ -1352,8 +1352,47 @@ class CleanTradingDashboard: if isinstance(btc_snapshot, list): btc_snapshot = None - eth_components = self.component_manager.format_cob_data(eth_snapshot, 'ETH/USDT', eth_imbalance_stats, cob_mode) - btc_components = self.component_manager.format_cob_data(btc_snapshot, 'BTC/USDT', btc_imbalance_stats, cob_mode) + # Compute and display COB update rate and include recent aggregated views + def _calc_update_rate(symbol): + if not hasattr(self, 'cob_last_update'): + return "n/a" + last_ts = self.cob_last_update.get(symbol) + if not last_ts: + return "n/a" + age = time.time() - last_ts + if age <= 0: + return "n/a" + hz = 1.0 / age if age > 0 else 0 + return f"{hz:.1f} Hz" + + # Fetch aggregated 1s COB and recent ~0.2s ticks + def _recent_ticks(symbol): + if hasattr(self.data_provider, 'get_cob_raw_ticks'): + ticks = self.data_provider.get_cob_raw_ticks(symbol, count=25) + return ticks[-5:] if ticks else [] + return [] + + eth_rate = _calc_update_rate('ETH/USDT') + btc_rate = _calc_update_rate('BTC/USDT') + eth_agg_1s = self.data_provider.get_cob_1s_aggregated('ETH/USDT') if hasattr(self.data_provider, 'get_cob_1s_aggregated') else [] + btc_agg_1s = self.data_provider.get_cob_1s_aggregated('BTC/USDT') if hasattr(self.data_provider, 'get_cob_1s_aggregated') else [] + eth_recent = _recent_ticks('ETH/USDT') + btc_recent = _recent_ticks('BTC/USDT') + + eth_components = self.component_manager.format_cob_data( + eth_snapshot, + 'ETH/USDT', + eth_imbalance_stats, + cob_mode, + update_info={'update_rate': eth_rate, 'aggregated_1s': eth_agg_1s[-5:], 'recent_ticks': eth_recent} + ) + btc_components = self.component_manager.format_cob_data( + btc_snapshot, + 'BTC/USDT', + btc_imbalance_stats, + cob_mode, + update_info={'update_rate': btc_rate, 'aggregated_1s': btc_agg_1s[-5:], 'recent_ticks': btc_recent} + ) return eth_components, btc_components @@ -3427,23 +3466,28 @@ class CleanTradingDashboard: def _get_cob_mode(self) -> str: """Get current COB data collection mode""" try: - # Check if data provider has WebSocket COB integration - if self.data_provider and hasattr(self.data_provider, 'cob_websocket'): - # Check WebSocket status - if hasattr(self.data_provider.cob_websocket, 'status'): - eth_status = self.data_provider.cob_websocket.status.get('ETH/USDT') - if eth_status and eth_status.connected: - return "WS" # WebSocket mode - - # Check if we have recent WebSocket data - if hasattr(self.data_provider, 'cob_raw_ticks'): - eth_ticks = self.data_provider.cob_raw_ticks.get('ETH/USDT', []) - if eth_ticks: - import time - latest_tick = eth_ticks[-1] - tick_time = latest_tick.get('timestamp', 0) - if isinstance(tick_time, (int, float)) and (time.time() - tick_time) < 10: - return "WS" # Recent WebSocket data + # Determine WS mode using provider's enhanced websocket or raw tick recency + if self.data_provider: + # Preferred: enhanced websocket status summary + if hasattr(self.data_provider, 'get_cob_websocket_status'): + try: + status = self.data_provider.get_cob_websocket_status() + overall = status.get('overall_status') or status.get('status') + if overall in ("active", "ok", "ready"): + return "WS" + except Exception: + pass + # Fallback: raw ticks recency + if hasattr(self.data_provider, 'get_cob_raw_ticks'): + try: + ticks = self.data_provider.get_cob_raw_ticks('ETH/USDT', count=1) + if ticks: + import time + t = ticks[-1].get('timestamp', 0) + if isinstance(t, (int, float)) and (time.time() - t) < 5: + return "WS" + except Exception: + pass # Check if we have any COB data (REST fallback) if hasattr(self, 'latest_cob_data') and 'ETH/USDT' in self.latest_cob_data: @@ -7193,9 +7237,8 @@ class CleanTradingDashboard: if hasattr(self.data_provider, 'get_base_data_input'): return self.data_provider.get_base_data_input(symbol) - # Fallback: create BaseDataInput from available data - from core.data_models import BaseDataInput, OHLCVBar, COBData - import random + # Fallback: create BaseDataInput from available data (no synthetic data) + from core.data_models import BaseDataInput # Get OHLCV data for different timeframes - ensure we have enough data ohlcv_1s = self._get_ohlcv_bars(symbol, '1s', 300) @@ -7206,53 +7249,11 @@ class CleanTradingDashboard: # Get BTC reference data btc_ohlcv_1s = self._get_ohlcv_bars('BTC/USDT', '1s', 300) - # Ensure we have minimum required data (pad if necessary) - def pad_ohlcv_data(bars, target_count=300): - if len(bars) < target_count: - # Pad with realistic variation instead of identical bars - if len(bars) > 0: - last_bar = bars[-1] - # Add small random variation to prevent identical data - for i in range(target_count - len(bars)): - # Create slight variations of the last bar - variation = random.uniform(-0.001, 0.001) # 0.1% variation - new_bar = OHLCVBar( - symbol=last_bar.symbol, - timestamp=last_bar.timestamp + timedelta(seconds=i), - open=last_bar.open * (1 + variation), - high=last_bar.high * (1 + variation), - low=last_bar.low * (1 + variation), - close=last_bar.close * (1 + variation), - volume=last_bar.volume * (1 + random.uniform(-0.1, 0.1)), - timeframe=last_bar.timeframe - ) - bars.append(new_bar) - else: - # Create realistic dummy bars with variation - base_price = 3500.0 - for i in range(target_count): - # Add realistic price movement - price_change = random.uniform(-0.02, 0.02) # 2% max change - current_price = base_price * (1 + price_change) - dummy_bar = OHLCVBar( - symbol=symbol, - timestamp=datetime.now() - timedelta(seconds=target_count-i), - open=current_price * random.uniform(0.998, 1.002), - high=current_price * random.uniform(1.000, 1.005), - low=current_price * random.uniform(0.995, 1.000), - close=current_price, - volume=random.uniform(500.0, 2000.0), - timeframe="1s" - ) - bars.append(dummy_bar) - return bars[:target_count] # Ensure exactly target_count - - # Pad all data to required length - ohlcv_1s = pad_ohlcv_data(ohlcv_1s, 300) - ohlcv_1m = pad_ohlcv_data(ohlcv_1m, 300) - ohlcv_1h = pad_ohlcv_data(ohlcv_1h, 300) - ohlcv_1d = pad_ohlcv_data(ohlcv_1d, 300) - btc_ohlcv_1s = pad_ohlcv_data(btc_ohlcv_1s, 300) + # Strictly require sufficient real data; otherwise return None + datasets = [ohlcv_1s, ohlcv_1m, ohlcv_1h, ohlcv_1d, btc_ohlcv_1s] + if any(len(ds) < 100 for ds in datasets): + logger.warning(f"Insufficient real OHLCV data for {symbol}; skipping BaseDataInput fallback") + return None logger.debug(f"OHLCV data lengths: 1s={len(ohlcv_1s)}, 1m={len(ohlcv_1m)}, 1h={len(ohlcv_1h)}, 1d={len(ohlcv_1d)}, BTC={len(btc_ohlcv_1s)}") diff --git a/web/component_manager.py b/web/component_manager.py index 99f102b..3a8758b 100644 --- a/web/component_manager.py +++ b/web/component_manager.py @@ -296,14 +296,17 @@ class DashboardComponentManager: logger.error(f"Error formatting system status: {e}") return [html.P(f"Error: {str(e)}", className="text-danger small")] - def format_cob_data(self, cob_snapshot, symbol, cumulative_imbalance_stats=None, cob_mode="Unknown"): - """Format COB data into a split view with summary, imbalance stats, and a compact ladder.""" + def format_cob_data(self, cob_snapshot, symbol, cumulative_imbalance_stats=None, cob_mode="Unknown", update_info: dict = None): + """Format COB data into a split view with summary, imbalance stats, and a compact ladder. + update_info can include keys: 'update_rate', 'aggregated_1s', 'recent_ticks'. + """ try: if not cob_snapshot: return html.Div([ html.H6(f"{symbol} COB", className="mb-2"), html.P("No COB data available", className="text-muted small"), - html.P(f"Mode: {cob_mode}", className="text-muted small") + html.P(f"Mode: {cob_mode}", className="text-muted small"), + html.P(f"Update: {(update_info or {}).get('update_rate', 'n/a')}", className="text-muted small") ]) # Defensive: If cob_snapshot is a list, log and return error @@ -312,7 +315,8 @@ class DashboardComponentManager: return html.Div([ html.H6(f"{symbol} COB", className="mb-2"), html.P("Invalid COB data format (list)", className="text-danger small"), - html.P(f"Mode: {cob_mode}", className="text-muted small") + html.P(f"Mode: {cob_mode}", className="text-muted small"), + html.P(f"Update: {(update_info or {}).get('update_rate', 'n/a')}", className="text-muted small") ]) # Debug: Log the type and structure of cob_snapshot @@ -347,7 +351,9 @@ class DashboardComponentManager: if mid_price == 0 or not bids or not asks: return html.Div([ html.H6(f"{symbol} COB", className="mb-2"), - html.P("Awaiting valid order book data...", className="text-muted small") + html.P("Awaiting valid order book data...", className="text-muted small"), + html.P(f"Mode: {cob_mode}", className="text-muted small"), + html.P(f"Update: {(update_info or {}).get('update_rate', 'n/a')}", className="text-muted small") ]) # Create stats dict for compatibility with existing code @@ -362,15 +368,38 @@ class DashboardComponentManager: } # --- Left Panel: Overview and Stats --- + # Prepend update info to overview overview_panel = self._create_cob_overview_panel(symbol, stats, cumulative_imbalance_stats, cob_mode) + if update_info and update_info.get('update_rate'): + # Wrap with a small header line for update rate + overview_panel = html.Div([ + html.Div(html.Small(f"Update: {update_info['update_rate']}", className="text-muted"), className="mb-1"), + overview_panel + ]) # --- Right Panel: Compact Ladder --- ladder_panel = self._create_cob_ladder_panel(bids, asks, mid_price, symbol) + # Append small extras line from aggregated_1s and recent_ticks + extras = [] + if update_info: + agg = (update_info.get('aggregated_1s') or []) + if agg and isinstance(agg[-1], dict): + last = agg[-1] + avg_spread = last.get('spread', {}).get('average_bps', 0) + avg_imb = last.get('imbalance', {}).get('average', 0) + tick_count = last.get('tick_count', 0) + extras.append(html.Small(f"1s agg: {tick_count} ticks, spread {avg_spread:.1f} bps, imb {avg_imb:.2f}", className="text-muted")) + recent = (update_info.get('recent_ticks') or []) + if recent: + extras.append(html.Small(f"Recent ticks: {len(recent)}", className="text-muted ms-2")) + extras_div = html.Div(extras, className="mb-1") if extras else None - return dbc.Row([ - dbc.Col(overview_panel, width=5, className="pe-1"), - dbc.Col(ladder_panel, width=7, className="ps-1") - ], className="g-0") # g-0 removes gutters + children = [dbc.Col(overview_panel, width=5, className="pe-1")] + right_children = [ladder_panel] + if extras_div: + right_children.insert(0, extras_div) + children.append(dbc.Col(html.Div(right_children), width=7, className="ps-1")) + return dbc.Row(children, className="g-0") # g-0 removes gutters except Exception as e: logger.error(f"Error formatting split COB data: {e}")