""" Bot Management Integration for Chart Signal Layers This module provides integration points between the signal layer system and the bot management system, including data fetching utilities, bot filtering, and integration helpers. """ import pandas as pd from typing import Dict, Any, Optional, List, Union, Tuple from dataclasses import dataclass from datetime import datetime, timedelta from decimal import Decimal from database.connection import get_session from database.models import Bot, Signal, Trade, BotPerformance from database.operations import DatabaseOperationError from utils.logger import get_logger # Initialize logger logger = get_logger() @dataclass class BotFilterConfig: """Configuration for filtering bot data for chart layers""" bot_ids: Optional[List[int]] = None # Specific bot IDs to include bot_names: Optional[List[str]] = None # Specific bot names to include strategies: Optional[List[str]] = None # Specific strategies to include symbols: Optional[List[str]] = None # Specific symbols to include statuses: Optional[List[str]] = None # Bot statuses to include date_range: Optional[Tuple[datetime, datetime]] = None # Date range filter active_only: bool = False # Only include active bots def __post_init__(self): if self.statuses is None: self.statuses = ['active', 'inactive', 'paused'] # Exclude 'error' by default class BotDataService: """ Service for fetching bot-related data for chart layers. """ def __init__(self): """Initialize bot data service.""" self.logger = logger def get_bots(self, filter_config: BotFilterConfig = None) -> pd.DataFrame: """ Get bot information based on filter configuration. Args: filter_config: Filter configuration (optional) Returns: DataFrame with bot information """ try: if filter_config is None: filter_config = BotFilterConfig() with get_session() as session: query = session.query(Bot) # Apply filters if filter_config.bot_ids: query = query.filter(Bot.id.in_(filter_config.bot_ids)) if filter_config.bot_names: query = query.filter(Bot.name.in_(filter_config.bot_names)) if filter_config.strategies: query = query.filter(Bot.strategy_name.in_(filter_config.strategies)) if filter_config.symbols: query = query.filter(Bot.symbol.in_(filter_config.symbols)) if filter_config.statuses: query = query.filter(Bot.status.in_(filter_config.statuses)) if filter_config.active_only: query = query.filter(Bot.status == 'active') # Execute query bots = query.all() # Convert to DataFrame bot_data = [] for bot in bots: bot_data.append({ 'id': bot.id, 'name': bot.name, 'strategy_name': bot.strategy_name, 'symbol': bot.symbol, 'timeframe': bot.timeframe, 'status': bot.status, 'config_file': bot.config_file, 'virtual_balance': float(bot.virtual_balance) if bot.virtual_balance else 0.0, 'current_balance': float(bot.current_balance) if bot.current_balance else 0.0, 'pnl': float(bot.pnl) if bot.pnl else 0.0, 'is_active': bot.is_active, 'last_heartbeat': bot.last_heartbeat, 'created_at': bot.created_at, 'updated_at': bot.updated_at }) df = pd.DataFrame(bot_data) self.logger.info(f"Bot Integration: Retrieved {len(df)} bots with filters: {filter_config}") return df except Exception as e: self.logger.error(f"Bot Integration: Error retrieving bots: {e}") raise DatabaseOperationError(f"Failed to retrieve bots: {e}") def get_signals_for_bots(self, bot_ids: Union[int, List[int]] = None, start_time: datetime = None, end_time: datetime = None, signal_types: List[str] = None, min_confidence: float = 0.0) -> pd.DataFrame: """ Get signals for specific bots or all bots. Args: bot_ids: Bot ID(s) to fetch signals for (None for all bots) start_time: Start time for signal filtering end_time: End time for signal filtering signal_types: Signal types to include (['buy', 'sell', 'hold']) min_confidence: Minimum confidence threshold Returns: DataFrame with signal data """ try: # Default time range if not provided if end_time is None: end_time = datetime.now() if start_time is None: start_time = end_time - timedelta(days=7) # Last 7 days by default # Normalize bot_ids to list if isinstance(bot_ids, int): bot_ids = [bot_ids] with get_session() as session: query = session.query(Signal) # Apply filters if bot_ids is not None: query = query.filter(Signal.bot_id.in_(bot_ids)) query = query.filter( Signal.timestamp >= start_time, Signal.timestamp <= end_time ) if signal_types: query = query.filter(Signal.signal_type.in_(signal_types)) if min_confidence > 0: query = query.filter(Signal.confidence >= min_confidence) # Order by timestamp query = query.order_by(Signal.timestamp.asc()) # Execute query signals = query.all() # Convert to DataFrame signal_data = [] for signal in signals: signal_data.append({ 'id': signal.id, 'bot_id': signal.bot_id, 'timestamp': signal.timestamp, 'signal_type': signal.signal_type, 'price': float(signal.price) if signal.price else None, 'confidence': float(signal.confidence) if signal.confidence else None, 'indicators': signal.indicators, # JSONB data 'created_at': signal.created_at }) df = pd.DataFrame(signal_data) self.logger.info(f"Bot Integration: Retrieved {len(df)} signals for bots: {bot_ids}") return df except Exception as e: self.logger.error(f"Bot Integration: Error retrieving signals: {e}") raise DatabaseOperationError(f"Failed to retrieve signals: {e}") def get_trades_for_bots(self, bot_ids: Union[int, List[int]] = None, start_time: datetime = None, end_time: datetime = None, sides: List[str] = None) -> pd.DataFrame: """ Get trades for specific bots or all bots. Args: bot_ids: Bot ID(s) to fetch trades for (None for all bots) start_time: Start time for trade filtering end_time: End time for trade filtering sides: Trade sides to include (['buy', 'sell']) Returns: DataFrame with trade data """ try: # Default time range if not provided if end_time is None: end_time = datetime.now() if start_time is None: start_time = end_time - timedelta(days=7) # Last 7 days by default # Normalize bot_ids to list if isinstance(bot_ids, int): bot_ids = [bot_ids] with get_session() as session: query = session.query(Trade) # Apply filters if bot_ids is not None: query = query.filter(Trade.bot_id.in_(bot_ids)) query = query.filter( Trade.timestamp >= start_time, Trade.timestamp <= end_time ) if sides: query = query.filter(Trade.side.in_(sides)) # Order by timestamp query = query.order_by(Trade.timestamp.asc()) # Execute query trades = query.all() # Convert to DataFrame trade_data = [] for trade in trades: trade_data.append({ 'id': trade.id, 'bot_id': trade.bot_id, 'signal_id': trade.signal_id, 'timestamp': trade.timestamp, 'side': trade.side, 'price': float(trade.price), 'quantity': float(trade.quantity), 'fees': float(trade.fees), 'pnl': float(trade.pnl) if trade.pnl else None, 'balance_after': float(trade.balance_after) if trade.balance_after else None, 'trade_value': float(trade.trade_value), 'net_pnl': float(trade.net_pnl), 'created_at': trade.created_at }) df = pd.DataFrame(trade_data) self.logger.info(f"Bot Integration: Retrieved {len(df)} trades for bots: {bot_ids}") return df except Exception as e: self.logger.error(f"Bot Integration: Error retrieving trades: {e}") raise DatabaseOperationError(f"Failed to retrieve trades: {e}") def get_bot_performance(self, bot_ids: Union[int, List[int]] = None, start_time: datetime = None, end_time: datetime = None) -> pd.DataFrame: """ Get performance data for specific bots. Args: bot_ids: Bot ID(s) to fetch performance for (None for all bots) start_time: Start time for performance filtering end_time: End time for performance filtering Returns: DataFrame with performance data """ try: # Default time range if not provided if end_time is None: end_time = datetime.now() if start_time is None: start_time = end_time - timedelta(days=30) # Last 30 days by default # Normalize bot_ids to list if isinstance(bot_ids, int): bot_ids = [bot_ids] with get_session() as session: query = session.query(BotPerformance) # Apply filters if bot_ids is not None: query = query.filter(BotPerformance.bot_id.in_(bot_ids)) query = query.filter( BotPerformance.timestamp >= start_time, BotPerformance.timestamp <= end_time ) # Order by timestamp query = query.order_by(BotPerformance.timestamp.asc()) # Execute query performance_records = query.all() # Convert to DataFrame performance_data = [] for perf in performance_records: performance_data.append({ 'id': perf.id, 'bot_id': perf.bot_id, 'timestamp': perf.timestamp, 'total_value': float(perf.total_value), 'cash_balance': float(perf.cash_balance), 'crypto_balance': float(perf.crypto_balance), 'total_trades': perf.total_trades, 'winning_trades': perf.winning_trades, 'total_fees': float(perf.total_fees), 'win_rate': perf.win_rate, 'portfolio_allocation': perf.portfolio_allocation, 'created_at': perf.created_at }) df = pd.DataFrame(performance_data) self.logger.info(f"Bot Integration: Retrieved {len(df)} performance records for bots: {bot_ids}") return df except Exception as e: self.logger.error(f"Bot Integration: Error retrieving bot performance: {e}") raise DatabaseOperationError(f"Failed to retrieve bot performance: {e}") class BotSignalLayerIntegration: """ Integration utilities for signal layers with bot management system. """ def __init__(self): """Initialize bot signal layer integration.""" self.data_service = BotDataService() self.logger = logger def get_signals_for_chart(self, symbol: str, timeframe: str = None, bot_filter: BotFilterConfig = None, time_range: Tuple[datetime, datetime] = None, signal_types: List[str] = None, min_confidence: float = 0.0) -> pd.DataFrame: """ Get signals filtered by chart context (symbol, timeframe) and bot criteria. Args: symbol: Trading symbol for the chart timeframe: Chart timeframe (optional) bot_filter: Bot filtering configuration time_range: (start_time, end_time) tuple signal_types: Signal types to include min_confidence: Minimum confidence threshold Returns: DataFrame with signals ready for chart rendering """ try: # Get relevant bots for this symbol/timeframe if bot_filter is None: bot_filter = BotFilterConfig() # Add symbol filter if bot_filter.symbols is None: bot_filter.symbols = [symbol] elif symbol not in bot_filter.symbols: bot_filter.symbols.append(symbol) # Get bots matching criteria bots_df = self.data_service.get_bots(bot_filter) if bots_df.empty: self.logger.info(f"No bots found for symbol {symbol}") return pd.DataFrame() bot_ids = bots_df['id'].tolist() # Get time range start_time, end_time = time_range if time_range else (None, None) # Get signals for these bots signals_df = self.data_service.get_signals_for_bots( bot_ids=bot_ids, start_time=start_time, end_time=end_time, signal_types=signal_types, min_confidence=min_confidence ) # Enrich signals with bot information if not signals_df.empty: signals_df = signals_df.merge( bots_df[['id', 'name', 'strategy_name', 'status']], left_on='bot_id', right_on='id', suffixes=('', '_bot') ) # Add metadata fields for chart rendering signals_df['bot_name'] = signals_df['name'] signals_df['strategy'] = signals_df['strategy_name'] signals_df['bot_status'] = signals_df['status'] # Clean up duplicate columns signals_df = signals_df.drop(['id_bot', 'name', 'strategy_name', 'status'], axis=1) self.logger.info(f"Bot Integration: Retrieved {len(signals_df)} signals for chart {symbol} from {len(bot_ids)} bots") return signals_df except Exception as e: self.logger.error(f"Bot Integration: Error getting signals for chart: {e}") return pd.DataFrame() def get_trades_for_chart(self, symbol: str, timeframe: str = None, bot_filter: BotFilterConfig = None, time_range: Tuple[datetime, datetime] = None, sides: List[str] = None) -> pd.DataFrame: """ Get trades filtered by chart context (symbol, timeframe) and bot criteria. Args: symbol: Trading symbol for the chart timeframe: Chart timeframe (optional) bot_filter: Bot filtering configuration time_range: (start_time, end_time) tuple sides: Trade sides to include Returns: DataFrame with trades ready for chart rendering """ try: # Get relevant bots for this symbol/timeframe if bot_filter is None: bot_filter = BotFilterConfig() # Add symbol filter if bot_filter.symbols is None: bot_filter.symbols = [symbol] elif symbol not in bot_filter.symbols: bot_filter.symbols.append(symbol) # Get bots matching criteria bots_df = self.data_service.get_bots(bot_filter) if bots_df.empty: self.logger.info(f"No bots found for symbol {symbol}") return pd.DataFrame() bot_ids = bots_df['id'].tolist() # Get time range start_time, end_time = time_range if time_range else (None, None) # Get trades for these bots trades_df = self.data_service.get_trades_for_bots( bot_ids=bot_ids, start_time=start_time, end_time=end_time, sides=sides ) # Enrich trades with bot information if not trades_df.empty: trades_df = trades_df.merge( bots_df[['id', 'name', 'strategy_name', 'status']], left_on='bot_id', right_on='id', suffixes=('', '_bot') ) # Add metadata fields for chart rendering trades_df['bot_name'] = trades_df['name'] trades_df['strategy'] = trades_df['strategy_name'] trades_df['bot_status'] = trades_df['status'] # Clean up duplicate columns trades_df = trades_df.drop(['id_bot', 'name', 'strategy_name', 'status'], axis=1) self.logger.info(f"Bot Integration: Retrieved {len(trades_df)} trades for chart {symbol} from {len(bot_ids)} bots") return trades_df except Exception as e: self.logger.error(f"Bot Integration: Error getting trades for chart: {e}") return pd.DataFrame() def get_bot_summary_stats(self, bot_ids: List[int] = None) -> Dict[str, Any]: """ Get summary statistics for bots. Args: bot_ids: Specific bot IDs (None for all bots) Returns: Dictionary with summary statistics """ try: # Get bots bot_filter = BotFilterConfig(bot_ids=bot_ids) if bot_ids else BotFilterConfig() bots_df = self.data_service.get_bots(bot_filter) if bots_df.empty: return { 'total_bots': 0, 'active_bots': 0, 'total_balance': 0.0, 'total_pnl': 0.0, 'strategies': [], 'symbols': [] } # Calculate statistics stats = { 'total_bots': len(bots_df), 'active_bots': len(bots_df[bots_df['status'] == 'active']), 'inactive_bots': len(bots_df[bots_df['status'] == 'inactive']), 'paused_bots': len(bots_df[bots_df['status'] == 'paused']), 'error_bots': len(bots_df[bots_df['status'] == 'error']), 'total_virtual_balance': bots_df['virtual_balance'].sum(), 'total_current_balance': bots_df['current_balance'].sum(), 'total_pnl': bots_df['pnl'].sum(), 'average_pnl': bots_df['pnl'].mean(), 'best_performing_bot': None, 'worst_performing_bot': None, 'strategies': bots_df['strategy_name'].unique().tolist(), 'symbols': bots_df['symbol'].unique().tolist(), 'timeframes': bots_df['timeframe'].unique().tolist() } # Get best and worst performing bots if not bots_df.empty: best_bot = bots_df.loc[bots_df['pnl'].idxmax()] worst_bot = bots_df.loc[bots_df['pnl'].idxmin()] stats['best_performing_bot'] = { 'id': best_bot['id'], 'name': best_bot['name'], 'pnl': best_bot['pnl'] } stats['worst_performing_bot'] = { 'id': worst_bot['id'], 'name': worst_bot['name'], 'pnl': worst_bot['pnl'] } return stats except Exception as e: self.logger.error(f"Bot Integration: Error getting bot summary stats: {e}") return {} # Global instances for easy access bot_data_service = BotDataService() bot_integration = BotSignalLayerIntegration() # Convenience functions for common use cases def get_active_bot_signals(symbol: str, timeframe: str = None, days_back: int = 7, signal_types: List[str] = None, min_confidence: float = 0.3) -> pd.DataFrame: """ Get signals from active bots for a specific symbol. Args: symbol: Trading symbol timeframe: Chart timeframe (optional) days_back: Number of days to look back signal_types: Signal types to include min_confidence: Minimum confidence threshold Returns: DataFrame with signals from active bots """ end_time = datetime.now() start_time = end_time - timedelta(days=days_back) bot_filter = BotFilterConfig( symbols=[symbol], active_only=True ) return bot_integration.get_signals_for_chart( symbol=symbol, timeframe=timeframe, bot_filter=bot_filter, time_range=(start_time, end_time), signal_types=signal_types, min_confidence=min_confidence ) def get_active_bot_trades(symbol: str, timeframe: str = None, days_back: int = 7, sides: List[str] = None) -> pd.DataFrame: """ Get trades from active bots for a specific symbol. Args: symbol: Trading symbol timeframe: Chart timeframe (optional) days_back: Number of days to look back sides: Trade sides to include Returns: DataFrame with trades from active bots """ end_time = datetime.now() start_time = end_time - timedelta(days=days_back) bot_filter = BotFilterConfig( symbols=[symbol], active_only=True ) return bot_integration.get_trades_for_chart( symbol=symbol, timeframe=timeframe, bot_filter=bot_filter, time_range=(start_time, end_time), sides=sides ) def get_bot_signals_by_strategy(strategy_name: str, symbol: str = None, days_back: int = 7, signal_types: List[str] = None) -> pd.DataFrame: """ Get signals from bots using a specific strategy. Args: strategy_name: Strategy name to filter by symbol: Trading symbol (optional) days_back: Number of days to look back signal_types: Signal types to include Returns: DataFrame with signals from strategy bots """ end_time = datetime.now() start_time = end_time - timedelta(days=days_back) bot_filter = BotFilterConfig( strategies=[strategy_name], symbols=[symbol] if symbol else None ) # Get bots for this strategy bots_df = bot_data_service.get_bots(bot_filter) if bots_df.empty: return pd.DataFrame() bot_ids = bots_df['id'].tolist() return bot_data_service.get_signals_for_bots( bot_ids=bot_ids, start_time=start_time, end_time=end_time, signal_types=signal_types ) def get_bot_performance_summary(bot_id: int = None, days_back: int = 30) -> Dict[str, Any]: """ Get performance summary for a specific bot or all bots. Args: bot_id: Specific bot ID (None for all bots) days_back: Number of days to analyze Returns: Dictionary with performance summary """ end_time = datetime.now() start_time = end_time - timedelta(days=days_back) # Get bot summary stats bot_ids = [bot_id] if bot_id else None bot_stats = bot_integration.get_bot_summary_stats(bot_ids) # Get signals and trades for performance analysis signals_df = bot_data_service.get_signals_for_bots( bot_ids=bot_ids, start_time=start_time, end_time=end_time ) trades_df = bot_data_service.get_trades_for_bots( bot_ids=bot_ids, start_time=start_time, end_time=end_time ) # Calculate additional performance metrics performance = { 'bot_stats': bot_stats, 'signal_count': len(signals_df), 'trade_count': len(trades_df), 'signals_by_type': signals_df['signal_type'].value_counts().to_dict() if not signals_df.empty else {}, 'trades_by_side': trades_df['side'].value_counts().to_dict() if not trades_df.empty else {}, 'total_trade_volume': trades_df['trade_value'].sum() if not trades_df.empty else 0.0, 'total_fees': trades_df['fees'].sum() if not trades_df.empty else 0.0, 'profitable_trades': len(trades_df[trades_df['pnl'] > 0]) if not trades_df.empty else 0, 'losing_trades': len(trades_df[trades_df['pnl'] < 0]) if not trades_df.empty else 0, 'win_rate': (len(trades_df[trades_df['pnl'] > 0]) / len(trades_df) * 100) if not trades_df.empty else 0.0, 'time_range': { 'start': start_time.isoformat(), 'end': end_time.isoformat(), 'days': days_back } } return performance