RL trainer

This commit is contained in:
Dobromir Popov
2025-05-28 13:20:15 +03:00
parent d6a71c2b1a
commit a6eaa01735
8 changed files with 1476 additions and 132 deletions

View File

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