dark mode. new COB style
This commit is contained in:
@ -46,12 +46,17 @@ import aiohttp.resolver
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# goal: use top 10 exchanges
|
||||||
|
# https://www.coingecko.com/en/exchanges
|
||||||
|
|
||||||
class ExchangeType(Enum):
|
class ExchangeType(Enum):
|
||||||
BINANCE = "binance"
|
BINANCE = "binance"
|
||||||
COINBASE = "coinbase"
|
COINBASE = "coinbase"
|
||||||
KRAKEN = "kraken"
|
KRAKEN = "kraken"
|
||||||
HUOBI = "huobi"
|
HUOBI = "huobi"
|
||||||
BITFINEX = "bitfinex"
|
BITFINEX = "bitfinex"
|
||||||
|
BYBIT = "bybit"
|
||||||
|
BITGET = "bitget"
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ExchangeOrderBookLevel:
|
class ExchangeOrderBookLevel:
|
||||||
@ -288,6 +293,24 @@ class MultiExchangeCOBProvider:
|
|||||||
rate_limits={'requests_per_minute': 1000}
|
rate_limits={'requests_per_minute': 1000}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Bybit configuration
|
||||||
|
configs[ExchangeType.BYBIT.value] = ExchangeConfig(
|
||||||
|
exchange_type=ExchangeType.BYBIT,
|
||||||
|
weight=0.18,
|
||||||
|
websocket_url="wss://stream.bybit.com/v5/public/spot",
|
||||||
|
rest_api_url="https://api.bybit.com",
|
||||||
|
symbols_mapping={'BTC/USDT': 'BTCUSDT', 'ETH/USDT': 'ETHUSDT'},
|
||||||
|
rate_limits={'requests_per_minute': 1200}
|
||||||
|
)
|
||||||
|
# Bitget configuration
|
||||||
|
configs[ExchangeType.BITGET.value] = ExchangeConfig(
|
||||||
|
exchange_type=ExchangeType.BITGET,
|
||||||
|
weight=0.12,
|
||||||
|
websocket_url="wss://ws.bitget.com/spot/v1/stream",
|
||||||
|
rest_api_url="https://api.bitget.com",
|
||||||
|
symbols_mapping={'BTC/USDT': 'BTCUSDT_SPBL', 'ETH/USDT': 'ETHUSDT_SPBL'},
|
||||||
|
rate_limits={'requests_per_minute': 1200}
|
||||||
|
)
|
||||||
return configs
|
return configs
|
||||||
|
|
||||||
async def start_streaming(self):
|
async def start_streaming(self):
|
||||||
@ -459,6 +482,10 @@ class MultiExchangeCOBProvider:
|
|||||||
await self._stream_huobi_orderbook(symbol, config)
|
await self._stream_huobi_orderbook(symbol, config)
|
||||||
elif exchange_name == ExchangeType.BITFINEX.value:
|
elif exchange_name == ExchangeType.BITFINEX.value:
|
||||||
await self._stream_bitfinex_orderbook(symbol, config)
|
await self._stream_bitfinex_orderbook(symbol, config)
|
||||||
|
elif exchange_name == ExchangeType.BYBIT.value:
|
||||||
|
await self._stream_bybit_orderbook(symbol, config)
|
||||||
|
elif exchange_name == ExchangeType.BITGET.value:
|
||||||
|
await self._stream_bitget_orderbook(symbol, config)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error streaming {exchange_name} for {symbol}: {e}")
|
logger.error(f"Error streaming {exchange_name} for {symbol}: {e}")
|
||||||
@ -467,7 +494,9 @@ class MultiExchangeCOBProvider:
|
|||||||
async def _stream_binance_orderbook(self, symbol: str, config: ExchangeConfig):
|
async def _stream_binance_orderbook(self, symbol: str, config: ExchangeConfig):
|
||||||
"""Stream order book data from Binance"""
|
"""Stream order book data from Binance"""
|
||||||
try:
|
try:
|
||||||
ws_url = f"{config.websocket_url}{config.symbols_mapping[symbol].lower()}@depth@1000ms"
|
# Use partial book depth stream with maximum levels - Binance format
|
||||||
|
# @depth20@100ms gives us 20 levels at 100ms, but we also have REST API for full depth
|
||||||
|
ws_url = f"{config.websocket_url}{config.symbols_mapping[symbol].lower()}@depth20@100ms"
|
||||||
logger.info(f"Connecting to Binance WebSocket: {ws_url}")
|
logger.info(f"Connecting to Binance WebSocket: {ws_url}")
|
||||||
|
|
||||||
if websockets is None or websockets_connect is None:
|
if websockets is None or websockets_connect is None:
|
||||||
|
@ -174,12 +174,64 @@ class CleanTradingDashboard:
|
|||||||
timezone_name = self.config.get('system', {}).get('timezone', 'Europe/Sofia')
|
timezone_name = self.config.get('system', {}).get('timezone', 'Europe/Sofia')
|
||||||
self.timezone = pytz.timezone(timezone_name)
|
self.timezone = pytz.timezone(timezone_name)
|
||||||
|
|
||||||
# Create Dash app
|
# Create Dash app with dark theme
|
||||||
self.app = Dash(__name__, external_stylesheets=[
|
self.app = Dash(__name__, external_stylesheets=[
|
||||||
'https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css',
|
'https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css',
|
||||||
'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css'
|
'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css'
|
||||||
])
|
])
|
||||||
|
|
||||||
|
# Add custom dark theme CSS
|
||||||
|
self.app.index_string = '''
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
{%metas%}
|
||||||
|
<title>{%title%}</title>
|
||||||
|
{%favicon%}
|
||||||
|
{%css%}
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: #111827 !important;
|
||||||
|
color: #f8f9fa !important;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background-color: #1f2937 !important;
|
||||||
|
border: 1px solid #374151 !important;
|
||||||
|
color: #f8f9fa !important;
|
||||||
|
}
|
||||||
|
.card-header {
|
||||||
|
background-color: #374151 !important;
|
||||||
|
border-bottom: 1px solid #4b5563 !important;
|
||||||
|
color: #f8f9fa !important;
|
||||||
|
}
|
||||||
|
.table {
|
||||||
|
color: #f8f9fa !important;
|
||||||
|
}
|
||||||
|
.table-dark {
|
||||||
|
background-color: #1f2937 !important;
|
||||||
|
}
|
||||||
|
.bg-light {
|
||||||
|
background-color: #374151 !important;
|
||||||
|
}
|
||||||
|
.text-muted {
|
||||||
|
color: #9ca3af !important;
|
||||||
|
}
|
||||||
|
.border {
|
||||||
|
border-color: #4b5563 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{%app_entry%}
|
||||||
|
<footer>
|
||||||
|
{%config%}
|
||||||
|
{%scripts%}
|
||||||
|
{%renderer%}
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
'''
|
||||||
|
|
||||||
# Suppress Dash development mode logging
|
# Suppress Dash development mode logging
|
||||||
self.app.enable_dev_tools(debug=False, dev_tools_silence_routes_logging=True)
|
self.app.enable_dev_tools(debug=False, dev_tools_silence_routes_logging=True)
|
||||||
|
|
||||||
|
@ -438,11 +438,20 @@ class DashboardComponentManager:
|
|||||||
all_usd_volumes = [b['usd_volume'] for b in bid_buckets.values()] + [a['usd_volume'] for a in ask_buckets.values()]
|
all_usd_volumes = [b['usd_volume'] for b in bid_buckets.values()] + [a['usd_volume'] for a in ask_buckets.values()]
|
||||||
max_volume = max(all_usd_volumes) if all_usd_volumes else 1
|
max_volume = max(all_usd_volumes) if all_usd_volumes else 1
|
||||||
|
|
||||||
# Create price levels around mid price
|
# Create price levels around mid price - expanded range for more bars
|
||||||
center_bucket = round(mid_price / bucket_size) * bucket_size
|
center_bucket = round(mid_price / bucket_size) * bucket_size
|
||||||
ask_levels = [center_bucket + i * bucket_size for i in range(1, num_levels + 1)]
|
ask_levels = [center_bucket + i * bucket_size for i in range(1, num_levels + 1)]
|
||||||
bid_levels = [center_bucket - i * bucket_size for i in range(num_levels)]
|
bid_levels = [center_bucket - i * bucket_size for i in range(num_levels)]
|
||||||
|
|
||||||
|
# Debug: Log how many orders we have to work with
|
||||||
|
print(f"DEBUG COB: {symbol} - Processing {len(bids)} bids, {len(asks)} asks")
|
||||||
|
print(f"DEBUG COB: Mid price: ${mid_price:.2f}, Bucket size: ${bucket_size}")
|
||||||
|
print(f"DEBUG COB: Bid buckets: {len(bid_buckets)}, Ask buckets: {len(ask_buckets)}")
|
||||||
|
if bid_buckets:
|
||||||
|
print(f"DEBUG COB: Bid price range: ${min(bid_buckets.keys()):.2f} - ${max(bid_buckets.keys()):.2f}")
|
||||||
|
if ask_buckets:
|
||||||
|
print(f"DEBUG COB: Ask price range: ${min(ask_buckets.keys()):.2f} - ${max(ask_buckets.keys()):.2f}")
|
||||||
|
|
||||||
def create_bookmap_row(price, bid_data, ask_data, max_vol):
|
def create_bookmap_row(price, bid_data, ask_data, max_vol):
|
||||||
"""Create a Bookmap-style row with horizontal bars extending from center"""
|
"""Create a Bookmap-style row with horizontal bars extending from center"""
|
||||||
bid_volume = bid_data.get('usd_volume', 0)
|
bid_volume = bid_data.get('usd_volume', 0)
|
||||||
@ -472,32 +481,40 @@ class DashboardComponentManager:
|
|||||||
html.Div([
|
html.Div([
|
||||||
html.Div(
|
html.Div(
|
||||||
bid_vol_str,
|
bid_vol_str,
|
||||||
className="text-end text-success small fw-bold px-1",
|
className="text-end small fw-bold px-2",
|
||||||
style={
|
style={
|
||||||
"background": "rgba(40, 167, 69, 0.8)" if bid_volume > 0 else "transparent",
|
"background": "linear-gradient(90deg, rgba(34, 197, 94, 0.3), rgba(34, 197, 94, 0.9))" if bid_volume > 0 else "transparent",
|
||||||
|
"color": "#ffffff" if bid_volume > 0 else "transparent",
|
||||||
"width": f"{bid_width}%",
|
"width": f"{bid_width}%",
|
||||||
"minHeight": "18px",
|
"minHeight": "22px",
|
||||||
"display": "flex",
|
"display": "flex",
|
||||||
"alignItems": "center",
|
"alignItems": "center",
|
||||||
"justifyContent": "flex-end",
|
"justifyContent": "flex-end",
|
||||||
"marginLeft": "auto"
|
"marginLeft": "auto",
|
||||||
|
"border": "1px solid rgba(34, 197, 94, 0.5)" if bid_volume > 0 else "none",
|
||||||
|
"borderRadius": "2px",
|
||||||
|
"textShadow": "1px 1px 2px rgba(0,0,0,0.8)",
|
||||||
|
"fontWeight": "600"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
], style={"width": "40%", "display": "flex", "justifyContent": "flex-end"}),
|
], style={"width": "40%", "display": "flex", "justifyContent": "flex-end", "padding": "1px"}),
|
||||||
|
|
||||||
# Price in center
|
# Price in center
|
||||||
html.Div(
|
html.Div(
|
||||||
f"{price:,.0f}",
|
f"{price:,.0f}",
|
||||||
className="text-center small fw-bold text-light px-2",
|
className="text-center small fw-bold px-2",
|
||||||
style={
|
style={
|
||||||
"width": "20%",
|
"width": "20%",
|
||||||
"minHeight": "18px",
|
"minHeight": "22px",
|
||||||
"display": "flex",
|
"display": "flex",
|
||||||
"alignItems": "center",
|
"alignItems": "center",
|
||||||
"justifyContent": "center",
|
"justifyContent": "center",
|
||||||
"background": "rgba(108, 117, 125, 0.8)",
|
"background": "linear-gradient(180deg, rgba(75, 85, 99, 0.9), rgba(55, 65, 81, 0.9))",
|
||||||
"borderLeft": "1px solid rgba(255,255,255,0.2)",
|
"color": "#f8f9fa",
|
||||||
"borderRight": "1px solid rgba(255,255,255,0.2)"
|
"borderLeft": "1px solid rgba(156, 163, 175, 0.3)",
|
||||||
|
"borderRight": "1px solid rgba(156, 163, 175, 0.3)",
|
||||||
|
"textShadow": "1px 1px 2px rgba(0,0,0,0.8)",
|
||||||
|
"fontWeight": "600"
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
|
||||||
@ -505,24 +522,30 @@ class DashboardComponentManager:
|
|||||||
html.Div([
|
html.Div([
|
||||||
html.Div(
|
html.Div(
|
||||||
ask_vol_str,
|
ask_vol_str,
|
||||||
className="text-start text-danger small fw-bold px-1",
|
className="text-start small fw-bold px-2",
|
||||||
style={
|
style={
|
||||||
"background": "rgba(220, 53, 69, 0.8)" if ask_volume > 0 else "transparent",
|
"background": "linear-gradient(270deg, rgba(239, 68, 68, 0.3), rgba(239, 68, 68, 0.9))" if ask_volume > 0 else "transparent",
|
||||||
|
"color": "#ffffff" if ask_volume > 0 else "transparent",
|
||||||
"width": f"{ask_width}%",
|
"width": f"{ask_width}%",
|
||||||
"minHeight": "18px",
|
"minHeight": "22px",
|
||||||
"display": "flex",
|
"display": "flex",
|
||||||
"alignItems": "center",
|
"alignItems": "center",
|
||||||
"justifyContent": "flex-start"
|
"justifyContent": "flex-start",
|
||||||
|
"border": "1px solid rgba(239, 68, 68, 0.5)" if ask_volume > 0 else "none",
|
||||||
|
"borderRadius": "2px",
|
||||||
|
"textShadow": "1px 1px 2px rgba(0,0,0,0.8)",
|
||||||
|
"fontWeight": "600"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
], style={"width": "40%", "display": "flex", "justifyContent": "flex-start"})
|
], style={"width": "40%", "display": "flex", "justifyContent": "flex-start", "padding": "1px"})
|
||||||
|
|
||||||
], style={
|
], style={
|
||||||
"display": "flex",
|
"display": "flex",
|
||||||
"alignItems": "center",
|
"alignItems": "center",
|
||||||
"marginBottom": "1px",
|
"marginBottom": "2px",
|
||||||
"background": "rgba(33, 37, 41, 0.9)",
|
"background": "rgba(17, 24, 39, 0.95)",
|
||||||
"border": "1px solid rgba(255,255,255,0.1)"
|
"border": "1px solid rgba(75, 85, 99, 0.3)",
|
||||||
|
"borderRadius": "3px"
|
||||||
})
|
})
|
||||||
])
|
])
|
||||||
|
|
||||||
@ -538,29 +561,41 @@ class DashboardComponentManager:
|
|||||||
if bid_data['usd_volume'] > 0 or ask_data['usd_volume'] > 0 or abs(price - mid_price) <= bucket_size * 5:
|
if bid_data['usd_volume'] > 0 or ask_data['usd_volume'] > 0 or abs(price - mid_price) <= bucket_size * 5:
|
||||||
rows.append(create_bookmap_row(price, bid_data, ask_data, max_volume))
|
rows.append(create_bookmap_row(price, bid_data, ask_data, max_volume))
|
||||||
|
|
||||||
# Add header
|
# Add header with improved dark theme styling
|
||||||
header = html.Div([
|
header = html.Div([
|
||||||
html.Div("BIDS", className="text-success text-center fw-bold small", style={"width": "40%"}),
|
html.Div("BIDS", className="text-center fw-bold small",
|
||||||
html.Div("PRICE", className="text-light text-center fw-bold small", style={"width": "20%"}),
|
style={"width": "40%", "color": "#10b981", "textShadow": "1px 1px 2px rgba(0,0,0,0.8)"}),
|
||||||
html.Div("ASKS", className="text-danger text-center fw-bold small", style={"width": "40%"})
|
html.Div("PRICE", className="text-center fw-bold small",
|
||||||
|
style={"width": "20%", "color": "#f8f9fa", "textShadow": "1px 1px 2px rgba(0,0,0,0.8)"}),
|
||||||
|
html.Div("ASKS", className="text-center fw-bold small",
|
||||||
|
style={"width": "40%", "color": "#ef4444", "textShadow": "1px 1px 2px rgba(0,0,0,0.8)"})
|
||||||
], style={
|
], style={
|
||||||
"display": "flex",
|
"display": "flex",
|
||||||
"marginBottom": "5px",
|
"marginBottom": "8px",
|
||||||
"padding": "5px",
|
"padding": "8px",
|
||||||
"background": "rgba(52, 58, 64, 0.9)",
|
"background": "linear-gradient(180deg, rgba(31, 41, 55, 0.95), rgba(17, 24, 39, 0.95))",
|
||||||
"border": "1px solid rgba(255,255,255,0.2)"
|
"border": "1px solid rgba(75, 85, 99, 0.4)",
|
||||||
|
"borderRadius": "6px",
|
||||||
|
"boxShadow": "0 2px 4px rgba(0,0,0,0.3)"
|
||||||
})
|
})
|
||||||
|
|
||||||
return html.Div([
|
return html.Div([
|
||||||
header,
|
header,
|
||||||
html.Div(rows, style={
|
html.Div(rows, style={
|
||||||
"maxHeight": "400px",
|
"maxHeight": "500px",
|
||||||
"overflowY": "auto",
|
"overflowY": "auto",
|
||||||
"background": "rgba(33, 37, 41, 0.95)",
|
"background": "linear-gradient(180deg, rgba(17, 24, 39, 0.98), rgba(31, 41, 55, 0.98))",
|
||||||
"border": "1px solid rgba(255,255,255,0.2)",
|
"border": "2px solid rgba(75, 85, 99, 0.4)",
|
||||||
"borderRadius": "4px"
|
"borderRadius": "8px",
|
||||||
|
"boxShadow": "inset 0 2px 4px rgba(0,0,0,0.3)"
|
||||||
|
})
|
||||||
|
], style={
|
||||||
|
"fontFamily": "monospace",
|
||||||
|
"background": "rgba(17, 24, 39, 0.9)",
|
||||||
|
"padding": "8px",
|
||||||
|
"borderRadius": "8px",
|
||||||
|
"border": "1px solid rgba(75, 85, 99, 0.3)"
|
||||||
})
|
})
|
||||||
], style={"fontFamily": "monospace"})
|
|
||||||
|
|
||||||
def format_cob_data_with_buckets(self, cob_snapshot, symbol, price_buckets, memory_stats, bucket_size=1.0):
|
def format_cob_data_with_buckets(self, cob_snapshot, symbol, price_buckets, memory_stats, bucket_size=1.0):
|
||||||
"""Format COB data with price buckets for high-frequency display"""
|
"""Format COB data with price buckets for high-frequency display"""
|
||||||
|
@ -15,12 +15,16 @@ class DashboardLayoutManager:
|
|||||||
self.trading_executor = trading_executor
|
self.trading_executor = trading_executor
|
||||||
|
|
||||||
def create_main_layout(self):
|
def create_main_layout(self):
|
||||||
"""Create the main dashboard layout"""
|
"""Create the main dashboard layout with dark theme"""
|
||||||
return html.Div([
|
return html.Div([
|
||||||
self._create_header(),
|
self._create_header(),
|
||||||
self._create_interval_component(),
|
self._create_interval_component(),
|
||||||
self._create_main_content()
|
self._create_main_content()
|
||||||
], className="container-fluid")
|
], className="container-fluid", style={
|
||||||
|
"backgroundColor": "#111827",
|
||||||
|
"minHeight": "100vh",
|
||||||
|
"color": "#f8f9fa"
|
||||||
|
})
|
||||||
|
|
||||||
def _create_header(self):
|
def _create_header(self):
|
||||||
"""Create the dashboard header"""
|
"""Create the dashboard header"""
|
||||||
@ -84,7 +88,12 @@ class DashboardLayoutManager:
|
|||||||
html.H5(id=card_id, className=f"{text_class} mb-0 small"),
|
html.H5(id=card_id, className=f"{text_class} mb-0 small"),
|
||||||
html.P(label, className="text-muted mb-0 tiny")
|
html.P(label, className="text-muted mb-0 tiny")
|
||||||
], className="card-body text-center p-2")
|
], className="card-body text-center p-2")
|
||||||
], className="card bg-light", style={"height": "60px"})
|
], className="card", style={
|
||||||
|
"height": "60px",
|
||||||
|
"backgroundColor": "#1f2937",
|
||||||
|
"border": "1px solid #374151",
|
||||||
|
"color": "#f8f9fa"
|
||||||
|
})
|
||||||
cards.append(card)
|
cards.append(card)
|
||||||
|
|
||||||
return html.Div(
|
return html.Div(
|
||||||
|
Reference in New Issue
Block a user