RL trainer
This commit is contained in:
318
web/dashboard.py
318
web/dashboard.py
@ -79,6 +79,44 @@ class TradingDashboard:
|
||||
self.trading_executor = trading_executor or TradingExecutor()
|
||||
self.model_registry = get_model_registry()
|
||||
|
||||
# IMPORTANT: Multi-symbol live data streaming for strategy analysis
|
||||
# WebSocket streams are available for major pairs on Binance
|
||||
# We get live data for correlated assets but only trade the primary symbol
|
||||
# MEXC trading only supports ETH/USDC (not ETH/USDT)
|
||||
self.available_symbols = {
|
||||
'ETH/USDT': 'ETHUSDT', # Primary trading symbol - Binance WebSocket
|
||||
'BTC/USDT': 'BTCUSDT', # Correlated asset - Binance WebSocket
|
||||
# Note: ETH/USDC is for MEXC trading but no Binance WebSocket
|
||||
}
|
||||
|
||||
# Primary symbol for trading (first available symbol)
|
||||
self.primary_symbol = None
|
||||
self.primary_websocket_symbol = None
|
||||
|
||||
# All symbols for live data streaming (strategy analysis)
|
||||
self.streaming_symbols = []
|
||||
self.websocket_symbols = []
|
||||
|
||||
# Find primary trading symbol
|
||||
for symbol in self.config.symbols:
|
||||
if symbol in self.available_symbols:
|
||||
self.primary_symbol = symbol
|
||||
self.primary_websocket_symbol = self.available_symbols[symbol]
|
||||
logger.info(f"DASHBOARD: Primary trading symbol: {symbol} (WebSocket: {self.primary_websocket_symbol})")
|
||||
break
|
||||
|
||||
# Fallback to ETH/USDT if no configured symbol is available
|
||||
if not self.primary_symbol:
|
||||
self.primary_symbol = 'ETH/USDT'
|
||||
self.primary_websocket_symbol = 'ETHUSDT'
|
||||
logger.warning(f"DASHBOARD: No configured symbols available for live data, using fallback: {self.primary_symbol}")
|
||||
|
||||
# Setup all available symbols for streaming (strategy analysis)
|
||||
for symbol, ws_symbol in self.available_symbols.items():
|
||||
self.streaming_symbols.append(symbol)
|
||||
self.websocket_symbols.append(ws_symbol)
|
||||
logger.info(f"DASHBOARD: Will stream live data for {symbol} (WebSocket: {ws_symbol})")
|
||||
|
||||
# Dashboard state
|
||||
self.recent_decisions = []
|
||||
self.recent_signals = [] # Track all signals (not just executed trades)
|
||||
@ -301,7 +339,15 @@ class TradingDashboard:
|
||||
html.I(className="fas fa-chart-pie me-2"),
|
||||
"Session Performance"
|
||||
], className="card-title mb-2"),
|
||||
html.Div(id="session-performance")
|
||||
html.Div([
|
||||
html.Button(
|
||||
"Clear Session",
|
||||
id="clear-session-btn",
|
||||
className="btn btn-sm btn-outline-warning mb-2",
|
||||
n_clicks=0
|
||||
),
|
||||
html.Div(id="session-performance")
|
||||
])
|
||||
], className="card-body p-2")
|
||||
], className="card", style={"width": "32%"}),
|
||||
|
||||
@ -378,16 +424,15 @@ class TradingDashboard:
|
||||
"""Update all dashboard components with trading signals"""
|
||||
try:
|
||||
# Get current prices with improved fallback handling
|
||||
symbol = self.config.symbols[0] if self.config.symbols else "ETH/USDT"
|
||||
symbol = self.primary_symbol # Use configured symbol instead of hardcoded
|
||||
current_price = None
|
||||
chart_data = None
|
||||
data_source = "UNKNOWN"
|
||||
|
||||
try:
|
||||
# First try WebSocket current price (lowest latency)
|
||||
ws_symbol = symbol.replace('/', '') # Convert ETH/USDT to ETHUSDT for WebSocket
|
||||
if ws_symbol in self.current_prices and self.current_prices[ws_symbol] > 0:
|
||||
current_price = self.current_prices[ws_symbol]
|
||||
if self.primary_websocket_symbol in self.current_prices and self.current_prices[self.primary_websocket_symbol] > 0:
|
||||
current_price = self.current_prices[self.primary_websocket_symbol]
|
||||
data_source = "WEBSOCKET"
|
||||
logger.debug(f"[WS_PRICE] Using WebSocket price for {symbol}: ${current_price:.2f}")
|
||||
else:
|
||||
@ -642,6 +687,19 @@ class TradingDashboard:
|
||||
self.clear_closed_trades_history()
|
||||
return [html.P("Trade history cleared", className="text-muted text-center")]
|
||||
return self._create_closed_trades_table()
|
||||
|
||||
# Clear session performance callback
|
||||
@self.app.callback(
|
||||
Output('session-performance', 'children', allow_duplicate=True),
|
||||
[Input('clear-session-btn', 'n_clicks')],
|
||||
prevent_initial_call=True
|
||||
)
|
||||
def clear_session_performance(n_clicks):
|
||||
"""Clear the session performance data"""
|
||||
if n_clicks and n_clicks > 0:
|
||||
self.clear_session_performance()
|
||||
return [html.P("Session performance cleared", className="text-muted text-center")]
|
||||
return self._create_session_performance()
|
||||
|
||||
def _simulate_price_update(self, symbol: str, base_price: float) -> float:
|
||||
"""
|
||||
@ -1483,7 +1541,7 @@ class TradingDashboard:
|
||||
'fees': fee + self.current_position['fees'],
|
||||
'net_pnl': net_pnl,
|
||||
'duration': current_time - entry_time,
|
||||
'symbol': decision.get('symbol', 'ETH/USDT'),
|
||||
'symbol': self.primary_symbol, # Use primary symbol instead of hardcoded
|
||||
'mexc_executed': decision.get('mexc_executed', False)
|
||||
}
|
||||
self.closed_trades.append(closed_trade)
|
||||
@ -1556,7 +1614,7 @@ class TradingDashboard:
|
||||
'fees': fee + self.current_position['fees'],
|
||||
'net_pnl': net_pnl,
|
||||
'duration': current_time - entry_time,
|
||||
'symbol': decision.get('symbol', 'ETH/USDT'),
|
||||
'symbol': self.primary_symbol, # Use primary symbol instead of hardcoded
|
||||
'mexc_executed': decision.get('mexc_executed', False)
|
||||
}
|
||||
self.closed_trades.append(closed_trade)
|
||||
@ -1608,7 +1666,7 @@ class TradingDashboard:
|
||||
'fees': fee + self.current_position['fees'],
|
||||
'net_pnl': net_pnl,
|
||||
'duration': current_time - entry_time,
|
||||
'symbol': decision.get('symbol', 'ETH/USDT'),
|
||||
'symbol': self.primary_symbol, # Use primary symbol instead of hardcoded
|
||||
'mexc_executed': decision.get('mexc_executed', False)
|
||||
}
|
||||
self.closed_trades.append(closed_trade)
|
||||
@ -1716,36 +1774,35 @@ class TradingDashboard:
|
||||
|
||||
# Simple trading loop without async complexity
|
||||
import time
|
||||
symbols = self.config.symbols if self.config.symbols else ['ETH/USDT']
|
||||
|
||||
while True:
|
||||
try:
|
||||
# Make trading decisions for each symbol every 30 seconds
|
||||
for symbol in symbols:
|
||||
try:
|
||||
# Get current price
|
||||
current_data = self.data_provider.get_historical_data(symbol, '1m', limit=1, refresh=True)
|
||||
if current_data is not None and not current_data.empty:
|
||||
current_price = float(current_data['close'].iloc[-1])
|
||||
|
||||
# Simple decision making
|
||||
decision = {
|
||||
'action': 'HOLD', # Conservative default
|
||||
'symbol': symbol,
|
||||
'price': current_price,
|
||||
'confidence': 0.5,
|
||||
'timestamp': datetime.now(),
|
||||
'size': 0.1,
|
||||
'reason': f"Orchestrator monitoring {symbol}"
|
||||
}
|
||||
|
||||
# Process the decision (adds to dashboard display)
|
||||
self._process_trading_decision(decision)
|
||||
|
||||
logger.debug(f"[ORCHESTRATOR] {decision['action']} {symbol} @ ${current_price:.2f}")
|
||||
# Make trading decisions for the primary symbol only
|
||||
symbol = self.primary_symbol
|
||||
try:
|
||||
# Get current price
|
||||
current_data = self.data_provider.get_historical_data(symbol, '1m', limit=1, refresh=True)
|
||||
if current_data is not None and not current_data.empty:
|
||||
current_price = float(current_data['close'].iloc[-1])
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"[ORCHESTRATOR] Error processing {symbol}: {e}")
|
||||
# Simple decision making
|
||||
decision = {
|
||||
'action': 'HOLD', # Conservative default
|
||||
'symbol': symbol,
|
||||
'price': current_price,
|
||||
'confidence': 0.5,
|
||||
'timestamp': datetime.now(),
|
||||
'size': 0.1,
|
||||
'reason': f"Orchestrator monitoring {symbol}"
|
||||
}
|
||||
|
||||
# Process the decision (adds to dashboard display)
|
||||
self._process_trading_decision(decision)
|
||||
|
||||
logger.debug(f"[ORCHESTRATOR] {decision['action']} {symbol} @ ${current_price:.2f}")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"[ORCHESTRATOR] Error processing {symbol}: {e}")
|
||||
|
||||
# Wait before next cycle
|
||||
time.sleep(30)
|
||||
@ -1916,6 +1973,33 @@ class TradingDashboard:
|
||||
except Exception as e:
|
||||
logger.error(f"Error clearing closed trades history: {e}")
|
||||
|
||||
def clear_session_performance(self):
|
||||
"""Clear session performance data and reset session tracking"""
|
||||
try:
|
||||
# Reset session start time
|
||||
self.session_start = datetime.now()
|
||||
|
||||
# Clear session tracking data
|
||||
self.session_trades = []
|
||||
self.session_pnl = 0.0
|
||||
self.total_realized_pnl = 0.0
|
||||
self.total_fees = 0.0
|
||||
|
||||
# Clear current position
|
||||
self.current_position = None
|
||||
|
||||
# Clear recent decisions and signals (but keep last few for context)
|
||||
self.recent_decisions = []
|
||||
self.recent_signals = []
|
||||
|
||||
# Reset signal timing
|
||||
self.last_signal_time = 0
|
||||
|
||||
logger.info("Session performance cleared and reset")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error clearing session performance: {e}")
|
||||
|
||||
def _create_session_performance(self) -> List:
|
||||
"""Create compact session performance display with signal statistics"""
|
||||
try:
|
||||
@ -1934,11 +2018,28 @@ class TradingDashboard:
|
||||
total_signals = len(executed_signals) + len(ignored_signals)
|
||||
execution_rate = (len(executed_signals) / total_signals * 100) if total_signals > 0 else 0
|
||||
|
||||
# Calculate portfolio metrics with better handling of small balances
|
||||
portfolio_value = self.starting_balance + self.total_realized_pnl
|
||||
|
||||
# Fix return calculation for small balances
|
||||
if self.starting_balance >= 1.0: # Normal balance
|
||||
portfolio_return = (self.total_realized_pnl / self.starting_balance * 100)
|
||||
logger.debug(f"SESSION_PERF: Normal balance ${self.starting_balance:.4f}, return {portfolio_return:+.2f}%")
|
||||
elif self.total_realized_pnl != 0: # Small balance with some P&L
|
||||
# For very small balances, show absolute P&L instead of percentage
|
||||
portfolio_return = None # Will display absolute value instead
|
||||
logger.debug(f"SESSION_PERF: Small balance ${self.starting_balance:.4f}, P&L ${self.total_realized_pnl:+.4f}")
|
||||
else: # No P&L
|
||||
portfolio_return = 0.0
|
||||
logger.debug(f"SESSION_PERF: No P&L, balance ${self.starting_balance:.4f}")
|
||||
|
||||
# Debug the final display value
|
||||
display_value = f"{portfolio_return:+.2f}%" if portfolio_return is not None else f"${self.total_realized_pnl:+.4f}"
|
||||
logger.debug(f"SESSION_PERF: Final display value: {display_value}")
|
||||
|
||||
# Calculate other metrics
|
||||
total_volume = sum(t.get('price', 0) * t.get('size', 0) for t in self.session_trades)
|
||||
avg_trade_pnl = (self.total_realized_pnl / closed_trades) if closed_trades > 0 else 0
|
||||
portfolio_value = self.starting_balance + self.total_realized_pnl
|
||||
portfolio_return = (self.total_realized_pnl / self.starting_balance * 100) if self.starting_balance > 0 else 0
|
||||
|
||||
performance_items = [
|
||||
# Row 1: Duration and Portfolio Value
|
||||
@ -1980,16 +2081,19 @@ class TradingDashboard:
|
||||
], className="col-6 small")
|
||||
], className="row mb-1"),
|
||||
|
||||
# Row 4: Portfolio Return and Fees
|
||||
# Row 4: Return/P&L and Fees
|
||||
html.Div([
|
||||
html.Div([
|
||||
html.Strong("Return: "),
|
||||
html.Span(f"{portfolio_return:+.2f}%",
|
||||
className="text-success" if portfolio_return >= 0 else "text-danger")
|
||||
html.Strong("Net P&L: "), # Changed label to force UI update
|
||||
# Show return percentage for normal balances, absolute P&L for small balances
|
||||
html.Span(
|
||||
f"{portfolio_return:+.2f}%" if portfolio_return is not None else f"${self.total_realized_pnl:+.4f}",
|
||||
className="text-success" if self.total_realized_pnl >= 0 else "text-danger"
|
||||
)
|
||||
], className="col-6 small"),
|
||||
html.Div([
|
||||
html.Strong("Fees: "),
|
||||
html.Span(f"${self.total_fees:.2f}", className="text-muted")
|
||||
html.Span(f"${self.total_fees:.4f}", className="text-muted")
|
||||
], className="col-6 small")
|
||||
], className="row")
|
||||
]
|
||||
@ -2179,12 +2283,12 @@ class TradingDashboard:
|
||||
logger.warning(f"RL prediction error: {e}")
|
||||
return np.array([0.33, 0.34, 0.33]), 0.5
|
||||
|
||||
def get_memory_usage(self):
|
||||
return 80 # MB estimate
|
||||
|
||||
def to_device(self, device):
|
||||
self.device = device
|
||||
return self
|
||||
def get_memory_usage(self):
|
||||
return 80 # MB estimate
|
||||
|
||||
def to_device(self, device):
|
||||
self.device = device
|
||||
return self
|
||||
|
||||
rl_wrapper = RLWrapper(rl_path)
|
||||
|
||||
@ -2276,52 +2380,66 @@ class TradingDashboard:
|
||||
}
|
||||
|
||||
def _start_websocket_stream(self):
|
||||
"""Start WebSocket connection for real-time tick data"""
|
||||
"""Start WebSocket connections for real-time tick data from multiple symbols"""
|
||||
try:
|
||||
if not WEBSOCKET_AVAILABLE:
|
||||
logger.warning("[WEBSOCKET] websocket-client not available. Using data provider fallback.")
|
||||
self.is_streaming = False
|
||||
return
|
||||
|
||||
symbol = self.config.symbols[0] if self.config.symbols else "ETHUSDT"
|
||||
# Check if we have symbols to stream
|
||||
if not self.websocket_symbols:
|
||||
logger.warning(f"[WEBSOCKET] No WebSocket symbols configured. Streaming disabled.")
|
||||
self.is_streaming = False
|
||||
return
|
||||
|
||||
# Start WebSocket in background thread
|
||||
self.ws_thread = threading.Thread(target=self._websocket_worker, args=(symbol,), daemon=True)
|
||||
self.ws_thread.start()
|
||||
|
||||
logger.info(f"[WEBSOCKET] Starting real-time tick stream for {symbol}")
|
||||
# Start WebSocket for each symbol in background threads
|
||||
self.ws_threads = []
|
||||
for i, ws_symbol in enumerate(self.websocket_symbols):
|
||||
symbol_name = self.streaming_symbols[i]
|
||||
thread = threading.Thread(
|
||||
target=self._websocket_worker,
|
||||
args=(ws_symbol, symbol_name),
|
||||
daemon=True
|
||||
)
|
||||
thread.start()
|
||||
self.ws_threads.append(thread)
|
||||
logger.info(f"[WEBSOCKET] Starting real-time tick stream for {symbol_name} (WebSocket: {ws_symbol})")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error starting WebSocket stream: {e}")
|
||||
logger.error(f"Error starting WebSocket streams: {e}")
|
||||
self.is_streaming = False
|
||||
|
||||
def _websocket_worker(self, symbol: str):
|
||||
def _websocket_worker(self, websocket_symbol: str, symbol_name: str):
|
||||
"""WebSocket worker thread for continuous tick data streaming"""
|
||||
try:
|
||||
# Use Binance WebSocket for real-time tick data
|
||||
ws_url = f"wss://stream.binance.com:9443/ws/{symbol.lower().replace('/', '')}@ticker"
|
||||
ws_url = f"wss://stream.binance.com:9443/ws/{websocket_symbol.lower()}@ticker"
|
||||
|
||||
def on_message(ws, message):
|
||||
try:
|
||||
data = json.loads(message)
|
||||
# Add symbol info to tick data for processing
|
||||
data['symbol_name'] = symbol_name
|
||||
data['websocket_symbol'] = websocket_symbol
|
||||
self._process_tick_data(data)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error processing WebSocket message: {e}")
|
||||
logger.warning(f"Error processing WebSocket message for {symbol_name}: {e}")
|
||||
|
||||
def on_error(ws, error):
|
||||
logger.error(f"WebSocket error: {error}")
|
||||
logger.error(f"WebSocket error for {symbol_name}: {error}")
|
||||
self.is_streaming = False
|
||||
|
||||
def on_close(ws, close_status_code, close_msg):
|
||||
logger.warning("WebSocket connection closed")
|
||||
logger.warning(f"WebSocket connection closed for {symbol_name}")
|
||||
self.is_streaming = False
|
||||
# Attempt to reconnect after 5 seconds
|
||||
time.sleep(5)
|
||||
if not self.is_streaming:
|
||||
self._websocket_worker(symbol)
|
||||
self._websocket_worker(websocket_symbol, symbol_name)
|
||||
|
||||
def on_open(ws):
|
||||
logger.info("[WEBSOCKET] Connected to Binance stream")
|
||||
logger.info(f"[WEBSOCKET] Connected to Binance stream for {symbol_name}")
|
||||
self.is_streaming = True
|
||||
|
||||
# Create WebSocket connection
|
||||
@ -2337,60 +2455,47 @@ class TradingDashboard:
|
||||
self.ws_connection.run_forever()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"WebSocket worker error: {e}")
|
||||
logger.error(f"WebSocket worker error for {symbol_name}: {e}")
|
||||
self.is_streaming = False
|
||||
|
||||
def _process_tick_data(self, tick_data: Dict):
|
||||
"""Process incoming tick data and update 1-second bars"""
|
||||
"""Process incoming WebSocket tick data for multiple symbols"""
|
||||
try:
|
||||
# Extract price and volume from Binance ticker data
|
||||
price = float(tick_data.get('c', 0)) # Current price
|
||||
volume = float(tick_data.get('v', 0)) # 24h volume
|
||||
timestamp = datetime.now(timezone.utc)
|
||||
# Extract price and symbol information
|
||||
price = float(tick_data.get('c', 0)) # 'c' is current price in Binance ticker
|
||||
websocket_symbol = tick_data.get('websocket_symbol', tick_data.get('s', 'UNKNOWN'))
|
||||
symbol_name = tick_data.get('symbol_name', 'UNKNOWN')
|
||||
|
||||
# Add to tick cache
|
||||
tick = {
|
||||
'timestamp': timestamp,
|
||||
if price <= 0:
|
||||
return
|
||||
|
||||
# Update current price for this symbol
|
||||
self.current_prices[websocket_symbol] = price
|
||||
|
||||
# Log price updates (less frequently to avoid spam)
|
||||
if len(self.tick_buffer) % 10 == 0: # Log every 10th tick
|
||||
logger.debug(f"[TICK] {symbol_name}: ${price:.2f}")
|
||||
|
||||
# Create tick record for training data
|
||||
tick_record = {
|
||||
'symbol': symbol_name,
|
||||
'websocket_symbol': websocket_symbol,
|
||||
'price': price,
|
||||
'volume': volume,
|
||||
'bid': float(tick_data.get('b', price)), # Best bid
|
||||
'ask': float(tick_data.get('a', price)), # Best ask
|
||||
'high_24h': float(tick_data.get('h', price)),
|
||||
'low_24h': float(tick_data.get('l', price))
|
||||
'volume': float(tick_data.get('v', 0)),
|
||||
'timestamp': datetime.now(),
|
||||
'is_primary': websocket_symbol == self.primary_websocket_symbol
|
||||
}
|
||||
|
||||
self.tick_cache.append(tick)
|
||||
# Add to tick buffer for 1-second bar creation
|
||||
self.tick_buffer.append(tick_record)
|
||||
|
||||
# Update current second bar
|
||||
current_second = timestamp.replace(microsecond=0)
|
||||
|
||||
if self.current_second_data['timestamp'] != current_second:
|
||||
# New second - finalize previous bar and start new one
|
||||
if self.current_second_data['timestamp'] is not None:
|
||||
self._finalize_second_bar()
|
||||
|
||||
# Start new second bar
|
||||
self.current_second_data = {
|
||||
'timestamp': current_second,
|
||||
'open': price,
|
||||
'high': price,
|
||||
'low': price,
|
||||
'close': price,
|
||||
'volume': 0,
|
||||
'tick_count': 1
|
||||
}
|
||||
else:
|
||||
# Update current second bar
|
||||
self.current_second_data['high'] = max(self.current_second_data['high'], price)
|
||||
self.current_second_data['low'] = min(self.current_second_data['low'], price)
|
||||
self.current_second_data['close'] = price
|
||||
self.current_second_data['tick_count'] += 1
|
||||
|
||||
# Update current price for dashboard
|
||||
self.current_prices[tick_data.get('s', 'ETHUSDT')] = price
|
||||
# Keep buffer size manageable (last 1000 ticks per symbol)
|
||||
if len(self.tick_buffer) > 1000:
|
||||
self.tick_buffer = self.tick_buffer[-1000:]
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error processing tick data: {e}")
|
||||
logger.error(f"Error processing tick data: {e}")
|
||||
logger.debug(f"Problematic tick data: {tick_data}")
|
||||
|
||||
def _finalize_second_bar(self):
|
||||
"""Finalize the current second bar and add to bars cache"""
|
||||
@ -2995,7 +3100,7 @@ class TradingDashboard:
|
||||
return {
|
||||
'ohlcv': ohlcv,
|
||||
'raw_ticks': df,
|
||||
'symbol': 'ETH/USDT',
|
||||
'symbol': self.primary_symbol, # Use primary symbol instead of hardcoded
|
||||
'timeframe': '1s',
|
||||
'features': ['open', 'high', 'low', 'close', 'volume', 'sma_20', 'sma_50', 'rsi'],
|
||||
'timestamp': datetime.now()
|
||||
@ -3268,6 +3373,7 @@ class TradingDashboard:
|
||||
logger.info("Continuous training stopped")
|
||||
except Exception as e:
|
||||
logger.error(f"Error stopping continuous training: {e}")
|
||||
|
||||
# Convenience function for integration
|
||||
def create_dashboard(data_provider: DataProvider = None, orchestrator: TradingOrchestrator = None, trading_executor: TradingExecutor = None) -> TradingDashboard:
|
||||
"""Create and return a trading dashboard instance"""
|
||||
|
Reference in New Issue
Block a user