wip COB data source - not ready
This commit is contained in:
@ -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
|
@ -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
|
||||
|
@ -99,6 +99,7 @@ class ExchangeType(Enum):
|
||||
KRAKEN = "kraken"
|
||||
HUOBI = "huobi"
|
||||
BITFINEX = "bitfinex"
|
||||
COINAPI = "coinapi"
|
||||
|
||||
@dataclass
|
||||
class ExchangeOrderBookLevel:
|
||||
|
@ -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}")
|
||||
|
10
main.py
10
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:
|
||||
|
@ -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)}")
|
||||
|
||||
|
@ -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}")
|
||||
|
Reference in New Issue
Block a user