From 77a96030ba8d34d014e05d882393e89ad1f69062 Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Tue, 24 Jun 2025 15:53:11 +0300 Subject: [PATCH 1/2] cob dash integration --- run_integrated_rl_cob_dashboard.py | 513 +++++++++++++++++++++++++++++ web/enhanced_cob_dashboard.html | Bin 0 -> 3298 bytes 2 files changed, 513 insertions(+) create mode 100644 run_integrated_rl_cob_dashboard.py create mode 100644 web/enhanced_cob_dashboard.html diff --git a/run_integrated_rl_cob_dashboard.py b/run_integrated_rl_cob_dashboard.py new file mode 100644 index 0000000..7f6731d --- /dev/null +++ b/run_integrated_rl_cob_dashboard.py @@ -0,0 +1,513 @@ +#!/usr/bin/env python3 +""" +Integrated Real-time RL COB Trading System with Dashboard + +This script starts both: +1. RealtimeRLCOBTrader - 1B parameter RL model with real-time training +2. COB Dashboard - Real-time visualization with RL predictions + +The RL predictions are integrated into the dashboard for live visualization. +""" + +import asyncio +import logging +import signal +import sys +import json +import os +from datetime import datetime +from typing import Dict, Any +from aiohttp import web + +# Local imports +from core.realtime_rl_cob_trader import RealtimeRLCOBTrader, PredictionResult +from core.trading_executor import TradingExecutor +from core.config import load_config +from web.cob_realtime_dashboard import COBDashboardServer + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler('logs/integrated_rl_cob_system.log'), + logging.StreamHandler(sys.stdout) + ] +) + +logger = logging.getLogger(__name__) + +class IntegratedRLCOBSystem: + """ + Integrated Real-time RL COB Trading System with Dashboard + """ + + def __init__(self, config_path: str = "config.yaml"): + """Initialize integrated system with configuration""" + self.config = load_config(config_path) + self.trader = None + self.dashboard = None + self.trading_executor = None + self.running = False + + # RL prediction storage for dashboard + self.rl_predictions: Dict[str, list] = {} + self.prediction_history: Dict[str, list] = {} + + # Setup signal handlers for graceful shutdown + signal.signal(signal.SIGINT, self._signal_handler) + signal.signal(signal.SIGTERM, self._signal_handler) + + logger.info("IntegratedRLCOBSystem initialized") + + def _signal_handler(self, signum, frame): + """Handle shutdown signals""" + logger.info(f"Received signal {signum}, initiating graceful shutdown...") + self.running = False + + async def start(self): + """Start the integrated RL COB trading system with dashboard""" + try: + logger.info("=" * 60) + logger.info("INTEGRATED RL COB SYSTEM STARTING") + logger.info("šŸ”„ Real-time RL Trading + Dashboard") + logger.info("=" * 60) + + # Initialize trading executor + await self._initialize_trading_executor() + + # Initialize RL trader with prediction callback + await self._initialize_rl_trader() + + # Initialize dashboard with RL integration + await self._initialize_dashboard() + + # Start the integrated system + await self._start_integrated_system() + + # Run main loop + await self._run_main_loop() + + except Exception as e: + logger.error(f"Critical error in integrated system: {e}") + raise + finally: + await self.stop() + + async def _initialize_trading_executor(self): + """Initialize the trading executor""" + logger.info("Initializing Trading Executor...") + + # Get trading configuration + trading_config = self.config.get('trading', {}) + mexc_config = self.config.get('mexc', {}) + + # Determine if we should run in simulation mode + simulation_mode = mexc_config.get('simulation_mode', True) + + if simulation_mode: + logger.info("Running in SIMULATION mode - no real trades will be executed") + else: + logger.warning("Running in LIVE TRADING mode - real money at risk!") + + # Add safety confirmation for live trading + confirmation = input("Type 'CONFIRM_LIVE_TRADING' to proceed with live trading: ") + if confirmation != 'CONFIRM_LIVE_TRADING': + logger.info("Live trading not confirmed, switching to simulation mode") + simulation_mode = True + + # Initialize trading executor + self.trading_executor = TradingExecutor( + simulation_mode=simulation_mode, + mexc_config=mexc_config + ) + + logger.info(f"Trading Executor initialized in {'SIMULATION' if simulation_mode else 'LIVE'} mode") + + async def _initialize_rl_trader(self): + """Initialize the RL trader with prediction callbacks""" + logger.info("Initializing Real-time RL COB Trader...") + + # Get RL configuration + rl_config = self.config.get('realtime_rl', {}) + + # Trading symbols + symbols = rl_config.get('symbols', ['BTC/USDT', 'ETH/USDT']) + + # Initialize prediction storage + for symbol in symbols: + self.rl_predictions[symbol] = [] + self.prediction_history[symbol] = [] + + # RL parameters + inference_interval_ms = rl_config.get('inference_interval_ms', 200) + min_confidence_threshold = rl_config.get('min_confidence_threshold', 0.7) + required_confident_predictions = rl_config.get('required_confident_predictions', 3) + model_checkpoint_dir = rl_config.get('model_checkpoint_dir', 'models/realtime_rl_cob') + + # Initialize RL trader + self.trader = RealtimeRLCOBTrader( + symbols=symbols, + trading_executor=self.trading_executor, + model_checkpoint_dir=model_checkpoint_dir, + inference_interval_ms=inference_interval_ms, + min_confidence_threshold=min_confidence_threshold, + required_confident_predictions=required_confident_predictions + ) + + # Monkey-patch the trader to capture predictions + original_add_signal = self.trader._add_signal + def enhanced_add_signal(symbol: str, prediction: PredictionResult): + # Call original method + original_add_signal(symbol, prediction) + # Capture prediction for dashboard + self._on_rl_prediction(symbol, prediction) + + self.trader._add_signal = enhanced_add_signal + + logger.info(f"RL Trader initialized for symbols: {symbols}") + logger.info(f"Inference interval: {inference_interval_ms}ms") + logger.info(f"Confidence threshold: {min_confidence_threshold}") + logger.info(f"Required predictions: {required_confident_predictions}") + + def _on_rl_prediction(self, symbol: str, prediction: PredictionResult): + """Handle RL predictions for dashboard integration""" + try: + # Convert prediction to dashboard format + prediction_data = { + 'timestamp': prediction.timestamp.isoformat(), + 'direction': prediction.predicted_direction, # 0=DOWN, 1=SIDEWAYS, 2=UP + 'confidence': prediction.confidence, + 'predicted_change': prediction.predicted_change, + 'direction_text': ['DOWN', 'SIDEWAYS', 'UP'][prediction.predicted_direction], + 'color': ['red', 'gray', 'green'][prediction.predicted_direction] + } + + # Add to current predictions (for live display) + self.rl_predictions[symbol].append(prediction_data) + if len(self.rl_predictions[symbol]) > 100: # Keep last 100 + self.rl_predictions[symbol] = self.rl_predictions[symbol][-100:] + + # Add to history (for chart overlay) + self.prediction_history[symbol].append(prediction_data) + if len(self.prediction_history[symbol]) > 1000: # Keep last 1000 + self.prediction_history[symbol] = self.prediction_history[symbol][-1000:] + + logger.debug(f"Captured RL prediction for {symbol}: {prediction.predicted_direction} " + f"(confidence: {prediction.confidence:.3f})") + + except Exception as e: + logger.error(f"Error capturing RL prediction: {e}") + + async def _initialize_dashboard(self): + """Initialize the COB dashboard with RL integration""" + logger.info("Initializing COB Dashboard with RL Integration...") + + # Get dashboard configuration + dashboard_config = self.config.get('dashboard', {}) + host = dashboard_config.get('host', 'localhost') + port = dashboard_config.get('port', 8053) + + # Create enhanced dashboard server + self.dashboard = EnhancedCOBDashboardServer( + host=host, + port=port, + rl_system=self # Pass reference to get predictions + ) + + logger.info(f"COB Dashboard initialized at http://{host}:{port}") + + async def _start_integrated_system(self): + """Start the complete integrated system""" + logger.info("Starting Integrated RL COB System...") + + # Start RL trader first (this initializes COB integration) + await self.trader.start() + logger.info("āœ… RL Trader started") + + # Start dashboard (uses same COB integration) + await self.dashboard.start() + logger.info("āœ… COB Dashboard started") + + self.running = True + + logger.info("šŸŽ‰ INTEGRATED SYSTEM FULLY OPERATIONAL!") + logger.info("šŸ”„ 1B parameter RL model: ACTIVE") + logger.info("šŸ“Š Real-time COB data: STREAMING") + logger.info("šŸŽÆ Signal accumulation: ACTIVE") + logger.info("šŸ’¹ Live predictions: VISIBLE IN DASHBOARD") + logger.info("⚔ Continuous training: ACTIVE") + logger.info(f"🌐 Dashboard URL: http://{self.dashboard.host}:{self.dashboard.port}") + + async def _run_main_loop(self): + """Main monitoring and statistics loop""" + logger.info("Starting integrated system monitoring...") + + last_stats_time = datetime.now() + stats_interval = 60 # Print stats every 60 seconds + + while self.running: + try: + # Sleep for a bit + await asyncio.sleep(10) + + # Print periodic statistics + current_time = datetime.now() + if (current_time - last_stats_time).total_seconds() >= stats_interval: + await self._print_integrated_stats() + last_stats_time = current_time + + except asyncio.CancelledError: + break + except Exception as e: + logger.error(f"Error in main loop: {e}") + await asyncio.sleep(5) + + logger.info("Integrated system monitoring stopped") + + async def _print_integrated_stats(self): + """Print comprehensive integrated system statistics""" + try: + logger.info("=" * 80) + logger.info("šŸ”„ INTEGRATED RL COB SYSTEM STATISTICS") + logger.info("=" * 80) + + # RL Trader Statistics + if self.trader: + rl_stats = self.trader.get_performance_stats() + logger.info("\nšŸ¤– RL TRADER PERFORMANCE:") + + for symbol in self.trader.symbols: + training_stats = rl_stats.get('training_stats', {}).get(symbol, {}) + inference_stats = rl_stats.get('inference_stats', {}).get(symbol, {}) + signal_stats = rl_stats.get('signal_stats', {}).get(symbol, {}) + + logger.info(f"\n šŸ“ˆ {symbol}:") + logger.info(f" Predictions: {training_stats.get('total_predictions', 0)}") + logger.info(f" Success Rate: {signal_stats.get('success_rate', 0):.1%}") + logger.info(f" Avg Inference: {inference_stats.get('average_inference_time_ms', 0):.1f}ms") + logger.info(f" Current Signals: {signal_stats.get('current_signals', 0)}") + + # RL prediction stats for dashboard + recent_predictions = len(self.rl_predictions.get(symbol, [])) + total_predictions = len(self.prediction_history.get(symbol, [])) + logger.info(f" Dashboard Predictions: {recent_predictions} recent, {total_predictions} total") + + # Dashboard Statistics + if self.dashboard: + logger.info(f"\n🌐 DASHBOARD STATISTICS:") + logger.info(f" Active Connections: {len(self.dashboard.websocket_connections)}") + logger.info(f" Server Status: {'RUNNING' if self.dashboard.site else 'STOPPED'}") + logger.info(f" URL: http://{self.dashboard.host}:{self.dashboard.port}") + + # Trading Executor Statistics + if self.trading_executor: + positions = self.trading_executor.get_positions() + trade_history = self.trading_executor.get_trade_history() + + logger.info(f"\nšŸ’° TRADING STATISTICS:") + logger.info(f" Active Positions: {len(positions)}") + logger.info(f" Total Trades: {len(trade_history)}") + + if trade_history: + total_pnl = sum(trade.pnl for trade in trade_history) + profitable_trades = sum(1 for trade in trade_history if trade.pnl > 0) + win_rate = (profitable_trades / len(trade_history)) * 100 + + logger.info(f" Total P&L: ${total_pnl:.2f}") + logger.info(f" Win Rate: {win_rate:.1f}%") + + logger.info("=" * 80) + + except Exception as e: + logger.error(f"Error printing integrated stats: {e}") + + async def stop(self): + """Stop the integrated system gracefully""" + if not self.running: + return + + logger.info("Stopping Integrated RL COB System...") + + self.running = False + + # Stop dashboard + if self.dashboard: + await self.dashboard.stop() + logger.info("āœ… Dashboard stopped") + + # Stop RL trader + if self.trader: + await self.trader.stop() + logger.info("āœ… RL Trader stopped") + + logger.info("šŸ Integrated system stopped successfully") + + def get_rl_predictions(self, symbol: str) -> Dict[str, Any]: + """Get RL predictions for dashboard display""" + return { + 'recent_predictions': self.rl_predictions.get(symbol, []), + 'prediction_history': self.prediction_history.get(symbol, []), + 'total_predictions': len(self.prediction_history.get(symbol, [])), + 'recent_count': len(self.rl_predictions.get(symbol, [])) + } + +class EnhancedCOBDashboardServer(COBDashboardServer): + """Enhanced COB Dashboard with RL prediction integration""" + + def __init__(self, host: str = 'localhost', port: int = 8053, rl_system: IntegratedRLCOBSystem = None): + super().__init__(host, port) + self.rl_system = rl_system + + # Add RL prediction routes + self._setup_rl_routes() + + logger.info("Enhanced COB Dashboard with RL predictions initialized") + + async def serve_dashboard(self, request): + """Serve the enhanced dashboard HTML with RL predictions""" + try: + # Read the enhanced dashboard HTML + dashboard_path = os.path.join(os.path.dirname(__file__), 'enhanced_cob_dashboard.html') + + if os.path.exists(dashboard_path): + with open(dashboard_path, 'r', encoding='utf-8') as f: + html_content = f.read() + return web.Response(text=html_content, content_type='text/html') + else: + # Fallback to basic dashboard + logger.warning("Enhanced dashboard HTML not found, using basic dashboard") + return await super().serve_dashboard(request) + + except Exception as e: + logger.error(f"Error serving enhanced dashboard: {e}") + return web.Response(text="Dashboard error", status=500) + + def _setup_rl_routes(self): + """Setup additional routes for RL predictions""" + self.app.router.add_get('/api/rl-predictions/{symbol}', self.get_rl_predictions) + self.app.router.add_get('/api/rl-status', self.get_rl_status) + + async def get_rl_predictions(self, request): + """Get RL predictions for a symbol""" + try: + symbol = request.match_info['symbol'] + symbol = symbol.replace('%2F', '/') + + if symbol not in self.symbols: + return web.json_response({ + 'error': f'Symbol {symbol} not supported' + }, status=400) + + if not self.rl_system: + return web.json_response({ + 'error': 'RL system not available' + }, status=503) + + predictions = self.rl_system.get_rl_predictions(symbol) + + return web.json_response({ + 'symbol': symbol, + 'timestamp': datetime.now().isoformat(), + 'predictions': predictions + }) + + except Exception as e: + logger.error(f"Error getting RL predictions: {e}") + return web.json_response({ + 'error': str(e) + }, status=500) + + async def get_rl_status(self, request): + """Get RL system status""" + try: + if not self.rl_system or not self.rl_system.trader: + return web.json_response({ + 'status': 'inactive', + 'error': 'RL system not available' + }) + + rl_stats = self.rl_system.trader.get_performance_stats() + + status = { + 'status': 'active', + 'symbols': self.rl_system.trader.symbols, + 'model_info': rl_stats.get('model_info', {}), + 'inference_interval_ms': self.rl_system.trader.inference_interval_ms, + 'confidence_threshold': self.rl_system.trader.min_confidence_threshold, + 'required_predictions': self.rl_system.trader.required_confident_predictions, + 'device': str(self.rl_system.trader.device), + 'running': self.rl_system.trader.running + } + + return web.json_response(status) + + except Exception as e: + logger.error(f"Error getting RL status: {e}") + return web.json_response({ + 'status': 'error', + 'error': str(e) + }, status=500) + + async def _broadcast_cob_update(self, symbol: str, data: Dict): + """Enhanced COB update broadcast with RL predictions""" + try: + # Get RL predictions if available + rl_data = {} + if self.rl_system: + rl_predictions = self.rl_system.get_rl_predictions(symbol) + rl_data = { + 'rl_predictions': rl_predictions.get('recent_predictions', [])[-10:], # Last 10 + 'prediction_count': rl_predictions.get('total_predictions', 0) + } + + # Enhanced data with RL predictions + enhanced_data = { + **data, + 'rl_data': rl_data + } + + # Broadcast to all WebSocket connections + message = json.dumps({ + 'type': 'cob_update', + 'symbol': symbol, + 'timestamp': datetime.now().isoformat(), + 'data': enhanced_data + }, default=str) + + # Send to all connected clients + disconnected = [] + for ws in self.websocket_connections: + try: + await ws.send_str(message) + except Exception as e: + logger.debug(f"WebSocket send failed: {e}") + disconnected.append(ws) + + # Remove disconnected clients + for ws in disconnected: + self.websocket_connections.discard(ws) + + except Exception as e: + logger.error(f"Error broadcasting enhanced COB update: {e}") + +async def main(): + """Main entry point for integrated RL COB system""" + # Create logs directory + os.makedirs('logs', exist_ok=True) + + # Initialize and start integrated system + system = IntegratedRLCOBSystem() + + try: + await system.start() + except KeyboardInterrupt: + logger.info("Received keyboard interrupt") + except Exception as e: + logger.error(f"System error: {e}") + raise + finally: + await system.stop() + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/web/enhanced_cob_dashboard.html b/web/enhanced_cob_dashboard.html new file mode 100644 index 0000000000000000000000000000000000000000..04f29eb749facedd018170654db25b7e7295f179 GIT binary patch literal 3298 zcmchaZA%+L6ov0+q5olMIu)9XB_}AM$XC~vu zWZgz6Maa(1%zZoO%-!+#ua-TxUHfQlJG9TXZ*MHJQ!DJ;&TPx(tabkuG1IZWo!F)| ztjDNvzoW-W;@%?c_1D5f4?i8oU3_#bv)s+d`V-OKTGL*Sd{kNWv5^n$A6m-j$_93U zpMn{AyzdHrhF6}u9~r@ST1%c{$>IkvKhWr{`0zLW@Otmf>}NU>Hdw&kg;w6}0O zwm_)w>ghY*3H}ajkKZ;Z?(jS$L!qH4C%o_SmSb^5zCs}Znap5|$0_lHid&AY z#BL@ut*nNQmx48~mkCzFUPXCZ8=L3;!X% zIhLW`fn!2lqU;ula+KI^Lqi(^YHUG4NiAJeM{60)wvQpEWlowOF$@LPZy=> zE1(`96HmSEjmsd+?x2kKcu^(~)wzI$&b~TgX7A|CA6%#21oK8kU)G8Acn-0MIEkUR z?^og-5!Zi#PFUBe@SMrY_mUgpR8=-zt9rWkSALmN`|0vHa+(y^<+rq`z$-AMFNf;5 z=BZkrj$^gzg{Nnr4Ku#-YvKs`)fyg!{}p+ZCun|@pZeZyMI}K2I`sprb(R~jE;f6N z7He$$x1{52>U@V7y44)v^$hhW&R6?Z>p$3qIQ~`>Y-foWJd}}reNvgfvD8->?87r; z@|5g@t!^c{ljwwqRh`Rhg!c>AJrla4Z(SMI8{b#Id-#l@ogjIU^ z>S(lPYi_iR=2qwrjT**7Eq5?CeX`2cI@MgDvv7Vrm$NXbE}Ubj(~!fR`i592sQQv| zUBr;rIS5GC`mXA^?L79}2{~ZSXC<$?KS-ss`7Lj?v)2=H^7oTlV(Mg^jPfe%3vc4{ lVK$N7)!-g{*6pcUyPNnbRNYxMCk2O_n!m?||6ZPv)gLn=&+q^M literal 0 HcmV?d00001 From 0c4dc8269c86bc9599c82de95b0a5c11908bad65 Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Tue, 24 Jun 2025 16:56:02 +0300 Subject: [PATCH 2/2] cob dash --- core/config.py | 22 +++++++++++++++++++--- web/enhanced_cob_dashboard.html | Bin 3298 -> 14896 bytes 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/core/config.py b/core/config.py index f86b26f..a95d8f0 100644 --- a/core/config.py +++ b/core/config.py @@ -1,9 +1,7 @@ -""" Central Configuration Management This module handles all configuration for the trading system. It loads settings from config.yaml and provides easy access to all components. -""" import os import yaml @@ -236,6 +234,15 @@ def get_config(config_path: str = "config.yaml") -> Config: _config_instance = Config(config_path) return _config_instance +def load_config(config_path: str = "config.yaml") -> Dict[str, Any]: + """Load configuration from YAML file""" + try: + config = get_config(config_path) + return config._config + except Exception as e: + logger.error(f"Error loading configuration: {e}") + return {} + def setup_logging(config: Optional[Config] = None): """Setup logging based on configuration""" if config is None: @@ -257,4 +264,13 @@ def setup_logging(config: Optional[Config] = None): ] ) - logger.info("Logging configured successfully") \ No newline at end of file + logger.info("Logging configured successfully") + +def load_config(config_path: str = "config.yaml") -> Dict[str, Any]: + """Load configuration from YAML file""" + try: + config = get_config(config_path) + return config._config + except Exception as e: + logger.error(f"Error loading configuration: {e}") + return {} diff --git a/web/enhanced_cob_dashboard.html b/web/enhanced_cob_dashboard.html index 04f29eb749facedd018170654db25b7e7295f179..712bffbd7eca98caa71a6b4da38c9db64b62c52b 100644 GIT binary patch literal 14896 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 literal 3298 zcmchaZA%+L6ov0+q5olMIu)9XB_}AM$XC~vu zWZgz6Maa(1%zZoO%-!+#ua-TxUHfQlJG9TXZ*MHJQ!DJ;&TPx(tabkuG1IZWo!F)| ztjDNvzoW-W;@%?c_1D5f4?i8oU3_#bv)s+d`V-OKTGL*Sd{kNWv5^n$A6m-j$_93U zpMn{AyzdHrhF6}u9~r@ST1%c{$>IkvKhWr{`0zLW@Otmf>}NU>Hdw&kg;w6}0O zwm_)w>ghY*3H}ajkKZ;Z?(jS$L!qH4C%o_SmSb^5zCs}Znap5|$0_lHid&AY z#BL@ut*nNQmx48~mkCzFUPXCZ8=L3;!X% zIhLW`fn!2lqU;ula+KI^Lqi(^YHUG4NiAJeM{60)wvQpEWlowOF$@LPZy=> zE1(`96HmSEjmsd+?x2kKcu^(~)wzI$&b~TgX7A|CA6%#21oK8kU)G8Acn-0MIEkUR z?^og-5!Zi#PFUBe@SMrY_mUgpR8=-zt9rWkSALmN`|0vHa+(y^<+rq`z$-AMFNf;5 z=BZkrj$^gzg{Nnr4Ku#-YvKs`)fyg!{}p+ZCun|@pZeZyMI}K2I`sprb(R~jE;f6N z7He$$x1{52>U@V7y44)v^$hhW&R6?Z>p$3qIQ~`>Y-foWJd}}reNvgfvD8->?87r; z@|5g@t!^c{ljwwqRh`Rhg!c>AJrla4Z(SMI8{b#Id-#l@ogjIU^ z>S(lPYi_iR=2qwrjT**7Eq5?CeX`2cI@MgDvv7Vrm$NXbE}Ubj(~!fR`i592sQQv| zUBr;rIS5GC`mXA^?L79}2{~ZSXC<$?KS-ss`7Lj?v)2=H^7oTlV(Mg^jPfe%3vc4{ lVK$N7)!-g{*6pcUyPNnbRNYxMCk2O_n!m?||6ZPv)gLn=&+q^M