wip COB data source - not ready

This commit is contained in:
Dobromir Popov
2025-08-08 00:49:13 +03:00
parent 0ce6e2691b
commit bd15bdc87d
7 changed files with 327 additions and 166 deletions

View File

@ -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

View File

@ -1137,8 +1137,19 @@ class DataProvider:
# Extract timestamp and price from tick # Extract timestamp and price from tick
if isinstance(tick, dict): if isinstance(tick, dict):
timestamp = tick.get('timestamp') timestamp = tick.get('timestamp')
price = tick.get('price', tick.get('mid_price', 0)) # Prefer explicit price if available, fallback to stats.mid_price
volume = tick.get('volume', 1.0) # Default volume if not available 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: else:
continue continue
@ -2221,14 +2232,40 @@ class DataProvider:
# Get latest COB data from cache # Get latest COB data from cache
cob_data = self.get_latest_cob_data(symbol) 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( return COBData(
symbol=symbol, symbol=symbol,
timestamp=datetime.now(), timestamp=datetime.now(),
current_price=cob_data['current_price'], current_price=float(current_price or 0.0),
bucket_size=1.0 if 'ETH' in symbol else 10.0, bucket_size=bucket_size,
price_buckets=cob_data.get('price_buckets', {}), price_buckets=price_buckets,
bid_ask_imbalance=cob_data.get('bid_ask_imbalance', {}), bid_ask_imbalance=bid_ask_imbalance,
volume_weighted_prices=cob_data.get('volume_weighted_prices', {}), volume_weighted_prices=cob_data.get('volume_weighted_prices', {}),
order_flow_metrics=cob_data.get('order_flow_metrics', {}), order_flow_metrics=cob_data.get('order_flow_metrics', {}),
ma_1s_imbalance=cob_data.get('ma_1s_imbalance', {}), 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}") logger.error(f"Error getting COB data object for {symbol}: {e}")
return None 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: def _add_basic_indicators(self, df: pd.DataFrame) -> pd.DataFrame:
@ -4278,13 +4376,46 @@ class DataProvider:
if symbol not in self.cob_data_cache: if symbol not in self.cob_data_cache:
self.cob_data_cache[symbol] = [] 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 = { standard_cob_data = {
'symbol': symbol, 'symbol': symbol,
'timestamp': int(cob_data['timestamp'] * 1000), # Convert to milliseconds 'timestamp': int(cob_data['timestamp'] * 1000), # Convert to milliseconds
'bids': [[bid['price'], bid['size']] for bid in cob_data.get('bids', [])[:50]], 'bids': bids_arr,
'asks': [[ask['price'], ask['size']] for ask in cob_data.get('asks', [])[:50]], 'asks': asks_arr,
'stats': cob_data.get('stats', {}) 'stats': stats_out
} }
# Add to cache # Add to cache

View File

@ -99,6 +99,7 @@ class ExchangeType(Enum):
KRAKEN = "kraken" KRAKEN = "kraken"
HUOBI = "huobi" HUOBI = "huobi"
BITFINEX = "bitfinex" BITFINEX = "bitfinex"
COINAPI = "coinapi"
@dataclass @dataclass
class ExchangeOrderBookLevel: class ExchangeOrderBookLevel:

View File

@ -86,6 +86,15 @@ class StandardizedDataProvider(DataProvider):
enabled=True, enabled=True,
websocket_url="wss://stream.binance.com:9443/ws/", websocket_url="wss://stream.binance.com:9443/ws/",
symbols_mapping={symbol: symbol.replace('/', '').lower() for symbol in self.symbols} 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,68 +238,23 @@ class StandardizedDataProvider(DataProvider):
COBData: COB data with price buckets and moving averages COBData: COB data with price buckets and moving averages
""" """
try: 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 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 # Calculate moving averages of imbalance for ±5 buckets
ma_data = self._calculate_cob_moving_averages(symbol, bid_ask_imbalance, timestamp) ma_data = self._calculate_cob_moving_averages(symbol, cob_obj.bid_ask_imbalance, timestamp)
cob_data = COBData( # Update MA fields
symbol=symbol, cob_obj.ma_1s_imbalance = ma_data.get('1s', {})
timestamp=timestamp, cob_obj.ma_5s_imbalance = ma_data.get('5s', {})
current_price=current_price, cob_obj.ma_15s_imbalance = ma_data.get('15s', {})
bucket_size=bucket_size, cob_obj.ma_60s_imbalance = ma_data.get('60s', {})
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 # Cache and return
self.cob_data_cache[symbol] = cob_data self.cob_data_cache[symbol] = cob_obj
return cob_obj
return cob_data
except Exception as e: except Exception as e:
logger.error(f"Error getting COB data for {symbol}: {e}") logger.error(f"Error getting COB data for {symbol}: {e}")
@ -379,16 +343,40 @@ class StandardizedDataProvider(DataProvider):
def _get_pivot_points(self, symbol: str) -> List[PivotPoint]: def _get_pivot_points(self, symbol: str) -> List[PivotPoint]:
"""Get pivot points for a symbol""" """Get pivot points for a symbol"""
try: try:
pivot_points = [] results: List[PivotPoint] = []
# Get pivot points from Williams Market Structure if available # Prefer DataProvider's Williams calculation (1s OHLCV based)
if symbol in self.williams_structure: try:
williams = self.williams_structure[symbol] levels = self.calculate_williams_pivot_points(symbol)
# This would need to be implemented based on the actual Williams structure except Exception:
# For now, return empty list levels = {}
pass
return pivot_points # 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: except Exception as e:
logger.error(f"Error getting pivot points for {symbol}: {e}") logger.error(f"Error getting pivot points for {symbol}: {e}")

10
main.py
View File

@ -51,12 +51,12 @@ async def run_web_dashboard():
config = get_config() config = get_config()
# Initialize core components for streamlined pipeline # 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.orchestrator import TradingOrchestrator
from core.trading_executor import TradingExecutor from core.trading_executor import TradingExecutor
# Create data provider # Create standardized data provider (validated BaseDataInput, pivots, COB)
data_provider = DataProvider() data_provider = StandardizedDataProvider()
# Start real-time streaming for BOM caching # Start real-time streaming for BOM caching
try: try:
@ -153,13 +153,13 @@ def start_web_ui(port=8051):
# Import and create the Clean Trading Dashboard # Import and create the Clean Trading Dashboard
from web.clean_dashboard import CleanTradingDashboard 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.orchestrator import TradingOrchestrator
from core.trading_executor import TradingExecutor from core.trading_executor import TradingExecutor
# Initialize components for the dashboard # Initialize components for the dashboard
config = get_config() config = get_config()
data_provider = DataProvider() data_provider = StandardizedDataProvider()
# Start real-time streaming for BOM caching (non-blocking) # Start real-time streaming for BOM caching (non-blocking)
try: try:

View File

@ -1300,7 +1300,7 @@ class CleanTradingDashboard:
"""Update COB data displays with real order book ladders and cumulative stats""" """Update COB data displays with real order book ladders and cumulative stats"""
try: try:
# COB data is critical for trading - keep at 2s interval # COB data is critical for trading - keep at 2s interval
import time
eth_snapshot = self._get_cob_snapshot('ETH/USDT') eth_snapshot = self._get_cob_snapshot('ETH/USDT')
btc_snapshot = self._get_cob_snapshot('BTC/USDT') btc_snapshot = self._get_cob_snapshot('BTC/USDT')
@ -1352,8 +1352,47 @@ class CleanTradingDashboard:
if isinstance(btc_snapshot, list): if isinstance(btc_snapshot, list):
btc_snapshot = None btc_snapshot = None
eth_components = self.component_manager.format_cob_data(eth_snapshot, 'ETH/USDT', eth_imbalance_stats, cob_mode) # Compute and display COB update rate and include recent aggregated views
btc_components = self.component_manager.format_cob_data(btc_snapshot, 'BTC/USDT', btc_imbalance_stats, cob_mode) 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 return eth_components, btc_components
@ -3427,23 +3466,28 @@ class CleanTradingDashboard:
def _get_cob_mode(self) -> str: def _get_cob_mode(self) -> str:
"""Get current COB data collection mode""" """Get current COB data collection mode"""
try: try:
# Check if data provider has WebSocket COB integration # Determine WS mode using provider's enhanced websocket or raw tick recency
if self.data_provider and hasattr(self.data_provider, 'cob_websocket'): if self.data_provider:
# Check WebSocket status # Preferred: enhanced websocket status summary
if hasattr(self.data_provider.cob_websocket, 'status'): if hasattr(self.data_provider, 'get_cob_websocket_status'):
eth_status = self.data_provider.cob_websocket.status.get('ETH/USDT') try:
if eth_status and eth_status.connected: status = self.data_provider.get_cob_websocket_status()
return "WS" # WebSocket mode overall = status.get('overall_status') or status.get('status')
if overall in ("active", "ok", "ready"):
# Check if we have recent WebSocket data return "WS"
if hasattr(self.data_provider, 'cob_raw_ticks'): except Exception:
eth_ticks = self.data_provider.cob_raw_ticks.get('ETH/USDT', []) pass
if eth_ticks: # Fallback: raw ticks recency
import time if hasattr(self.data_provider, 'get_cob_raw_ticks'):
latest_tick = eth_ticks[-1] try:
tick_time = latest_tick.get('timestamp', 0) ticks = self.data_provider.get_cob_raw_ticks('ETH/USDT', count=1)
if isinstance(tick_time, (int, float)) and (time.time() - tick_time) < 10: if ticks:
return "WS" # Recent WebSocket data 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) # Check if we have any COB data (REST fallback)
if hasattr(self, 'latest_cob_data') and 'ETH/USDT' in self.latest_cob_data: 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'): if hasattr(self.data_provider, 'get_base_data_input'):
return self.data_provider.get_base_data_input(symbol) return self.data_provider.get_base_data_input(symbol)
# Fallback: create BaseDataInput from available data # Fallback: create BaseDataInput from available data (no synthetic data)
from core.data_models import BaseDataInput, OHLCVBar, COBData from core.data_models import BaseDataInput
import random
# Get OHLCV data for different timeframes - ensure we have enough data # Get OHLCV data for different timeframes - ensure we have enough data
ohlcv_1s = self._get_ohlcv_bars(symbol, '1s', 300) ohlcv_1s = self._get_ohlcv_bars(symbol, '1s', 300)
@ -7206,53 +7249,11 @@ class CleanTradingDashboard:
# Get BTC reference data # Get BTC reference data
btc_ohlcv_1s = self._get_ohlcv_bars('BTC/USDT', '1s', 300) btc_ohlcv_1s = self._get_ohlcv_bars('BTC/USDT', '1s', 300)
# Ensure we have minimum required data (pad if necessary) # Strictly require sufficient real data; otherwise return None
def pad_ohlcv_data(bars, target_count=300): datasets = [ohlcv_1s, ohlcv_1m, ohlcv_1h, ohlcv_1d, btc_ohlcv_1s]
if len(bars) < target_count: if any(len(ds) < 100 for ds in datasets):
# Pad with realistic variation instead of identical bars logger.warning(f"Insufficient real OHLCV data for {symbol}; skipping BaseDataInput fallback")
if len(bars) > 0: return None
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)
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)}") 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)}")

View File

@ -296,14 +296,17 @@ class DashboardComponentManager:
logger.error(f"Error formatting system status: {e}") logger.error(f"Error formatting system status: {e}")
return [html.P(f"Error: {str(e)}", className="text-danger small")] 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"): 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.""" """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: try:
if not cob_snapshot: if not cob_snapshot:
return html.Div([ return html.Div([
html.H6(f"{symbol} COB", className="mb-2"), html.H6(f"{symbol} COB", className="mb-2"),
html.P("No COB data available", className="text-muted small"), 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 # Defensive: If cob_snapshot is a list, log and return error
@ -312,7 +315,8 @@ class DashboardComponentManager:
return html.Div([ return html.Div([
html.H6(f"{symbol} COB", className="mb-2"), html.H6(f"{symbol} COB", className="mb-2"),
html.P("Invalid COB data format (list)", className="text-danger small"), 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 # 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: if mid_price == 0 or not bids or not asks:
return html.Div([ return html.Div([
html.H6(f"{symbol} COB", className="mb-2"), 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 # Create stats dict for compatibility with existing code
@ -362,15 +368,38 @@ class DashboardComponentManager:
} }
# --- Left Panel: Overview and Stats --- # --- Left Panel: Overview and Stats ---
# Prepend update info to overview
overview_panel = self._create_cob_overview_panel(symbol, stats, cumulative_imbalance_stats, cob_mode) 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 --- # --- Right Panel: Compact Ladder ---
ladder_panel = self._create_cob_ladder_panel(bids, asks, mid_price, symbol) 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([ children = [dbc.Col(overview_panel, width=5, className="pe-1")]
dbc.Col(overview_panel, width=5, className="pe-1"), right_children = [ladder_panel]
dbc.Col(ladder_panel, width=7, className="ps-1") if extras_div:
], className="g-0") # g-0 removes gutters 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: except Exception as e:
logger.error(f"Error formatting split COB data: {e}") logger.error(f"Error formatting split COB data: {e}")