""" Trading Signal Chart Layers This module implements signal overlay layers for displaying buy/sell/hold signals generated by trading strategies on charts. Integrates with the database signal model. """ import pandas as pd import plotly.graph_objects as go from typing import Dict, Any, Optional, List, Union, Tuple from dataclasses import dataclass from decimal import Decimal from datetime import datetime from ..error_handling import ( ChartErrorHandler, ChartError, ErrorSeverity, DataValidationError, create_error_annotation, get_error_message ) from .base import BaseLayer, LayerConfig from utils.logger import get_logger # Initialize logger logger = get_logger() @dataclass class SignalLayerConfig(LayerConfig): """Extended configuration for signal layers""" signal_types: List[str] = None # ['buy', 'sell', 'hold'] or subset confidence_threshold: float = 0.0 # Minimum confidence to display (0.0-1.0) show_confidence: bool = True # Show confidence in marker hover text marker_size: int = 12 # Size of signal markers show_price_labels: bool = True # Show price labels on signals bot_id: Optional[int] = None # Filter signals by specific bot def __post_init__(self): super().__post_init__() if self.signal_types is None: self.signal_types = ['buy', 'sell'] # Default to buy/sell only @dataclass class TradeLayerConfig(LayerConfig): """Extended configuration for trade visualization layers""" show_pnl: bool = True # Show profit/loss information show_trade_lines: bool = True # Draw lines connecting entry/exit points show_quantity: bool = True # Show trade quantity in hover show_fees: bool = True # Show fees in hover min_pnl_display: Optional[float] = None # Minimum P&L to display trade bot_id: Optional[int] = None # Filter trades by specific bot trade_marker_size: int = 14 # Size of trade markers (slightly larger than signals) def __post_init__(self): super().__post_init__() class BaseSignalLayer(BaseLayer): """ Base class for all signal layers with database integration. """ def __init__(self, config: SignalLayerConfig): """ Initialize base signal layer. Args: config: Signal layer configuration """ super().__init__(config) self.signal_data = None # Signal styling defaults self.signal_colors = { 'buy': '#4caf50', # Green 'sell': '#f44336', # Red 'hold': '#ff9800' # Orange } self.signal_symbols = { 'buy': 'triangle-up', 'sell': 'triangle-down', 'hold': 'circle' } def validate_signal_data(self, signals: Union[pd.DataFrame, List[Dict[str, Any]]]) -> bool: """ Validate signal data structure and requirements. Args: signals: Signal data from database or API Returns: True if data is valid for signal rendering """ try: # Clear previous errors self.error_handler.clear_errors() # Convert to DataFrame if needed if isinstance(signals, list): if not signals: # Empty signals are valid (no signals to show) return True df = pd.DataFrame(signals) else: df = signals.copy() # Check required columns for signals required_columns = ['timestamp', 'signal_type', 'price', 'confidence'] missing_columns = [col for col in required_columns if col not in df.columns] if missing_columns: error = ChartError( code='MISSING_SIGNAL_COLUMNS', message=f'Missing signal columns: {missing_columns}', severity=ErrorSeverity.ERROR, context={ 'missing_columns': missing_columns, 'available_columns': list(df.columns), 'layer_type': 'signal' }, recovery_suggestion=f'Ensure signal data contains: {required_columns}' ) self.error_handler.errors.append(error) return False # Validate signal types valid_signal_types = {'buy', 'sell', 'hold'} invalid_signals = df[~df['signal_type'].isin(valid_signal_types)] if not invalid_signals.empty: error = ChartError( code='INVALID_SIGNAL_TYPES', message=f'Invalid signal types found: {set(invalid_signals["signal_type"].unique())}', severity=ErrorSeverity.WARNING, context={ 'invalid_types': list(invalid_signals['signal_type'].unique()), 'valid_types': list(valid_signal_types) }, recovery_suggestion='Signal types must be: buy, sell, or hold' ) self.error_handler.warnings.append(error) # Validate confidence range invalid_confidence = df[(df['confidence'] < 0) | (df['confidence'] > 1)] if not invalid_confidence.empty: error = ChartError( code='INVALID_CONFIDENCE_RANGE', message=f'Confidence values must be between 0.0 and 1.0', severity=ErrorSeverity.WARNING, context={ 'invalid_count': len(invalid_confidence), 'min_found': float(df['confidence'].min()), 'max_found': float(df['confidence'].max()) }, recovery_suggestion='Confidence values will be clamped to 0.0-1.0 range' ) self.error_handler.warnings.append(error) return True except Exception as e: self.logger.error(f"Chart Signals: Error validating signal data: {e}") error = ChartError( code='SIGNAL_VALIDATION_ERROR', message=f'Signal validation failed: {str(e)}', severity=ErrorSeverity.ERROR, context={'exception': str(e), 'layer_type': 'signal'} ) self.error_handler.errors.append(error) return False def filter_signals_by_config(self, signals: pd.DataFrame) -> pd.DataFrame: """ Filter signals based on layer configuration. Args: signals: Raw signal data Returns: Filtered signal data """ try: if signals.empty: return signals filtered = signals.copy() # Filter by signal types if self.config.signal_types: filtered = filtered[filtered['signal_type'].isin(self.config.signal_types)] # Filter by confidence threshold if self.config.confidence_threshold > 0: filtered = filtered[filtered['confidence'] >= self.config.confidence_threshold] # Filter by bot_id if specified if self.config.bot_id is not None: if 'bot_id' in filtered.columns: filtered = filtered[filtered['bot_id'] == self.config.bot_id] else: self.logger.warning(f"bot_id filter requested but no bot_id column in signal data") # Clamp confidence values to valid range filtered['confidence'] = filtered['confidence'].clip(0.0, 1.0) self.logger.info(f"Chart Signals: Filtered signals: {len(signals)} -> {len(filtered)} signals") return filtered except Exception as e: self.logger.error(f"Chart Signals: Error filtering signals: {e}") return pd.DataFrame() # Return empty DataFrame on error def create_signal_traces(self, signals: pd.DataFrame) -> List[go.Scatter]: """ Create Plotly traces for signal markers. Args: signals: Filtered signal data Returns: List of Plotly traces for each signal type """ traces = [] try: if signals.empty: return traces # Group signals by type for signal_type in signals['signal_type'].unique(): signal_group = signals[signals['signal_type'] == signal_type] if signal_group.empty: continue # Prepare hover text hover_text = [] for _, signal in signal_group.iterrows(): hover_parts = [ f"Signal: {signal['signal_type'].upper()}", f"Price: ${signal['price']:.4f}", f"Time: {signal['timestamp']}" ] if self.config.show_confidence: confidence_pct = signal['confidence'] * 100 hover_parts.append(f"Confidence: {confidence_pct:.1f}%") if 'bot_id' in signal_group.columns: hover_parts.append(f"Bot ID: {signal['bot_id']}") hover_text.append("
".join(hover_parts)) # Create trace for this signal type trace = go.Scatter( x=signal_group['timestamp'], y=signal_group['price'], mode='markers', marker=dict( symbol=self.signal_symbols.get(signal_type, 'circle'), size=self.config.marker_size, color=self.signal_colors.get(signal_type, '#666666'), line=dict(width=1, color='white'), opacity=0.8 ), name=f"{signal_type.upper()} Signals", text=hover_text, hoverinfo='text', showlegend=True, legendgroup=f"signals_{signal_type}" ) traces.append(trace) # Add price labels if enabled if self.config.show_price_labels: price_trace = go.Scatter( x=signal_group['timestamp'], y=signal_group['price'], mode='text', text=[f"${price:.2f}" for price in signal_group['price']], textposition='top center' if signal_type == 'buy' else 'bottom center', textfont=dict( size=8, color=self.signal_colors.get(signal_type, '#666666') ), showlegend=False, hoverinfo='skip' ) traces.append(price_trace) return traces except Exception as e: self.logger.error(f"Chart Signals: Error creating signal traces: {e}") # Return error trace error_trace = self.create_error_trace(f"Error displaying signals: {str(e)}") return [error_trace] def is_enabled(self) -> bool: """Check if the signal layer is enabled.""" return self.config.enabled def is_overlay(self) -> bool: """Signal layers are always overlays on the main chart.""" return True def get_subplot_row(self) -> Optional[int]: """Signal layers appear on main chart (no subplot).""" return None class BaseTradeLayer(BaseLayer): """ Base class for trade visualization layers with database integration. """ def __init__(self, config: TradeLayerConfig): """ Initialize base trade layer. Args: config: Trade layer configuration """ super().__init__(config) self.trade_data = None # Trade styling defaults self.trade_colors = { 'buy': '#2e7d32', # Darker green for trades 'sell': '#c62828', # Darker red for trades 'profit': '#4caf50', # Green for profitable trades 'loss': '#f44336' # Red for losing trades } self.trade_symbols = { 'buy': 'triangle-up', 'sell': 'triangle-down' } def validate_trade_data(self, trades: Union[pd.DataFrame, List[Dict[str, Any]]]) -> bool: """ Validate trade data structure and requirements. Args: trades: Trade data from database Returns: True if data is valid for trade rendering """ try: # Clear previous errors self.error_handler.clear_errors() # Convert to DataFrame if needed if isinstance(trades, list): if not trades: # Empty trades are valid (no trades to show) return True df = pd.DataFrame(trades) else: df = trades.copy() # Check required columns for trades required_columns = ['timestamp', 'side', 'price', 'quantity'] missing_columns = [col for col in required_columns if col not in df.columns] if missing_columns: error = ChartError( code='MISSING_TRADE_COLUMNS', message=f'Missing trade columns: {missing_columns}', severity=ErrorSeverity.ERROR, context={ 'missing_columns': missing_columns, 'available_columns': list(df.columns), 'layer_type': 'trade' }, recovery_suggestion=f'Ensure trade data contains: {required_columns}' ) self.error_handler.errors.append(error) return False # Validate trade sides valid_sides = {'buy', 'sell'} invalid_trades = df[~df['side'].isin(valid_sides)] if not invalid_trades.empty: error = ChartError( code='INVALID_TRADE_SIDES', message=f'Invalid trade sides found: {set(invalid_trades["side"].unique())}', severity=ErrorSeverity.WARNING, context={ 'invalid_sides': list(invalid_trades['side'].unique()), 'valid_sides': list(valid_sides) }, recovery_suggestion='Trade sides must be: buy or sell' ) self.error_handler.warnings.append(error) # Validate positive prices and quantities invalid_prices = df[df['price'] <= 0] invalid_quantities = df[df['quantity'] <= 0] if not invalid_prices.empty: error = ChartError( code='INVALID_TRADE_PRICES', message=f'Invalid trade prices found (must be > 0)', severity=ErrorSeverity.WARNING, context={'invalid_count': len(invalid_prices)}, recovery_suggestion='Trade prices must be positive values' ) self.error_handler.warnings.append(error) if not invalid_quantities.empty: error = ChartError( code='INVALID_TRADE_QUANTITIES', message=f'Invalid trade quantities found (must be > 0)', severity=ErrorSeverity.WARNING, context={'invalid_count': len(invalid_quantities)}, recovery_suggestion='Trade quantities must be positive values' ) self.error_handler.warnings.append(error) return True except Exception as e: self.logger.error(f"Chart Trade: Error validating trade data: {e}") error = ChartError( code='TRADE_VALIDATION_ERROR', message=f'Trade validation failed: {str(e)}', severity=ErrorSeverity.ERROR, context={'exception': str(e), 'layer_type': 'trade'} ) self.error_handler.errors.append(error) return False def filter_trades_by_config(self, trades: pd.DataFrame) -> pd.DataFrame: """ Filter trades based on layer configuration. Args: trades: Raw trade data Returns: Filtered trade data """ try: if trades.empty: return trades filtered = trades.copy() # Filter by bot_id if specified if self.config.bot_id is not None: if 'bot_id' in filtered.columns: filtered = filtered[filtered['bot_id'] == self.config.bot_id] else: self.logger.warning(f"bot_id filter requested but no bot_id column in trade data") # Filter by minimum P&L if specified if self.config.min_pnl_display is not None and 'pnl' in filtered.columns: # Only show trades with P&L above threshold (absolute value) filtered = filtered[filtered['pnl'].abs() >= self.config.min_pnl_display] self.logger.info(f"Filtered trades: {len(trades)} -> {len(filtered)} trades") return filtered except Exception as e: self.logger.error(f"Chart Trade: Error filtering trades: {e}") return pd.DataFrame() # Return empty DataFrame on error def pair_entry_exit_trades(self, trades: pd.DataFrame) -> List[Dict[str, Any]]: """ Pair buy and sell trades to create entry/exit connections. Args: trades: Filtered trade data Returns: List of trade pairs with entry/exit information """ try: trade_pairs = [] if trades.empty: return trade_pairs # Sort trades by timestamp sorted_trades = trades.sort_values('timestamp').reset_index(drop=True) # Simple FIFO pairing logic position = 0 # Current position (positive = long, negative = short) open_positions = [] # Stack of open positions for _, trade in sorted_trades.iterrows(): trade_dict = trade.to_dict() if trade['side'] == 'buy': # Opening long position or reducing short position if position < 0: # Closing short position(s) remaining_quantity = trade['quantity'] while remaining_quantity > 0 and open_positions: open_trade = open_positions.pop() close_quantity = min(remaining_quantity, open_trade['quantity']) # Create trade pair pnl = (open_trade['price'] - trade['price']) * close_quantity trade_pair = { 'entry_trade': open_trade, 'exit_trade': trade_dict, 'entry_time': open_trade['timestamp'], 'exit_time': trade['timestamp'], 'entry_price': open_trade['price'], 'exit_price': trade['price'], 'quantity': close_quantity, 'pnl': pnl, 'side': 'short', # This was a short position 'duration': trade['timestamp'] - open_trade['timestamp'] } trade_pairs.append(trade_pair) remaining_quantity -= close_quantity open_trade['quantity'] -= close_quantity # If open trade still has quantity, put it back if open_trade['quantity'] > 0: open_positions.append(open_trade) # If there's remaining quantity, it opens a new long position if remaining_quantity > 0: new_trade = trade_dict.copy() new_trade['quantity'] = remaining_quantity open_positions.append(new_trade) position += remaining_quantity else: # Opening new long position open_positions.append(trade_dict) position += trade['quantity'] else: # sell # Opening short position or reducing long position if position > 0: # Closing long position(s) remaining_quantity = trade['quantity'] while remaining_quantity > 0 and open_positions: open_trade = open_positions.pop(0) # FIFO for long positions close_quantity = min(remaining_quantity, open_trade['quantity']) # Create trade pair pnl = (trade['price'] - open_trade['price']) * close_quantity trade_pair = { 'entry_trade': open_trade, 'exit_trade': trade_dict, 'entry_time': open_trade['timestamp'], 'exit_time': trade['timestamp'], 'entry_price': open_trade['price'], 'exit_price': trade['price'], 'quantity': close_quantity, 'pnl': pnl, 'side': 'long', # This was a long position 'duration': trade['timestamp'] - open_trade['timestamp'] } trade_pairs.append(trade_pair) remaining_quantity -= close_quantity open_trade['quantity'] -= close_quantity # If open trade still has quantity, put it back if open_trade['quantity'] > 0: open_positions.insert(0, open_trade) # If there's remaining quantity, it opens a new short position if remaining_quantity > 0: new_trade = trade_dict.copy() new_trade['quantity'] = remaining_quantity open_positions.append(new_trade) position -= remaining_quantity else: # Opening new short position open_positions.append(trade_dict) position -= trade['quantity'] self.logger.info(f"Paired {len(trade_pairs)} trade pairs from {len(sorted_trades)} trades") return trade_pairs except Exception as e: self.logger.error(f"Chart Trade: Error pairing trades: {e}") return [] def is_enabled(self) -> bool: """Check if the trade layer is enabled.""" return self.config.enabled def is_overlay(self) -> bool: """Trade layers are always overlays on the main chart.""" return True def get_subplot_row(self) -> Optional[int]: """Trade layers appear on main chart (no subplot).""" return None class TradingSignalLayer(BaseSignalLayer): """ Main trading signal layer for displaying buy/sell/hold signals from database. """ def __init__(self, config: SignalLayerConfig = None): """ Initialize trading signal layer. Args: config: Signal layer configuration (optional, uses defaults) """ if config is None: config = SignalLayerConfig( name="Trading Signals", enabled=True, signal_types=['buy', 'sell'], confidence_threshold=0.3, # Only show signals with >30% confidence marker_size=10, show_confidence=True, show_price_labels=True ) super().__init__(config) self.logger.info(f"Initialized TradingSignalLayer: {config.name}") def render(self, fig: go.Figure, data: pd.DataFrame, signals: pd.DataFrame = None, **kwargs) -> go.Figure: """ Render signal markers on the chart. Args: fig: Plotly figure to render onto data: Market data (OHLCV format) signals: Signal data from database (optional) **kwargs: Additional rendering parameters Returns: Updated figure with signal overlays """ try: if signals is None or signals.empty: self.logger.info("No signals provided for rendering") return fig # Validate signal data if not self.validate_signal_data(signals): self.logger.warning("Signal data validation failed") # Add error annotation if validation failed error_message = self.error_handler.get_user_friendly_message() fig.add_annotation( text=f"Signal Error: {error_message}", x=0.5, y=0.95, xref="paper", yref="paper", showarrow=False, font=dict(color="red", size=10) ) return fig # Filter signals based on configuration filtered_signals = self.filter_signals_by_config(signals) if filtered_signals.empty: self.logger.info("No signals remain after filtering") return fig # Create signal traces signal_traces = self.create_signal_traces(filtered_signals) # Add traces to figure for trace in signal_traces: fig.add_trace(trace) # Store processed data for potential reuse self.signal_data = filtered_signals self.logger.info(f"Successfully rendered {len(filtered_signals)} signals") return fig except Exception as e: self.logger.error(f"Chart Signals: Error rendering signal layer: {e}") # Add error annotation to chart fig.add_annotation( text=f"Signal Rendering Error: {str(e)}", x=0.5, y=0.9, xref="paper", yref="paper", showarrow=False, font=dict(color="red", size=10) ) return fig class TradeExecutionLayer(BaseTradeLayer): """ Trade execution layer for displaying actual buy/sell trades with entry/exit connections. """ def __init__(self, config: TradeLayerConfig = None): """ Initialize trade execution layer. Args: config: Trade layer configuration (optional, uses defaults) """ if config is None: config = TradeLayerConfig( name="Trade Executions", enabled=True, show_pnl=True, show_trade_lines=True, show_quantity=True, show_fees=True, trade_marker_size=12 ) super().__init__(config) self.logger.info(f"Initialized TradeExecutionLayer: {config.name}") def create_trade_traces(self, trades: pd.DataFrame) -> List[go.Scatter]: """ Create Plotly traces for trade markers and connections. Args: trades: Filtered trade data Returns: List of Plotly traces for trades """ traces = [] try: if trades.empty: return traces # Create trade pairs for entry/exit connections trade_pairs = self.pair_entry_exit_trades(trades) # Create individual trade markers for side in ['buy', 'sell']: side_trades = trades[trades['side'] == side] if side_trades.empty: continue # Prepare hover text hover_text = [] for _, trade in side_trades.iterrows(): hover_parts = [ f"Trade: {trade['side'].upper()}", f"Price: ${trade['price']:.4f}", f"Time: {trade['timestamp']}" ] if self.config.show_quantity: hover_parts.append(f"Quantity: {trade['quantity']:.8f}") if self.config.show_pnl and 'pnl' in trade: pnl_value = trade.get('pnl', 0) if pnl_value != 0: hover_parts.append(f"P&L: ${pnl_value:.4f}") if self.config.show_fees and 'fees' in trade: fees = trade.get('fees', 0) if fees > 0: hover_parts.append(f"Fees: ${fees:.4f}") if 'bot_id' in trade: hover_parts.append(f"Bot ID: {trade['bot_id']}") hover_text.append("
".join(hover_parts)) # Create trace for this trade side trace = go.Scatter( x=side_trades['timestamp'], y=side_trades['price'], mode='markers', marker=dict( symbol=self.trade_symbols.get(side, 'circle'), size=self.config.trade_marker_size, color=self.trade_colors.get(side, '#666666'), line=dict(width=2, color='white'), opacity=0.9 ), name=f"{side.upper()} Trades", text=hover_text, hoverinfo='text', showlegend=True, legendgroup=f"trades_{side}" ) traces.append(trace) # Create entry/exit connection lines if enabled if self.config.show_trade_lines and trade_pairs: for i, pair in enumerate(trade_pairs): # Determine line color based on P&L line_color = self.trade_colors['profit'] if pair['pnl'] >= 0 else self.trade_colors['loss'] # Create connection line line_trace = go.Scatter( x=[pair['entry_time'], pair['exit_time']], y=[pair['entry_price'], pair['exit_price']], mode='lines', line=dict( color=line_color, width=2, dash='solid' if pair['pnl'] >= 0 else 'dash' ), name=f"Trade #{i+1}" if i < 10 else None, # Only show legend for first 10 showlegend=i < 10, legendgroup=f"trade_lines", hovertext=f"P&L: ${pair['pnl']:.4f}
Duration: {pair['duration']}", hoverinfo='text' ) traces.append(line_trace) return traces except Exception as e: self.logger.error(f"Chart Trade: Error creating trade traces: {e}") # Return error trace error_trace = self.create_error_trace(f"Error displaying trades: {str(e)}") return [error_trace] def render(self, fig: go.Figure, data: pd.DataFrame, trades: pd.DataFrame = None, **kwargs) -> go.Figure: """ Render trade execution markers and connections on the chart. Args: fig: Plotly figure to render onto data: Market data (OHLCV format) trades: Trade data from database (optional) **kwargs: Additional rendering parameters Returns: Updated figure with trade overlays """ try: if trades is None or trades.empty: self.logger.info("No trades provided for rendering") return fig # Validate trade data if not self.validate_trade_data(trades): self.logger.warning("Trade data validation failed") # Add error annotation if validation failed error_message = self.error_handler.get_user_friendly_message() fig.add_annotation( text=f"Trade Error: {error_message}", x=0.5, y=0.95, xref="paper", yref="paper", showarrow=False, font=dict(color="red", size=10) ) return fig # Filter trades based on configuration filtered_trades = self.filter_trades_by_config(trades) if filtered_trades.empty: self.logger.info("No trades remain after filtering") return fig # Create trade traces trade_traces = self.create_trade_traces(filtered_trades) # Add traces to figure for trace in trade_traces: fig.add_trace(trace) # Store processed data for potential reuse self.trade_data = filtered_trades self.logger.info(f"Successfully rendered {len(filtered_trades)} trades") return fig except Exception as e: self.logger.error(f"Chart Trade: Error rendering trade layer: {e}") # Add error annotation to chart fig.add_annotation( text=f"Trade Rendering Error: {str(e)}", x=0.5, y=0.9, xref="paper", yref="paper", showarrow=False, font=dict(color="red", size=10) ) return fig # Convenience functions for creating signal layers def create_trading_signal_layer(bot_id: Optional[int] = None, confidence_threshold: float = 0.3, signal_types: List[str] = None, **kwargs) -> TradingSignalLayer: """ Create a trading signal layer with common configurations. Args: bot_id: Filter signals by specific bot (None for all bots) confidence_threshold: Minimum confidence to display signals signal_types: Signal types to display (['buy', 'sell'] by default) **kwargs: Additional configuration options Returns: Configured TradingSignalLayer instance """ if signal_types is None: signal_types = ['buy', 'sell'] config = SignalLayerConfig( name=f"Bot {bot_id} Signals" if bot_id else "Trading Signals", enabled=True, signal_types=signal_types, confidence_threshold=confidence_threshold, bot_id=bot_id, marker_size=kwargs.get('marker_size', 10), show_confidence=kwargs.get('show_confidence', True), show_price_labels=kwargs.get('show_price_labels', True), **{k: v for k, v in kwargs.items() if k not in ['marker_size', 'show_confidence', 'show_price_labels']} ) return TradingSignalLayer(config) def create_buy_signals_only_layer(**kwargs) -> TradingSignalLayer: """Create a signal layer that shows only buy signals.""" return create_trading_signal_layer(signal_types=['buy'], **kwargs) def create_sell_signals_only_layer(**kwargs) -> TradingSignalLayer: """Create a signal layer that shows only sell signals.""" return create_trading_signal_layer(signal_types=['sell'], **kwargs) def create_high_confidence_signals_layer(confidence_threshold: float = 0.7, **kwargs) -> TradingSignalLayer: """Create a signal layer for high-confidence signals only.""" return create_trading_signal_layer( confidence_threshold=confidence_threshold, **kwargs ) # Convenience functions for creating trade layers def create_trade_execution_layer(bot_id: Optional[int] = None, show_pnl: bool = True, show_trade_lines: bool = True, **kwargs) -> TradeExecutionLayer: """ Create a trade execution layer with common configurations. Args: bot_id: Filter trades by specific bot (None for all bots) show_pnl: Show profit/loss information show_trade_lines: Draw lines connecting entry/exit points **kwargs: Additional configuration options Returns: Configured TradeExecutionLayer instance """ config = TradeLayerConfig( name=f"Bot {bot_id} Trades" if bot_id else "Trade Executions", enabled=True, show_pnl=show_pnl, show_trade_lines=show_trade_lines, bot_id=bot_id, show_quantity=kwargs.get('show_quantity', True), show_fees=kwargs.get('show_fees', True), trade_marker_size=kwargs.get('trade_marker_size', 12), min_pnl_display=kwargs.get('min_pnl_display', None), **{k: v for k, v in kwargs.items() if k not in ['show_quantity', 'show_fees', 'trade_marker_size', 'min_pnl_display']} ) return TradeExecutionLayer(config) def create_profitable_trades_only_layer(**kwargs) -> TradeExecutionLayer: """Create a trade layer that shows only profitable trades.""" return create_trade_execution_layer(min_pnl_display=0.01, **kwargs) def create_losing_trades_only_layer(**kwargs) -> TradeExecutionLayer: """Create a trade layer that shows only losing trades (for analysis).""" config = kwargs.copy() config['min_pnl_display'] = -float('inf') # Show all losing trades layer = create_trade_execution_layer(**config) # Override filter to show only losing trades original_filter = layer.filter_trades_by_config def losing_trades_filter(trades): filtered = original_filter(trades) if not filtered.empty and 'pnl' in filtered.columns: filtered = filtered[filtered['pnl'] < 0] return filtered layer.filter_trades_by_config = losing_trades_filter return layer @dataclass class SupportResistanceLayerConfig(LayerConfig): """Extended configuration for support/resistance line layers""" line_types: List[str] = None # ['support', 'resistance', 'trend'] or subset line_width: int = 2 # Width of support/resistance lines line_opacity: float = 0.7 # Opacity of lines show_price_labels: bool = True # Show price labels on lines show_break_points: bool = True # Show where price breaks S/R levels auto_detect: bool = False # Auto-detect S/R levels from price data manual_levels: List[Dict[str, Any]] = None # Manual S/R levels sensitivity: float = 0.02 # Price sensitivity for level detection (2% default) min_touches: int = 2 # Minimum touches required for valid S/R level def __post_init__(self): super().__post_init__() if self.line_types is None: self.line_types = ['support', 'resistance'] if self.manual_levels is None: self.manual_levels = [] class BaseSupportResistanceLayer(BaseLayer): """ Base class for support/resistance line layers. """ def __init__(self, config: SupportResistanceLayerConfig): """ Initialize base support/resistance layer. Args: config: Support/resistance layer configuration """ super().__init__(config) self.sr_data = None # Support/resistance styling defaults self.sr_colors = { 'support': '#4caf50', # Green for support 'resistance': '#f44336', # Red for resistance 'trend': '#2196f3', # Blue for trend lines 'broken_support': '#ff9800', # Orange for broken support (becomes resistance) 'broken_resistance': '#9c27b0' # Purple for broken resistance (becomes support) } self.line_styles = { 'support': 'solid', 'resistance': 'solid', 'trend': 'dash', 'broken_support': 'dot', 'broken_resistance': 'dot' } def validate_sr_data(self, sr_levels: Union[pd.DataFrame, List[Dict[str, Any]]]) -> bool: """ Validate support/resistance data structure. Args: sr_levels: Support/resistance level data Returns: True if data is valid for S/R rendering """ try: # Clear previous errors self.error_handler.clear_errors() # Convert to DataFrame if needed if isinstance(sr_levels, list): if not sr_levels: # Empty levels are valid (no S/R to show) return True df = pd.DataFrame(sr_levels) else: df = sr_levels.copy() # Check required columns for S/R levels required_columns = ['price_level', 'line_type'] missing_columns = [col for col in required_columns if col not in df.columns] if missing_columns: error = ChartError( code='MISSING_SR_COLUMNS', message=f'Missing S/R columns: {missing_columns}', severity=ErrorSeverity.ERROR, context={ 'missing_columns': missing_columns, 'available_columns': list(df.columns), 'layer_type': 'support_resistance' }, recovery_suggestion=f'Ensure S/R data contains: {required_columns}' ) self.error_handler.errors.append(error) return False # Validate line types valid_line_types = {'support', 'resistance', 'trend'} invalid_lines = df[~df['line_type'].isin(valid_line_types)] if not invalid_lines.empty: error = ChartError( code='INVALID_SR_TYPES', message=f'Invalid S/R line types: {set(invalid_lines["line_type"].unique())}', severity=ErrorSeverity.WARNING, context={ 'invalid_types': list(invalid_lines['line_type'].unique()), 'valid_types': list(valid_line_types) }, recovery_suggestion='Line types must be: support, resistance, or trend' ) self.error_handler.warnings.append(error) # Validate positive price levels invalid_prices = df[df['price_level'] <= 0] if not invalid_prices.empty: error = ChartError( code='INVALID_SR_PRICES', message=f'Invalid price levels found (must be > 0)', severity=ErrorSeverity.WARNING, context={'invalid_count': len(invalid_prices)}, recovery_suggestion='Price levels must be positive values' ) self.error_handler.warnings.append(error) return True except Exception as e: self.logger.error(f"Chart Support Resistance: Error validating S/R data: {e}") error = ChartError( code='SR_VALIDATION_ERROR', message=f'S/R validation failed: {str(e)}', severity=ErrorSeverity.ERROR, context={'exception': str(e), 'layer_type': 'support_resistance'} ) self.error_handler.errors.append(error) return False def detect_support_resistance_levels(self, data: pd.DataFrame) -> List[Dict[str, Any]]: """ Auto-detect support and resistance levels from price data. Args: data: OHLCV market data Returns: List of detected S/R levels """ try: sr_levels = [] if data.empty: return sr_levels # Simple pivot point detection for support/resistance window = 5 # Look for pivots in 5-period windows sensitivity = self.config.sensitivity highs = data['high'].values lows = data['low'].values timestamps = data['timestamp'].values # Find pivot highs (potential resistance) for i in range(window, len(highs) - window): is_pivot_high = True current_high = highs[i] # Check if this is a local maximum for j in range(i - window, i + window + 1): if j != i and highs[j] >= current_high: is_pivot_high = False break if is_pivot_high: # Count how many times price touched this level touches = 0 level_range = current_high * sensitivity for price in highs: if abs(price - current_high) <= level_range: touches += 1 if touches >= self.config.min_touches: sr_levels.append({ 'price_level': current_high, 'line_type': 'resistance', 'strength': touches, 'first_touch': timestamps[i], 'last_touch': timestamps[i], 'touch_count': touches }) # Find pivot lows (potential support) for i in range(window, len(lows) - window): is_pivot_low = True current_low = lows[i] # Check if this is a local minimum for j in range(i - window, i + window + 1): if j != i and lows[j] <= current_low: is_pivot_low = False break if is_pivot_low: # Count how many times price touched this level touches = 0 level_range = current_low * sensitivity for price in lows: if abs(price - current_low) <= level_range: touches += 1 if touches >= self.config.min_touches: sr_levels.append({ 'price_level': current_low, 'line_type': 'support', 'strength': touches, 'first_touch': timestamps[i], 'last_touch': timestamps[i], 'touch_count': touches }) # Sort by strength (touch count) and remove duplicates sr_levels = sorted(sr_levels, key=lambda x: x['strength'], reverse=True) # Remove levels that are too close to each other filtered_levels = [] for level in sr_levels: is_duplicate = False for existing in filtered_levels: if abs(level['price_level'] - existing['price_level']) / existing['price_level'] < sensitivity: is_duplicate = True break if not is_duplicate: filtered_levels.append(level) self.logger.info(f"Detected {len(filtered_levels)} S/R levels from {len(data)} candles") return filtered_levels except Exception as e: self.logger.error(f"Chart Support Resistance: Error detecting S/R levels: {e}") return [] def filter_sr_by_config(self, sr_levels: pd.DataFrame) -> pd.DataFrame: """ Filter support/resistance levels based on configuration. Args: sr_levels: Raw S/R level data Returns: Filtered S/R level data """ try: if sr_levels.empty: return sr_levels filtered = sr_levels.copy() # Filter by line types if self.config.line_types: filtered = filtered[filtered['line_type'].isin(self.config.line_types)] self.logger.info(f"Filtered S/R levels: {len(sr_levels)} -> {len(filtered)} levels") return filtered except Exception as e: self.logger.error(f"Chart Support Resistance: Error filtering S/R levels: {e}") return pd.DataFrame() def create_sr_traces(self, sr_levels: pd.DataFrame, data_range: Tuple[datetime, datetime]) -> List[go.Scatter]: """ Create Plotly traces for support/resistance lines. Args: sr_levels: Filtered S/R level data data_range: (start_time, end_time) for drawing lines Returns: List of Plotly traces for S/R lines """ traces = [] try: if sr_levels.empty: return traces start_time, end_time = data_range # Group levels by type for line_type in sr_levels['line_type'].unique(): type_levels = sr_levels[sr_levels['line_type'] == line_type] if type_levels.empty: continue # Create horizontal lines for each level for _, level in type_levels.iterrows(): price = level['price_level'] # Prepare hover text hover_parts = [ f"{level['line_type'].upper()}: ${price:.4f}" ] if 'strength' in level: hover_parts.append(f"Strength: {level['strength']}") if 'touch_count' in level: hover_parts.append(f"Touches: {level['touch_count']}") hover_text = "
".join(hover_parts) # Create horizontal line trace line_trace = go.Scatter( x=[start_time, end_time], y=[price, price], mode='lines', line=dict( color=self.sr_colors.get(line_type, '#666666'), width=self.config.line_width, dash=self.line_styles.get(line_type, 'solid') ), opacity=self.config.line_opacity, name=f"{line_type.upper()} ${price:.2f}", text=hover_text, hoverinfo='text', showlegend=True, legendgroup=f"sr_{line_type}" ) traces.append(line_trace) # Add price labels if enabled if self.config.show_price_labels: label_trace = go.Scatter( x=[end_time], y=[price], mode='text', text=[f"${price:.2f}"], textposition='middle right', textfont=dict( size=10, color=self.sr_colors.get(line_type, '#666666') ), showlegend=False, hoverinfo='skip' ) traces.append(label_trace) return traces except Exception as e: self.logger.error(f"Chart Support Resistance: Error creating S/R traces: {e}") # Return error trace error_trace = self.create_error_trace(f"Error displaying S/R lines: {str(e)}") return [error_trace] def is_enabled(self) -> bool: """Check if the S/R layer is enabled.""" return self.config.enabled def is_overlay(self) -> bool: """S/R layers are always overlays on the main chart.""" return True def get_subplot_row(self) -> Optional[int]: """S/R layers appear on main chart (no subplot).""" return None class SupportResistanceLayer(BaseSupportResistanceLayer): """ Support and resistance line layer for displaying key price levels. """ def __init__(self, config: SupportResistanceLayerConfig = None): """ Initialize support/resistance layer. Args: config: S/R layer configuration (optional, uses defaults) """ if config is None: config = SupportResistanceLayerConfig( name="Support/Resistance", enabled=True, line_types=['support', 'resistance'], line_width=2, line_opacity=0.7, show_price_labels=True, auto_detect=True, sensitivity=0.02, min_touches=2 ) super().__init__(config) self.logger.info(f"Initialized SupportResistanceLayer: {config.name}") def render(self, fig: go.Figure, data: pd.DataFrame, sr_levels: pd.DataFrame = None, **kwargs) -> go.Figure: """ Render support/resistance lines on the chart. Args: fig: Plotly figure to render onto data: Market data (OHLCV format) sr_levels: Manual S/R level data (optional) **kwargs: Additional rendering parameters Returns: Updated figure with S/R overlays """ try: # Determine data time range for drawing lines if data.empty: self.logger.warning("No market data provided for S/R rendering") return fig start_time = data['timestamp'].min() end_time = data['timestamp'].max() data_range = (start_time, end_time) # Combine manual levels and auto-detected levels combined_levels = [] # Add manual levels from configuration if self.config.manual_levels: for level in self.config.manual_levels: if 'price_level' in level and 'line_type' in level: combined_levels.append(level) # Add manual levels from parameter if sr_levels is not None and not sr_levels.empty: # Validate manual S/R data if self.validate_sr_data(sr_levels): combined_levels.extend(sr_levels.to_dict('records')) # Auto-detect levels if enabled if self.config.auto_detect: detected_levels = self.detect_support_resistance_levels(data) combined_levels.extend(detected_levels) if not combined_levels: self.logger.info("No S/R levels to display") return fig # Convert to DataFrame and filter sr_df = pd.DataFrame(combined_levels) # Validate combined data if not self.validate_sr_data(sr_df): self.logger.warning("S/R data validation failed") error_message = self.error_handler.get_user_friendly_message() fig.add_annotation( text=f"S/R Error: {error_message}", x=0.5, y=0.95, xref="paper", yref="paper", showarrow=False, font=dict(color="orange", size=10) ) return fig # Filter S/R levels based on configuration filtered_sr = self.filter_sr_by_config(sr_df) if filtered_sr.empty: self.logger.info("No S/R levels remain after filtering") return fig # Create S/R traces sr_traces = self.create_sr_traces(filtered_sr, data_range) # Add traces to figure for trace in sr_traces: fig.add_trace(trace) # Store processed data for potential reuse self.sr_data = filtered_sr self.logger.info(f"Successfully rendered {len(filtered_sr)} S/R levels") return fig except Exception as e: self.logger.error(f"Chart Support Resistance: Error rendering S/R layer: {e}") # Add error annotation to chart fig.add_annotation( text=f"S/R Rendering Error: {str(e)}", x=0.5, y=0.9, xref="paper", yref="paper", showarrow=False, font=dict(color="orange", size=10) ) return fig # Convenience functions for creating support/resistance layers def create_support_resistance_layer(auto_detect: bool = True, manual_levels: List[Dict[str, Any]] = None, sensitivity: float = 0.02, line_types: List[str] = None, **kwargs) -> SupportResistanceLayer: """ Create a support/resistance layer with common configurations. Args: auto_detect: Automatically detect S/R levels from price data manual_levels: List of manual S/R levels to display sensitivity: Price sensitivity for level detection (2% default) line_types: Types of lines to display (['support', 'resistance'] by default) **kwargs: Additional configuration options Returns: Configured SupportResistanceLayer instance """ if line_types is None: line_types = ['support', 'resistance'] if manual_levels is None: manual_levels = [] config = SupportResistanceLayerConfig( name="Support/Resistance", enabled=True, line_types=line_types, auto_detect=auto_detect, manual_levels=manual_levels, sensitivity=sensitivity, line_width=kwargs.get('line_width', 2), line_opacity=kwargs.get('line_opacity', 0.7), show_price_labels=kwargs.get('show_price_labels', True), min_touches=kwargs.get('min_touches', 2), **{k: v for k, v in kwargs.items() if k not in ['line_width', 'line_opacity', 'show_price_labels', 'min_touches']} ) return SupportResistanceLayer(config) def create_support_only_layer(**kwargs) -> SupportResistanceLayer: """Create a layer that shows only support levels.""" return create_support_resistance_layer(line_types=['support'], **kwargs) def create_resistance_only_layer(**kwargs) -> SupportResistanceLayer: """Create a layer that shows only resistance levels.""" return create_support_resistance_layer(line_types=['resistance'], **kwargs) def create_trend_lines_layer(manual_levels: List[Dict[str, Any]] = None, **kwargs) -> SupportResistanceLayer: """ Create a layer for manual trend lines. Args: manual_levels: List of trend line definitions **kwargs: Additional configuration options Returns: Configured SupportResistanceLayer for trend lines """ if manual_levels is None: manual_levels = [] return create_support_resistance_layer( auto_detect=False, # Trend lines are usually manual line_types=['trend'], manual_levels=manual_levels, **kwargs ) def create_key_levels_layer(levels: List[float], level_type: str = 'resistance', **kwargs) -> SupportResistanceLayer: """ Create a layer for specific price levels (e.g., round numbers, previous highs/lows). Args: levels: List of price levels to display level_type: Type of level ('support', 'resistance', or 'trend') **kwargs: Additional configuration options Returns: Configured SupportResistanceLayer for key levels """ manual_levels = [ {'price_level': level, 'line_type': level_type, 'strength': 1} for level in levels ] return create_support_resistance_layer( auto_detect=False, manual_levels=manual_levels, line_types=[level_type], **kwargs ) @dataclass class CustomStrategySignalConfig(LayerConfig): """Configuration for custom strategy signal definitions""" signal_definitions: Dict[str, Dict[str, Any]] = None # Custom signal type definitions custom_colors: Dict[str, str] = None # Custom colors for signal types custom_symbols: Dict[str, str] = None # Custom symbols for signal types custom_sizes: Dict[str, int] = None # Custom sizes for signal types strategy_name: str = "Custom Strategy" # Name of the strategy allow_multiple_signals: bool = True # Allow multiple signals at same time signal_priority: Dict[str, int] = None # Priority order for overlapping signals def __post_init__(self): super().__post_init__() if self.signal_definitions is None: self.signal_definitions = {} if self.custom_colors is None: self.custom_colors = {} if self.custom_symbols is None: self.custom_symbols = {} if self.custom_sizes is None: self.custom_sizes = {} if self.signal_priority is None: self.signal_priority = {} class CustomStrategySignalInterface: """ Interface for custom trading strategies to define their signal visualization. """ def __init__(self): """Initialize custom strategy signal interface.""" self.signal_types = {} self.signal_validators = {} self.signal_renderers = {} def register_signal_type(self, signal_type: str, color: str, symbol: str, size: int = 12, description: str = "", validator: callable = None, renderer: callable = None) -> None: """ Register a custom signal type with visualization properties. Args: signal_type: Unique signal type identifier color: Color for the signal marker (hex or CSS color) symbol: Plotly marker symbol size: Marker size in pixels description: Human-readable description validator: Optional custom validation function renderer: Optional custom rendering function """ self.signal_types[signal_type] = { 'color': color, 'symbol': symbol, 'size': size, 'description': description } if validator: self.signal_validators[signal_type] = validator if renderer: self.signal_renderers[signal_type] = renderer def get_signal_style(self, signal_type: str) -> Dict[str, Any]: """ Get style properties for a signal type. Args: signal_type: Signal type identifier Returns: Style properties dictionary """ return self.signal_types.get(signal_type, { 'color': '#666666', 'symbol': 'circle', 'size': 10, 'description': 'Unknown signal' }) def validate_custom_signal(self, signal_type: str, signal_data: Dict[str, Any]) -> bool: """ Validate custom signal data using registered validators. Args: signal_type: Signal type to validate signal_data: Signal data dictionary Returns: True if signal is valid """ if signal_type in self.signal_validators: return self.signal_validators[signal_type](signal_data) return True # Default to valid if no validator def render_custom_signal(self, signal_type: str, signal_data: Dict[str, Any]) -> Dict[str, Any]: """ Render custom signal using registered renderers. Args: signal_type: Signal type to render signal_data: Signal data dictionary Returns: Rendered signal properties """ if signal_type in self.signal_renderers: return self.signal_renderers[signal_type](signal_data) return signal_data # Default passthrough def get_all_signal_types(self) -> List[str]: """Get list of all registered signal types.""" return list(self.signal_types.keys()) class BaseCustomStrategyLayer(BaseLayer): """ Base class for custom strategy signal layers. """ def __init__(self, config: CustomStrategySignalConfig): """ Initialize custom strategy signal layer. Args: config: Custom strategy signal configuration """ super().__init__(config) self.signal_interface = CustomStrategySignalInterface() self.strategy_data = None # Register custom signal types from config self._register_config_signals() # Default fallback styling self.default_colors = { 'entry_long': '#4caf50', # Green 'exit_long': '#81c784', # Light green 'entry_short': '#f44336', # Red 'exit_short': '#e57373', # Light red 'stop_loss': '#ff5722', # Deep orange 'take_profit': '#2196f3', # Blue 'rebalance': '#9c27b0', # Purple 'hedge': '#ff9800', # Orange } self.default_symbols = { 'entry_long': 'triangle-up', 'exit_long': 'triangle-up-open', 'entry_short': 'triangle-down', 'exit_short': 'triangle-down-open', 'stop_loss': 'x', 'take_profit': 'star', 'rebalance': 'diamond', 'hedge': 'hexagon', } def _register_config_signals(self): """Register signal types from configuration.""" for signal_type, definition in self.config.signal_definitions.items(): color = self.config.custom_colors.get(signal_type, definition.get('color', '#666666')) symbol = self.config.custom_symbols.get(signal_type, definition.get('symbol', 'circle')) size = self.config.custom_sizes.get(signal_type, definition.get('size', 12)) description = definition.get('description', f'{signal_type} signal') self.signal_interface.register_signal_type( signal_type=signal_type, color=color, symbol=symbol, size=size, description=description ) def validate_strategy_data(self, signals: Union[pd.DataFrame, List[Dict[str, Any]]]) -> bool: """ Validate custom strategy signal data. Args: signals: Strategy signal data Returns: True if data is valid """ try: # Clear previous errors self.error_handler.clear_errors() # Convert to DataFrame if needed if isinstance(signals, list): if not signals: return True df = pd.DataFrame(signals) else: df = signals.copy() # Check required columns required_columns = ['timestamp', 'signal_type', 'price'] missing_columns = [col for col in required_columns if col not in df.columns] if missing_columns: error = ChartError( code='MISSING_STRATEGY_COLUMNS', message=f'Missing strategy signal columns: {missing_columns}', severity=ErrorSeverity.ERROR, context={ 'missing_columns': missing_columns, 'available_columns': list(df.columns), 'layer_type': 'custom_strategy' }, recovery_suggestion=f'Ensure strategy data contains: {required_columns}' ) self.error_handler.errors.append(error) return False # Validate custom signal types using interface for _, signal in df.iterrows(): signal_data = signal.to_dict() signal_type = signal_data.get('signal_type') if not self.signal_interface.validate_custom_signal(signal_type, signal_data): error = ChartError( code='INVALID_CUSTOM_SIGNAL', message=f'Custom signal validation failed for type: {signal_type}', severity=ErrorSeverity.WARNING, context={ 'signal_type': signal_type, 'signal_data': signal_data }, recovery_suggestion='Check custom signal validator logic' ) self.error_handler.warnings.append(error) return True except Exception as e: self.logger.error(f"Chart Custom Strategy: Error validating strategy data: {e}") error = ChartError( code='STRATEGY_VALIDATION_ERROR', message=f'Strategy validation failed: {str(e)}', severity=ErrorSeverity.ERROR, context={'exception': str(e), 'layer_type': 'custom_strategy'} ) self.error_handler.errors.append(error) return False def create_strategy_traces(self, signals: pd.DataFrame) -> List[go.Scatter]: """ Create Plotly traces for custom strategy signals. Args: signals: Filtered strategy signal data Returns: List of Plotly traces for strategy signals """ traces = [] try: if signals.empty: return traces # Group signals by type for better legend organization for signal_type in signals['signal_type'].unique(): type_signals = signals[signals['signal_type'] == signal_type] if type_signals.empty: continue # Get style for this signal type style = self.signal_interface.get_signal_style(signal_type) # Prepare hover text hover_texts = [] for _, signal in type_signals.iterrows(): # Allow custom renderer to modify signal data rendered_signal = self.signal_interface.render_custom_signal( signal_type, signal.to_dict() ) hover_parts = [ f"{signal_type.upper()}: ${signal['price']:.4f}", f"Time: {signal['timestamp']}" ] # Add custom fields if present for field in ['confidence', 'quantity', 'reason', 'metadata']: if field in rendered_signal and rendered_signal[field] is not None: if field == 'confidence': hover_parts.append(f"Confidence: {rendered_signal[field]:.2%}") elif field == 'quantity': hover_parts.append(f"Quantity: {rendered_signal[field]}") elif field == 'reason': hover_parts.append(f"Reason: {rendered_signal[field]}") elif field == 'metadata' and isinstance(rendered_signal[field], dict): for key, value in rendered_signal[field].items(): hover_parts.append(f"{key}: {value}") hover_texts.append("
".join(hover_parts)) # Create scatter trace for this signal type trace = go.Scatter( x=type_signals['timestamp'], y=type_signals['price'], mode='markers', marker=dict( symbol=style['symbol'], size=style['size'], color=style['color'], line=dict(width=1, color='white'), opacity=0.8 ), name=f"{self.config.strategy_name} - {signal_type.replace('_', ' ').title()}", text=hover_texts, hoverinfo='text', showlegend=True, legendgroup=f"strategy_{signal_type}" ) traces.append(trace) return traces except Exception as e: self.logger.error(f"Chart Custom Strategy: Error creating strategy traces: {e}") # Return error trace error_trace = self.create_error_trace(f"Error displaying strategy signals: {str(e)}") return [error_trace] def is_enabled(self) -> bool: """Check if the custom strategy layer is enabled.""" return self.config.enabled def is_overlay(self) -> bool: """Custom strategy layers are overlays on the main chart.""" return True def get_subplot_row(self) -> Optional[int]: """Custom strategy layers appear on main chart (no subplot).""" return None class CustomStrategySignalLayer(BaseCustomStrategyLayer): """ Custom strategy signal layer for flexible strategy signal visualization. """ def __init__(self, config: CustomStrategySignalConfig = None): """ Initialize custom strategy signal layer. Args: config: Custom strategy signal configuration (optional) """ if config is None: config = CustomStrategySignalConfig( name="Custom Strategy", enabled=True, strategy_name="Custom Strategy", signal_definitions={}, allow_multiple_signals=True ) super().__init__(config) self.logger.info(f"Initialized CustomStrategySignalLayer: {config.strategy_name}") def add_signal_type(self, signal_type: str, color: str, symbol: str, size: int = 12, **kwargs): """ Add a new signal type to this layer. Args: signal_type: Signal type identifier color: Signal color symbol: Plotly marker symbol size: Marker size **kwargs: Additional properties """ self.signal_interface.register_signal_type( signal_type=signal_type, color=color, symbol=symbol, size=size, **kwargs ) self.logger.info(f"Added signal type '{signal_type}' to {self.config.strategy_name}") def render(self, fig: go.Figure, data: pd.DataFrame, signals: pd.DataFrame = None, **kwargs) -> go.Figure: """ Render custom strategy signals on the chart. Args: fig: Plotly figure to render onto data: Market data (OHLCV format) signals: Strategy signal data (optional) **kwargs: Additional rendering parameters Returns: Updated figure with strategy signal overlays """ try: if signals is None or signals.empty: self.logger.info(f"No signals provided for {self.config.strategy_name}") return fig # Validate strategy signal data if not self.validate_strategy_data(signals): self.logger.warning(f"Strategy signal data validation failed for {self.config.strategy_name}") error_message = self.error_handler.get_user_friendly_message() fig.add_annotation( text=f"Strategy Error: {error_message}", x=0.5, y=0.95, xref="paper", yref="paper", showarrow=False, font=dict(color="purple", size=10) ) return fig # Create strategy signal traces strategy_traces = self.create_strategy_traces(signals) # Add traces to figure for trace in strategy_traces: fig.add_trace(trace) # Store processed data for potential reuse self.strategy_data = signals self.logger.info(f"Successfully rendered {len(signals)} {self.config.strategy_name} signals") return fig except Exception as e: self.logger.error(f"Chart Custom Strategy: Error rendering {self.config.strategy_name} layer: {e}") # Add error annotation to chart fig.add_annotation( text=f"Strategy Rendering Error: {str(e)}", x=0.5, y=0.9, xref="paper", yref="paper", showarrow=False, font=dict(color="purple", size=10) ) return fig # Convenience functions for creating custom strategy signal layers def create_custom_strategy_layer(strategy_name: str, signal_definitions: Dict[str, Dict[str, Any]] = None, **kwargs) -> CustomStrategySignalLayer: """ Create a custom strategy signal layer. Args: strategy_name: Name of the strategy signal_definitions: Dictionary of signal type definitions **kwargs: Additional configuration options Returns: Configured CustomStrategySignalLayer instance """ if signal_definitions is None: signal_definitions = {} config = CustomStrategySignalConfig( name=f"{strategy_name} Signals", enabled=True, strategy_name=strategy_name, signal_definitions=signal_definitions, custom_colors=kwargs.get('custom_colors', {}), custom_symbols=kwargs.get('custom_symbols', {}), custom_sizes=kwargs.get('custom_sizes', {}), allow_multiple_signals=kwargs.get('allow_multiple_signals', True), signal_priority=kwargs.get('signal_priority', {}), **{k: v for k, v in kwargs.items() if k not in [ 'custom_colors', 'custom_symbols', 'custom_sizes', 'allow_multiple_signals', 'signal_priority' ]} ) return CustomStrategySignalLayer(config) def create_pairs_trading_layer(**kwargs) -> CustomStrategySignalLayer: """Create a layer for pairs trading signals.""" signal_definitions = { 'long_spread': { 'color': '#4caf50', 'symbol': 'triangle-up', 'size': 12, 'description': 'Long spread signal' }, 'short_spread': { 'color': '#f44336', 'symbol': 'triangle-down', 'size': 12, 'description': 'Short spread signal' }, 'close_spread': { 'color': '#ff9800', 'symbol': 'circle', 'size': 10, 'description': 'Close spread signal' } } return create_custom_strategy_layer( strategy_name="Pairs Trading", signal_definitions=signal_definitions, **kwargs ) def create_momentum_strategy_layer(**kwargs) -> CustomStrategySignalLayer: """Create a layer for momentum trading signals.""" signal_definitions = { 'momentum_buy': { 'color': '#2e7d32', 'symbol': 'triangle-up', 'size': 14, 'description': 'Momentum buy signal' }, 'momentum_sell': { 'color': '#c62828', 'symbol': 'triangle-down', 'size': 14, 'description': 'Momentum sell signal' }, 'momentum_exit': { 'color': '#1565c0', 'symbol': 'circle-open', 'size': 12, 'description': 'Momentum exit signal' } } return create_custom_strategy_layer( strategy_name="Momentum Strategy", signal_definitions=signal_definitions, **kwargs ) def create_arbitrage_layer(**kwargs) -> CustomStrategySignalLayer: """Create a layer for arbitrage opportunity signals.""" signal_definitions = { 'arb_opportunity': { 'color': '#6a1b9a', 'symbol': 'star', 'size': 16, 'description': 'Arbitrage opportunity' }, 'arb_entry': { 'color': '#8e24aa', 'symbol': 'diamond', 'size': 12, 'description': 'Arbitrage entry' }, 'arb_exit': { 'color': '#ab47bc', 'symbol': 'diamond-open', 'size': 12, 'description': 'Arbitrage exit' } } return create_custom_strategy_layer( strategy_name="Arbitrage", signal_definitions=signal_definitions, **kwargs ) def create_mean_reversion_layer(**kwargs) -> CustomStrategySignalLayer: """Create a layer for mean reversion strategy signals.""" signal_definitions = { 'oversold_entry': { 'color': '#388e3c', 'symbol': 'triangle-up', 'size': 12, 'description': 'Oversold entry signal' }, 'overbought_entry': { 'color': '#d32f2f', 'symbol': 'triangle-down', 'size': 12, 'description': 'Overbought entry signal' }, 'mean_revert': { 'color': '#1976d2', 'symbol': 'circle', 'size': 10, 'description': 'Mean reversion exit' } } return create_custom_strategy_layer( strategy_name="Mean Reversion", signal_definitions=signal_definitions, **kwargs ) def create_breakout_strategy_layer(**kwargs) -> CustomStrategySignalLayer: """Create a layer for breakout strategy signals.""" signal_definitions = { 'breakout_long': { 'color': '#43a047', 'symbol': 'triangle-up', 'size': 14, 'description': 'Breakout long signal' }, 'breakout_short': { 'color': '#e53935', 'symbol': 'triangle-down', 'size': 14, 'description': 'Breakout short signal' }, 'false_breakout': { 'color': '#fb8c00', 'symbol': 'x', 'size': 12, 'description': 'False breakout signal' } } return create_custom_strategy_layer( strategy_name="Breakout", signal_definitions=signal_definitions, **kwargs ) @dataclass class SignalStyleConfig: """Configuration for signal visual styling and customization""" color_scheme: str = "default" # Color scheme name custom_colors: Dict[str, str] = None # Custom color mappings marker_shapes: Dict[str, str] = None # Custom marker shapes marker_sizes: Dict[str, int] = None # Custom marker sizes opacity: float = 0.8 # Signal marker opacity border_width: int = 1 # Marker border width border_color: str = "white" # Marker border color gradient_effects: bool = False # Enable gradient effects animation_enabled: bool = False # Enable marker animations hover_effects: Dict[str, Any] = None # Custom hover styling def __post_init__(self): if self.custom_colors is None: self.custom_colors = {} if self.marker_shapes is None: self.marker_shapes = {} if self.marker_sizes is None: self.marker_sizes = {} if self.hover_effects is None: self.hover_effects = {} class SignalStyleManager: """ Manager for signal styling, themes, and customization options. """ def __init__(self): """Initialize signal style manager with predefined themes.""" self.color_schemes = { 'default': { 'buy': '#4caf50', 'sell': '#f44336', 'hold': '#ff9800', 'entry_long': '#4caf50', 'exit_long': '#81c784', 'entry_short': '#f44336', 'exit_short': '#e57373', 'stop_loss': '#ff5722', 'take_profit': '#2196f3' }, 'professional': { 'buy': '#00c853', 'sell': '#d50000', 'hold': '#ff6f00', 'entry_long': '#00c853', 'exit_long': '#69f0ae', 'entry_short': '#d50000', 'exit_short': '#ff5252', 'stop_loss': '#ff1744', 'take_profit': '#2979ff' }, 'colorblind_friendly': { 'buy': '#1f77b4', # Blue 'sell': '#ff7f0e', # Orange 'hold': '#2ca02c', # Green 'entry_long': '#1f77b4', 'exit_long': '#aec7e8', 'entry_short': '#ff7f0e', 'exit_short': '#ffbb78', 'stop_loss': '#d62728', 'take_profit': '#9467bd' }, 'dark_theme': { 'buy': '#66bb6a', 'sell': '#ef5350', 'hold': '#ffa726', 'entry_long': '#66bb6a', 'exit_long': '#a5d6a7', 'entry_short': '#ef5350', 'exit_short': '#ffab91', 'stop_loss': '#ff7043', 'take_profit': '#42a5f5' }, 'minimal': { 'buy': '#424242', 'sell': '#757575', 'hold': '#9e9e9e', 'entry_long': '#424242', 'exit_long': '#616161', 'entry_short': '#757575', 'exit_short': '#bdbdbd', 'stop_loss': '#212121', 'take_profit': '#424242' } } self.marker_shapes = { 'default': { 'buy': 'triangle-up', 'sell': 'triangle-down', 'hold': 'circle', 'entry_long': 'triangle-up', 'exit_long': 'triangle-up-open', 'entry_short': 'triangle-down', 'exit_short': 'triangle-down-open', 'stop_loss': 'x', 'take_profit': 'star' }, 'geometric': { 'buy': 'diamond', 'sell': 'diamond', 'hold': 'square', 'entry_long': 'diamond', 'exit_long': 'diamond-open', 'entry_short': 'diamond', 'exit_short': 'diamond-open', 'stop_loss': 'square', 'take_profit': 'hexagon' }, 'arrows': { 'buy': 'triangle-up', 'sell': 'triangle-down', 'hold': 'circle', 'entry_long': 'triangle-up', 'exit_long': 'triangle-right', 'entry_short': 'triangle-down', 'exit_short': 'triangle-left', 'stop_loss': 'x', 'take_profit': 'cross' } } self.size_schemes = { 'small': { 'default': 8, 'important': 10, 'critical': 12 }, 'medium': { 'default': 12, 'important': 14, 'critical': 16 }, 'large': { 'default': 16, 'important': 18, 'critical': 20 } } def get_signal_style(self, signal_type: str, style_config: SignalStyleConfig) -> Dict[str, Any]: """ Get complete styling for a signal type. Args: signal_type: Type of signal style_config: Style configuration Returns: Complete style dictionary """ # Get base color scheme color_scheme = self.color_schemes.get(style_config.color_scheme, self.color_schemes['default']) # Apply custom color if specified color = style_config.custom_colors.get(signal_type, color_scheme.get(signal_type, '#666666')) # Get marker shape shape_scheme = self.marker_shapes.get(style_config.color_scheme, self.marker_shapes['default']) shape = style_config.marker_shapes.get(signal_type, shape_scheme.get(signal_type, 'circle')) # Get marker size size = style_config.marker_sizes.get(signal_type, 12) return { 'color': color, 'symbol': shape, 'size': size, 'opacity': style_config.opacity, 'border_width': style_config.border_width, 'border_color': style_config.border_color, 'hover_effects': style_config.hover_effects.get(signal_type, {}) } def create_gradient_colors(self, base_color: str, steps: int = 5) -> List[str]: """ Create gradient color variations for enhanced styling. Args: base_color: Base hex color steps: Number of gradient steps Returns: List of gradient colors """ try: # Simple gradient implementation # In a real implementation, you might use a color library base_rgb = int(base_color[1:], 16) colors = [] for i in range(steps): # Create lighter/darker variations factor = 0.7 + (i * 0.6 / steps) # Range from 0.7 to 1.3 r = min(255, int(((base_rgb >> 16) & 0xFF) * factor)) g = min(255, int(((base_rgb >> 8) & 0xFF) * factor)) b = min(255, int((base_rgb & 0xFF) * factor)) color_hex = f"#{r:02x}{g:02x}{b:02x}" colors.append(color_hex) return colors except Exception: # Fallback to base color return [base_color] * steps def apply_theme(self, theme_name: str, signals: pd.DataFrame) -> Dict[str, Dict[str, Any]]: """ Apply a complete theme to signals. Args: theme_name: Name of the theme to apply signals: Signal data Returns: Theme styling for all signal types """ if theme_name not in self.color_schemes: theme_name = 'default' theme_colors = self.color_schemes[theme_name] theme_shapes = self.marker_shapes.get(theme_name, self.marker_shapes['default']) styles = {} if not signals.empty and 'signal_type' in signals.columns: for signal_type in signals['signal_type'].unique(): styles[signal_type] = { 'color': theme_colors.get(signal_type, '#666666'), 'symbol': theme_shapes.get(signal_type, 'circle'), 'size': 12, 'opacity': 0.8 } return styles def create_custom_style(self, signal_type: str, color: str = None, shape: str = None, size: int = None, **kwargs) -> Dict[str, Any]: """ Create custom style for a specific signal type. Args: signal_type: Signal type identifier color: Custom color shape: Custom marker shape size: Custom marker size **kwargs: Additional style properties Returns: Custom style dictionary """ style = { 'color': color or '#666666', 'symbol': shape or 'circle', 'size': size or 12, 'opacity': kwargs.get('opacity', 0.8), 'border_width': kwargs.get('border_width', 1), 'border_color': kwargs.get('border_color', 'white') } return style class EnhancedSignalLayer(BaseSignalLayer): """ Enhanced signal layer with advanced styling and customization options. """ def __init__(self, config: SignalLayerConfig, style_config: SignalStyleConfig = None): """ Initialize enhanced signal layer. Args: config: Signal layer configuration style_config: Style configuration (optional) """ super().__init__(config) if style_config is None: style_config = SignalStyleConfig() self.style_config = style_config self.style_manager = SignalStyleManager() self.logger.info(f"Enhanced Signal Layer: Initialized with {style_config.color_scheme} theme") def update_style_config(self, style_config: SignalStyleConfig): """Update the style configuration.""" self.style_config = style_config self.logger.info(f"Enhanced Signal Layer: Updated style config to {style_config.color_scheme} theme") def set_color_scheme(self, scheme_name: str): """ Set the color scheme for signals. Args: scheme_name: Name of the color scheme """ self.style_config.color_scheme = scheme_name self.logger.info(f"Enhanced Signal Layer: Set color scheme to: {scheme_name}") def add_custom_signal_style(self, signal_type: str, color: str, shape: str, size: int = 12): """ Add custom styling for a signal type. Args: signal_type: Signal type identifier color: Signal color shape: Marker shape size: Marker size """ self.style_config.custom_colors[signal_type] = color self.style_config.marker_shapes[signal_type] = shape self.style_config.marker_sizes[signal_type] = size self.logger.info(f"Enhanced Signal Layer: Added custom style for {signal_type}: {color}, {shape}, {size}") def create_enhanced_signal_traces(self, signals: pd.DataFrame) -> List[go.Scatter]: """ Create enhanced signal traces with advanced styling. Args: signals: Filtered signal data Returns: List of enhanced Plotly traces """ traces = [] try: if signals.empty: return traces # Group signals by type for styling for signal_type in signals['signal_type'].unique(): type_signals = signals[signals['signal_type'] == signal_type] if type_signals.empty: continue # Get enhanced styling style = self.style_manager.get_signal_style(signal_type, self.style_config) # Prepare enhanced hover text hover_texts = [] for _, signal in type_signals.iterrows(): hover_parts = [ f"{signal_type.upper()}", f"Price: ${signal['price']:.4f}", f"Time: {signal['timestamp']}" ] if 'confidence' in signal and signal['confidence'] is not None: confidence = float(signal['confidence']) hover_parts.append(f"Confidence: {confidence:.1%}") if 'reason' in signal and signal['reason']: hover_parts.append(f"Reason: {signal['reason']}") hover_texts.append("
".join(hover_parts)) # Create enhanced marker styling marker_dict = { 'symbol': style['symbol'], 'size': style['size'], 'color': style['color'], 'opacity': style['opacity'], 'line': dict( width=style['border_width'], color=style['border_color'] ) } # Add gradient effects if enabled if self.style_config.gradient_effects: gradient_colors = self.style_manager.create_gradient_colors(style['color'], len(type_signals)) marker_dict['color'] = gradient_colors[:len(type_signals)] # Create enhanced trace trace = go.Scatter( x=type_signals['timestamp'], y=type_signals['price'], mode='markers', marker=marker_dict, name=f"{signal_type.replace('_', ' ').title()}", text=hover_texts, hoverinfo='text', hovertemplate='%{text}', showlegend=True, legendgroup=f"enhanced_{signal_type}" ) traces.append(trace) return traces except Exception as e: self.logger.error(f"Enhanced Signal Layer: Error creating enhanced signal traces: {e}") error_trace = self.create_error_trace(f"Error displaying enhanced signals: {str(e)}") return [error_trace] def render(self, fig: go.Figure, data: pd.DataFrame, signals: pd.DataFrame = None, **kwargs) -> go.Figure: """ Render enhanced signals with advanced styling. Args: fig: Plotly figure to render onto data: Market data (OHLCV format) signals: Signal data (optional) **kwargs: Additional rendering parameters Returns: Updated figure with enhanced signal overlays """ try: if signals is None or signals.empty: self.logger.info("No signals provided for enhanced rendering") return fig # Validate signal data if not self.validate_signal_data(signals): self.logger.warning("Enhanced signal data validation failed") error_message = self.error_handler.get_user_friendly_message() fig.add_annotation( text=f"Enhanced Signal Error: {error_message}", x=0.5, y=0.95, xref="paper", yref="paper", showarrow=False, font=dict(color="blue", size=10) ) return fig # Filter signals based on configuration filtered_signals = self.filter_signals_by_config(signals) if filtered_signals.empty: self.logger.info("No signals remain after enhanced filtering") return fig # Create enhanced signal traces enhanced_traces = self.create_enhanced_signal_traces(filtered_signals) # Add traces to figure for trace in enhanced_traces: fig.add_trace(trace) # Store processed data self.signal_data = filtered_signals self.logger.info(f"Successfully rendered {len(filtered_signals)} enhanced signals") return fig except Exception as e: self.logger.error(f"Enhanced Signal Layer: Error rendering enhanced signal layer: {e}") # Add error annotation fig.add_annotation( text=f"Enhanced Signal Rendering Error: {str(e)}", x=0.5, y=0.9, xref="paper", yref="paper", showarrow=False, font=dict(color="blue", size=10) ) return fig # Convenience functions for creating custom strategy signal layers def create_custom_strategy_layer(strategy_name: str, signal_definitions: Dict[str, Dict[str, Any]] = None, **kwargs) -> CustomStrategySignalLayer: """ Create a custom strategy signal layer. Args: strategy_name: Name of the strategy signal_definitions: Dictionary of signal type definitions **kwargs: Additional configuration options Returns: Configured CustomStrategySignalLayer instance """ if signal_definitions is None: signal_definitions = {} config = CustomStrategySignalConfig( name=f"{strategy_name} Signals", enabled=True, strategy_name=strategy_name, signal_definitions=signal_definitions, custom_colors=kwargs.get('custom_colors', {}), custom_symbols=kwargs.get('custom_symbols', {}), custom_sizes=kwargs.get('custom_sizes', {}), allow_multiple_signals=kwargs.get('allow_multiple_signals', True), signal_priority=kwargs.get('signal_priority', {}), **{k: v for k, v in kwargs.items() if k not in [ 'custom_colors', 'custom_symbols', 'custom_sizes', 'allow_multiple_signals', 'signal_priority' ]} ) return CustomStrategySignalLayer(config) def create_pairs_trading_layer(**kwargs) -> CustomStrategySignalLayer: """Create a layer for pairs trading signals.""" signal_definitions = { 'long_spread': { 'color': '#4caf50', 'symbol': 'triangle-up', 'size': 12, 'description': 'Long spread signal' }, 'short_spread': { 'color': '#f44336', 'symbol': 'triangle-down', 'size': 12, 'description': 'Short spread signal' }, 'close_spread': { 'color': '#ff9800', 'symbol': 'circle', 'size': 10, 'description': 'Close spread signal' } } return create_custom_strategy_layer( strategy_name="Pairs Trading", signal_definitions=signal_definitions, **kwargs ) def create_momentum_strategy_layer(**kwargs) -> CustomStrategySignalLayer: """Create a layer for momentum trading signals.""" signal_definitions = { 'momentum_buy': { 'color': '#2e7d32', 'symbol': 'triangle-up', 'size': 14, 'description': 'Momentum buy signal' }, 'momentum_sell': { 'color': '#c62828', 'symbol': 'triangle-down', 'size': 14, 'description': 'Momentum sell signal' }, 'momentum_exit': { 'color': '#1565c0', 'symbol': 'circle-open', 'size': 12, 'description': 'Momentum exit signal' } } return create_custom_strategy_layer( strategy_name="Momentum Strategy", signal_definitions=signal_definitions, **kwargs ) def create_arbitrage_layer(**kwargs) -> CustomStrategySignalLayer: """Create a layer for arbitrage opportunity signals.""" signal_definitions = { 'arb_opportunity': { 'color': '#6a1b9a', 'symbol': 'star', 'size': 16, 'description': 'Arbitrage opportunity' }, 'arb_entry': { 'color': '#8e24aa', 'symbol': 'diamond', 'size': 12, 'description': 'Arbitrage entry' }, 'arb_exit': { 'color': '#ab47bc', 'symbol': 'diamond-open', 'size': 12, 'description': 'Arbitrage exit' } } return create_custom_strategy_layer( strategy_name="Arbitrage", signal_definitions=signal_definitions, **kwargs ) def create_mean_reversion_layer(**kwargs) -> CustomStrategySignalLayer: """Create a layer for mean reversion strategy signals.""" signal_definitions = { 'oversold_entry': { 'color': '#388e3c', 'symbol': 'triangle-up', 'size': 12, 'description': 'Oversold entry signal' }, 'overbought_entry': { 'color': '#d32f2f', 'symbol': 'triangle-down', 'size': 12, 'description': 'Overbought entry signal' }, 'mean_revert': { 'color': '#1976d2', 'symbol': 'circle', 'size': 10, 'description': 'Mean reversion exit' } } return create_custom_strategy_layer( strategy_name="Mean Reversion", signal_definitions=signal_definitions, **kwargs ) def create_breakout_strategy_layer(**kwargs) -> CustomStrategySignalLayer: """Create a layer for breakout strategy signals.""" signal_definitions = { 'breakout_long': { 'color': '#43a047', 'symbol': 'triangle-up', 'size': 14, 'description': 'Breakout long signal' }, 'breakout_short': { 'color': '#e53935', 'symbol': 'triangle-down', 'size': 14, 'description': 'Breakout short signal' }, 'false_breakout': { 'color': '#fb8c00', 'symbol': 'x', 'size': 12, 'description': 'False breakout signal' } } return create_custom_strategy_layer( strategy_name="Breakout", signal_definitions=signal_definitions, **kwargs ) # Convenience functions for creating enhanced signal layers def create_enhanced_signal_layer(color_scheme: str = "default", signal_types: List[str] = None, **kwargs) -> EnhancedSignalLayer: """ Create an enhanced signal layer with styling. Args: color_scheme: Color scheme name signal_types: Signal types to display **kwargs: Additional configuration options Returns: Configured EnhancedSignalLayer instance """ if signal_types is None: signal_types = ['buy', 'sell'] signal_config = SignalLayerConfig( name="Enhanced Signals", enabled=True, signal_types=signal_types, confidence_threshold=kwargs.get('confidence_threshold', 0.0), show_confidence=kwargs.get('show_confidence', True), marker_size=kwargs.get('marker_size', 12), show_price_labels=kwargs.get('show_price_labels', True), bot_id=kwargs.get('bot_id', None) ) style_config = SignalStyleConfig( color_scheme=color_scheme, custom_colors=kwargs.get('custom_colors', {}), marker_shapes=kwargs.get('marker_shapes', {}), marker_sizes=kwargs.get('marker_sizes', {}), opacity=kwargs.get('opacity', 0.8), border_width=kwargs.get('border_width', 1), border_color=kwargs.get('border_color', 'white'), gradient_effects=kwargs.get('gradient_effects', False), animation_enabled=kwargs.get('animation_enabled', False) ) return EnhancedSignalLayer(signal_config, style_config) def create_professional_signal_layer(**kwargs) -> EnhancedSignalLayer: """Create an enhanced signal layer with professional styling.""" return create_enhanced_signal_layer(color_scheme="professional", **kwargs) def create_colorblind_friendly_signal_layer(**kwargs) -> EnhancedSignalLayer: """Create an enhanced signal layer with colorblind-friendly styling.""" return create_enhanced_signal_layer(color_scheme="colorblind_friendly", **kwargs) def create_dark_theme_signal_layer(**kwargs) -> EnhancedSignalLayer: """Create an enhanced signal layer with dark theme styling.""" return create_enhanced_signal_layer(color_scheme="dark_theme", **kwargs) def create_minimal_signal_layer(**kwargs) -> EnhancedSignalLayer: """Create an enhanced signal layer with minimal styling.""" return create_enhanced_signal_layer(color_scheme="minimal", **kwargs)