From 5506f5db6461ca54141ffa3106caa6d8394b460e Mon Sep 17 00:00:00 2001 From: "Vasily.onl" Date: Wed, 4 Jun 2025 15:54:14 +0800 Subject: [PATCH] Add trading signal and execution layers with database integration - Introduced `TradingSignalLayer` and `TradeExecutionLayer` for visualizing buy/sell signals and trade entries/exits on charts. - Implemented signal validation and filtering mechanisms to ensure data integrity and user-configurable options. - Enhanced market data layout to support new timeframes for improved user experience. - Updated documentation to reflect the new signal layer architecture and its integration with the dashboard. - Ensured compatibility with existing components while maintaining a modular structure for future enhancements. --- components/charts/layers/__init__.py | 37 +- components/charts/layers/signals.py | 1009 ++++++++++++++++++++++++++ dashboard/layouts/market_data.py | 8 +- tasks/3.4. Chart layers.md | 14 +- 4 files changed, 1059 insertions(+), 9 deletions(-) create mode 100644 components/charts/layers/signals.py diff --git a/components/charts/layers/__init__.py b/components/charts/layers/__init__.py index cc5f228..85f0bc1 100644 --- a/components/charts/layers/__init__.py +++ b/components/charts/layers/__init__.py @@ -14,6 +14,8 @@ Components: - BollingerBandsLayer: Bollinger Bands overlay with fill area - RSILayer: RSI oscillator subplot - MACDLayer: MACD lines and histogram subplot +- TradingSignalLayer: Buy/sell/hold signal markers +- TradeExecutionLayer: Trade entry/exit point visualization """ from .base import ( @@ -47,6 +49,22 @@ from .subplots import ( create_common_subplot_indicators ) +from .signals import ( + BaseSignalLayer, + SignalLayerConfig, + TradingSignalLayer, + BaseTradeLayer, + TradeLayerConfig, + TradeExecutionLayer, + create_trading_signal_layer, + create_buy_signals_only_layer, + create_sell_signals_only_layer, + create_high_confidence_signals_layer, + create_trade_execution_layer, + create_profitable_trades_only_layer, + create_losing_trades_only_layer +) + __all__ = [ # Base layers 'BaseChartLayer', @@ -68,6 +86,16 @@ __all__ = [ 'RSILayer', 'MACDLayer', + # Signal layers + 'BaseSignalLayer', + 'SignalLayerConfig', + 'TradingSignalLayer', + + # Trade layers + 'BaseTradeLayer', + 'TradeLayerConfig', + 'TradeExecutionLayer', + # Convenience functions 'create_sma_layer', 'create_ema_layer', @@ -76,7 +104,14 @@ __all__ = [ 'create_common_overlay_indicators', 'create_rsi_layer', 'create_macd_layer', - 'create_common_subplot_indicators' + 'create_common_subplot_indicators', + 'create_trading_signal_layer', + 'create_buy_signals_only_layer', + 'create_sell_signals_only_layer', + 'create_high_confidence_signals_layer', + 'create_trade_execution_layer', + 'create_profitable_trades_only_layer', + 'create_losing_trades_only_layer' ] __version__ = "0.1.0" diff --git a/components/charts/layers/signals.py b/components/charts/layers/signals.py new file mode 100644 index 0000000..d7788b7 --- /dev/null +++ b/components/charts/layers/signals.py @@ -0,0 +1,1009 @@ +""" +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("chart_signals") + + +@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"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"Filtered signals: {len(signals)} -> {len(filtered)} signals") + return filtered + + except Exception as e: + self.logger.error(f"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"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"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"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"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"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"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"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 \ No newline at end of file diff --git a/dashboard/layouts/market_data.py b/dashboard/layouts/market_data.py index 756d2b0..b1c099f 100644 --- a/dashboard/layouts/market_data.py +++ b/dashboard/layouts/market_data.py @@ -25,6 +25,10 @@ def get_market_data_layout(): # Create dropdown options symbol_options = [{'label': symbol, 'value': symbol} for symbol in symbols] timeframe_options = [ + {'label': "1 Second", 'value': '1s'}, + {'label': "5 Seconds", 'value': '5s'}, + {'label': "15 Seconds", 'value': '15s'}, + {'label': "30 Seconds", 'value': '30s'}, {'label': '1 Minute', 'value': '1m'}, {'label': '5 Minutes', 'value': '5m'}, {'label': '15 Minutes', 'value': '15m'}, @@ -34,9 +38,9 @@ def get_market_data_layout(): ] # Filter timeframe options to only show those available in database - available_timeframes = [tf for tf in ['1m', '5m', '15m', '1h', '4h', '1d'] if tf in timeframes] + available_timeframes = [tf for tf in ['1s', '5s', '15s', '30s', '1m', '5m', '15m', '1h', '4h', '1d'] if tf in timeframes] if not available_timeframes: - available_timeframes = ['1h'] # Default fallback + available_timeframes = ['5m'] # Default fallback timeframe_options = [opt for opt in timeframe_options if opt['value'] in available_timeframes] diff --git a/tasks/3.4. Chart layers.md b/tasks/3.4. Chart layers.md index cc1d13a..c0c3de0 100644 --- a/tasks/3.4. Chart layers.md +++ b/tasks/3.4. Chart layers.md @@ -19,7 +19,7 @@ Implementation of a flexible, strategy-driven chart system that supports technic - `components/charts/layers/base.py` - Base layer system with CandlestickLayer, VolumeLayer, and LayerManager - `components/charts/layers/indicators.py` - Indicator overlay rendering (SMA, EMA, Bollinger Bands) - `components/charts/layers/subplots.py` - Subplot management for indicators like RSI and MACD -- `components/charts/layers/signals.py` - Strategy signal overlays and trade markers (future bot integration) +- `components/charts/layers/signals.py` - Strategy signal overlays and trade markers with database integration - `dashboard/` - **NEW: Modular dashboard structure with separated layouts and callbacks** - `dashboard/layouts/market_data.py` - Enhanced market data layout with chart configuration UI - `dashboard/callbacks/charts.py` - **NEW: Modular chart callbacks with strategy handling** @@ -43,6 +43,7 @@ Implementation of a flexible, strategy-driven chart system that supports technic - Backward compatibility maintained with existing `components/charts.py` API - Use `uv run pytest tests/test_chart_*.py` to run chart-specific tests - **Modular dashboard structure implemented with complete separation of concerns** +- **Signal layer architecture implemented with database integration for bot signals** - Create documentation with important components in ./docs/components/charts/ folder without redundancy ## Tasks @@ -85,13 +86,13 @@ Implementation of a flexible, strategy-driven chart system that supports technic - [x] 4.7 Test dashboard integration with real market data - [ ] 5.0 Signal Layer Foundation for Future Bot Integration - - [ ] 5.1 Create signal layer architecture for buy/sell markers - - [ ] 5.2 Implement trade entry/exit point visualization + - [x] 5.1 Create signal layer architecture for buy/sell markers + - [x] 5.2 Implement trade entry/exit point visualization - [ ] 5.3 Add support/resistance line drawing capabilities - [ ] 5.4 Create extensible interface for custom strategy signals - [ ] 5.5 Add signal color and style customization options - [ ] 5.6 Prepare integration points for bot management system - - [ ] 5.7 Create foundation tests for signal layer functionality + - [ ] 5.7 Create foundation tests for signal layer functionality - [ ] 6.0 Documentation **⏳ IN PROGRESS** - [x] 6.1 Create documentation for the chart layers system @@ -116,10 +117,11 @@ Implementation of a flexible, strategy-driven chart system that supports technic - **Chart callbacks**: Updated to handle new layer system with strategy support - **Real-time updates**: Working chart updates with indicator toggling - **Market data integration**: Confirmed working with live data +- **Signal layer architecture**: Complete foundation for bot signal visualization ### 📋 **NEXT PHASES** -- **5.0 Signal Layer**: Foundation for bot signal integration +- **5.2-5.7**: Complete signal layer implementation - **6.0 Documentation**: Complete README and final documentation updates -The chart layers system is now **production-ready** with full dashboard integration! 🚀 +The signal layer foundation is now **implemented and ready** for bot integration! 🚀