2977 lines
110 KiB
Python
2977 lines
110 KiB
Python
"""
|
|
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("<br>".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("<br>".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}<br>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 = "<br>".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("<br>".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"<b>{signal_type.upper()}</b>",
|
|
f"Price: <b>${signal['price']:.4f}</b>",
|
|
f"Time: {signal['timestamp']}"
|
|
]
|
|
|
|
if 'confidence' in signal and signal['confidence'] is not None:
|
|
confidence = float(signal['confidence'])
|
|
hover_parts.append(f"Confidence: <b>{confidence:.1%}</b>")
|
|
|
|
if 'reason' in signal and signal['reason']:
|
|
hover_parts.append(f"Reason: {signal['reason']}")
|
|
|
|
hover_texts.append("<br>".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}<extra></extra>',
|
|
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) |