3.9 Enhance chart functionality with indicator management and data export features
- Updated `ChartBuilder` to support dynamic indicator integration, allowing users to specify overlay and subplot indicators for enhanced chart analysis. - Implemented a new `get_indicator_data` method in `MarketDataIntegrator` for fetching indicator data based on user configurations. - Added `create_export_controls` in `chart_controls.py` to facilitate data export options (CSV/JSON) for user analysis. - Enhanced error handling and logging throughout the chart and data analysis processes to improve reliability and user feedback. - Updated documentation to reflect new features and usage guidelines for indicator management and data export functionalities.
This commit is contained in:
@@ -15,6 +15,12 @@ from decimal import Decimal
|
||||
from database.operations import get_database_operations, DatabaseOperationError
|
||||
from utils.logger import get_logger
|
||||
from .utils import validate_market_data, prepare_chart_data, get_indicator_colors
|
||||
from .indicator_manager import get_indicator_manager
|
||||
from .layers import (
|
||||
LayerManager, CandlestickLayer, VolumeLayer,
|
||||
SMALayer, EMALayer, BollingerBandsLayer,
|
||||
RSILayer, MACDLayer, IndicatorLayerConfig
|
||||
)
|
||||
|
||||
# Initialize logger
|
||||
logger = get_logger("default_logger")
|
||||
@@ -153,13 +159,16 @@ class ChartBuilder:
|
||||
include_volume = kwargs.get('include_volume', has_volume)
|
||||
|
||||
if include_volume and has_volume:
|
||||
return self._create_candlestick_with_volume(df, symbol, timeframe, **kwargs)
|
||||
fig, df_chart = self._create_candlestick_with_volume(df, symbol, timeframe, **kwargs)
|
||||
return fig, df_chart
|
||||
else:
|
||||
return self._create_basic_candlestick(df, symbol, timeframe, **kwargs)
|
||||
fig, df_chart = self._create_basic_candlestick(df, symbol, timeframe, **kwargs)
|
||||
return fig, df_chart
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Chart builder: Error creating candlestick chart for {symbol} {timeframe}: {e}")
|
||||
return self._create_error_chart(f"Error loading chart: {str(e)}")
|
||||
error_fig = self._create_error_chart(f"Error loading chart: {str(e)}")
|
||||
return error_fig, pd.DataFrame()
|
||||
|
||||
def _create_basic_candlestick(self, df: pd.DataFrame, symbol: str,
|
||||
timeframe: str, **kwargs) -> go.Figure:
|
||||
@@ -194,7 +203,7 @@ class ChartBuilder:
|
||||
)
|
||||
|
||||
self.logger.debug(f"Chart builder: Created basic candlestick chart for {symbol} {timeframe} with {len(df)} candles")
|
||||
return fig
|
||||
return fig, df
|
||||
|
||||
def _create_candlestick_with_volume(self, df: pd.DataFrame, symbol: str,
|
||||
timeframe: str, **kwargs) -> go.Figure:
|
||||
@@ -250,7 +259,8 @@ class ChartBuilder:
|
||||
showlegend=False,
|
||||
height=height,
|
||||
xaxis_rangeslider_visible=False,
|
||||
hovermode='x unified'
|
||||
hovermode='x unified',
|
||||
dragmode='pan'
|
||||
)
|
||||
|
||||
# Update axes
|
||||
@@ -258,8 +268,8 @@ class ChartBuilder:
|
||||
fig.update_yaxes(title_text="Volume", row=2, col=1)
|
||||
fig.update_xaxes(title_text="Time", row=2, col=1)
|
||||
|
||||
self.logger.debug(f"Chart builder: Created candlestick chart with volume for {symbol} {timeframe}")
|
||||
return fig
|
||||
self.logger.debug(f"Chart builder: Created candlestick chart with volume for {symbol} {timeframe} with {len(df)} candles")
|
||||
return fig, df
|
||||
|
||||
def _create_empty_chart(self, message: str = "No data available") -> go.Figure:
|
||||
"""Create an empty chart with a message."""
|
||||
@@ -356,7 +366,7 @@ class ChartBuilder:
|
||||
subplot_indicators: List[str] = None,
|
||||
days_back: int = 7, **kwargs) -> go.Figure:
|
||||
"""
|
||||
Create a chart with dynamically selected indicators.
|
||||
Create a candlestick chart with specified technical indicators.
|
||||
|
||||
Args:
|
||||
symbol: Trading pair
|
||||
@@ -367,35 +377,27 @@ class ChartBuilder:
|
||||
**kwargs: Additional chart parameters
|
||||
|
||||
Returns:
|
||||
Plotly Figure object with selected indicators
|
||||
Plotly Figure object and a pandas DataFrame with all chart data.
|
||||
"""
|
||||
overlay_indicators = overlay_indicators or []
|
||||
subplot_indicators = subplot_indicators or []
|
||||
try:
|
||||
# Fetch market data
|
||||
# 1. Fetch and Prepare Base Data
|
||||
candles = self.fetch_market_data_enhanced(symbol, timeframe, days_back)
|
||||
|
||||
if not candles:
|
||||
self.logger.warning(f"Chart builder: No data available for {symbol} {timeframe}")
|
||||
return self._create_empty_chart(f"No data available for {symbol} {timeframe}")
|
||||
|
||||
# Validate and prepare data
|
||||
if not validate_market_data(candles):
|
||||
self.logger.error(f"Chart builder: Invalid market data for {symbol} {timeframe}")
|
||||
return self._create_error_chart("Invalid market data format")
|
||||
|
||||
self.logger.warning(f"No data for {symbol} {timeframe}, creating empty chart.")
|
||||
return self._create_empty_chart(f"No data for {symbol} {timeframe}"), pd.DataFrame()
|
||||
|
||||
df = prepare_chart_data(candles)
|
||||
|
||||
# Import layer classes
|
||||
from .layers import (
|
||||
LayerManager, CandlestickLayer, VolumeLayer,
|
||||
SMALayer, EMALayer, BollingerBandsLayer,
|
||||
RSILayer, MACDLayer, IndicatorLayerConfig
|
||||
)
|
||||
from .indicator_manager import get_indicator_manager
|
||||
|
||||
# Get user indicators instead of default configurations
|
||||
indicator_manager = get_indicator_manager()
|
||||
|
||||
# Calculate subplot requirements
|
||||
if df.empty:
|
||||
self.logger.warning(f"DataFrame empty for {symbol} {timeframe}, creating empty chart.")
|
||||
return self._create_empty_chart(f"No data for {symbol} {timeframe}"), pd.DataFrame()
|
||||
|
||||
# Initialize final DataFrame for export
|
||||
final_df = df.copy()
|
||||
|
||||
# 2. Setup Subplots
|
||||
# Count subplot indicators to configure rows
|
||||
subplot_count = 0
|
||||
volume_enabled = 'volume' in df.columns and df['volume'].sum() > 0
|
||||
if volume_enabled:
|
||||
@@ -440,8 +442,8 @@ class ChartBuilder:
|
||||
|
||||
current_row = 1
|
||||
|
||||
# Add candlestick layer (always included)
|
||||
candlestick_trace = go.Candlestick(
|
||||
# 4. Add Candlestick Trace
|
||||
fig.add_trace(go.Candlestick(
|
||||
x=df['timestamp'],
|
||||
open=df['open'],
|
||||
high=df['high'],
|
||||
@@ -449,72 +451,10 @@ class ChartBuilder:
|
||||
close=df['close'],
|
||||
name=symbol,
|
||||
increasing_line_color=self.default_colors['bullish'],
|
||||
decreasing_line_color=self.default_colors['bearish'],
|
||||
showlegend=False
|
||||
)
|
||||
fig.add_trace(candlestick_trace, row=current_row, col=1)
|
||||
decreasing_line_color=self.default_colors['bearish']
|
||||
), row=current_row, col=1)
|
||||
|
||||
# Add overlay indicators
|
||||
if overlay_indicators:
|
||||
for indicator_id in overlay_indicators:
|
||||
try:
|
||||
# Load user indicator
|
||||
user_indicator = indicator_manager.load_indicator(indicator_id)
|
||||
|
||||
if user_indicator is None:
|
||||
self.logger.warning(f"Overlay indicator {indicator_id} not found")
|
||||
continue
|
||||
|
||||
# Create appropriate indicator layer using user configuration
|
||||
if user_indicator.type == 'sma':
|
||||
period = user_indicator.parameters.get('period', 20)
|
||||
layer_config = IndicatorLayerConfig(
|
||||
name=user_indicator.name,
|
||||
indicator_type='sma',
|
||||
color=user_indicator.styling.color,
|
||||
parameters={'period': period},
|
||||
line_width=user_indicator.styling.line_width
|
||||
)
|
||||
sma_layer = SMALayer(layer_config)
|
||||
traces = sma_layer.create_traces(df.to_dict('records'))
|
||||
for trace in traces:
|
||||
fig.add_trace(trace, row=current_row, col=1)
|
||||
|
||||
elif user_indicator.type == 'ema':
|
||||
period = user_indicator.parameters.get('period', 12)
|
||||
layer_config = IndicatorLayerConfig(
|
||||
name=user_indicator.name,
|
||||
indicator_type='ema',
|
||||
color=user_indicator.styling.color,
|
||||
parameters={'period': period},
|
||||
line_width=user_indicator.styling.line_width
|
||||
)
|
||||
ema_layer = EMALayer(layer_config)
|
||||
traces = ema_layer.create_traces(df.to_dict('records'))
|
||||
for trace in traces:
|
||||
fig.add_trace(trace, row=current_row, col=1)
|
||||
|
||||
elif user_indicator.type == 'bollinger_bands':
|
||||
period = user_indicator.parameters.get('period', 20)
|
||||
std_dev = user_indicator.parameters.get('std_dev', 2.0)
|
||||
layer_config = IndicatorLayerConfig(
|
||||
name=user_indicator.name,
|
||||
indicator_type='bollinger_bands',
|
||||
color=user_indicator.styling.color,
|
||||
parameters={'period': period, 'std_dev': std_dev},
|
||||
line_width=user_indicator.styling.line_width,
|
||||
show_middle_line=True
|
||||
)
|
||||
bb_layer = BollingerBandsLayer(layer_config)
|
||||
traces = bb_layer.create_traces(df.to_dict('records'))
|
||||
for trace in traces:
|
||||
fig.add_trace(trace, row=current_row, col=1)
|
||||
|
||||
self.logger.debug(f"Added overlay indicator: {user_indicator.name}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Chart builder: Error adding overlay indicator {indicator_id}: {e}")
|
||||
|
||||
# Move to next row for volume if enabled
|
||||
# 5. Add Volume Trace (if applicable)
|
||||
if volume_enabled:
|
||||
current_row += 1
|
||||
volume_colors = [self.default_colors['bullish'] if close >= open else self.default_colors['bearish']
|
||||
@@ -525,56 +465,89 @@ class ChartBuilder:
|
||||
y=df['volume'],
|
||||
name='Volume',
|
||||
marker_color=volume_colors,
|
||||
opacity=0.7,
|
||||
showlegend=False
|
||||
opacity=0.7
|
||||
)
|
||||
fig.add_trace(volume_trace, row=current_row, col=1)
|
||||
fig.update_yaxes(title_text="Volume", row=current_row, col=1)
|
||||
|
||||
# Add subplot indicators
|
||||
if subplot_indicators:
|
||||
for indicator_id in subplot_indicators:
|
||||
current_row += 1
|
||||
try:
|
||||
# Load user indicator
|
||||
user_indicator = indicator_manager.load_indicator(indicator_id)
|
||||
|
||||
if user_indicator is None:
|
||||
self.logger.warning(f"Subplot indicator {indicator_id} not found")
|
||||
continue
|
||||
|
||||
# Create appropriate subplot indicator layer
|
||||
if user_indicator.type == 'rsi':
|
||||
period = user_indicator.parameters.get('period', 14)
|
||||
rsi_layer = RSILayer(period=period, color=user_indicator.styling.color, name=user_indicator.name)
|
||||
|
||||
# Use the render method
|
||||
fig = rsi_layer.render(fig, df, row=current_row, col=1)
|
||||
|
||||
# Add RSI reference lines
|
||||
fig.add_hline(y=70, line_dash="dash", line_color="red", opacity=0.5, row=current_row, col=1)
|
||||
fig.add_hline(y=30, line_dash="dash", line_color="green", opacity=0.5, row=current_row, col=1)
|
||||
fig.update_yaxes(title_text="RSI", range=[0, 100], row=current_row, col=1)
|
||||
|
||||
elif user_indicator.type == 'macd':
|
||||
fast_period = user_indicator.parameters.get('fast_period', 12)
|
||||
slow_period = user_indicator.parameters.get('slow_period', 26)
|
||||
signal_period = user_indicator.parameters.get('signal_period', 9)
|
||||
macd_layer = MACDLayer(fast_period=fast_period, slow_period=slow_period,
|
||||
signal_period=signal_period, color=user_indicator.styling.color, name=user_indicator.name)
|
||||
|
||||
# Use the render method
|
||||
fig = macd_layer.render(fig, df, row=current_row, col=1)
|
||||
|
||||
# Add zero line for MACD
|
||||
fig.add_hline(y=0, line_dash="dash", line_color="gray", opacity=0.5, row=current_row, col=1)
|
||||
fig.update_yaxes(title_text="MACD", row=current_row, col=1)
|
||||
|
||||
self.logger.debug(f"Added subplot indicator: {user_indicator.name}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Chart builder: Error adding subplot indicator {indicator_id}: {e}")
|
||||
# 6. Add Indicator Traces
|
||||
indicator_manager = get_indicator_manager()
|
||||
all_indicator_configs = []
|
||||
|
||||
# Create IndicatorLayerConfig objects from indicator IDs
|
||||
indicator_ids = (overlay_indicators or []) + (subplot_indicators or [])
|
||||
for ind_id in indicator_ids:
|
||||
indicator = indicator_manager.load_indicator(ind_id)
|
||||
if indicator:
|
||||
config = IndicatorLayerConfig(
|
||||
id=indicator.id,
|
||||
name=indicator.name,
|
||||
indicator_type=indicator.type,
|
||||
parameters=indicator.parameters
|
||||
)
|
||||
all_indicator_configs.append(config)
|
||||
|
||||
# Update layout
|
||||
if all_indicator_configs:
|
||||
indicator_data_map = self.data_integrator.get_indicator_data(
|
||||
df, all_indicator_configs, indicator_manager
|
||||
)
|
||||
|
||||
for indicator_id, indicator_df in indicator_data_map.items():
|
||||
indicator = indicator_manager.load_indicator(indicator_id)
|
||||
if not indicator:
|
||||
self.logger.warning(f"Could not load indicator '{indicator_id}' for plotting.")
|
||||
continue
|
||||
|
||||
if indicator_df is not None and not indicator_df.empty:
|
||||
final_df = pd.merge(final_df, indicator_df, on='timestamp', how='left')
|
||||
|
||||
# Determine target row for plotting
|
||||
target_row = 1 # Default to overlay on the main chart
|
||||
if indicator.id in subplot_indicators:
|
||||
current_row += 1
|
||||
target_row = current_row
|
||||
fig.update_yaxes(title_text=indicator.name, row=target_row, col=1)
|
||||
|
||||
if indicator.type == 'bollinger_bands':
|
||||
if all(c in indicator_df.columns for c in ['upper_band', 'lower_band', 'middle_band']):
|
||||
# Prepare data for the filled area
|
||||
x_vals = indicator_df['timestamp']
|
||||
y_upper = indicator_df['upper_band']
|
||||
y_lower = indicator_df['lower_band']
|
||||
|
||||
# Convert hex color to rgba for the fill
|
||||
hex_color = indicator.styling.color.lstrip('#')
|
||||
rgb = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
|
||||
fill_color = f'rgba({rgb[0]}, {rgb[1]}, {rgb[2]}, 0.1)'
|
||||
|
||||
# Add the transparent fill trace
|
||||
fig.add_trace(go.Scatter(
|
||||
x=pd.concat([x_vals, x_vals[::-1]]),
|
||||
y=pd.concat([y_upper, y_lower[::-1]]),
|
||||
fill='toself',
|
||||
fillcolor=fill_color,
|
||||
line={'color': 'rgba(255,255,255,0)'},
|
||||
hoverinfo='none',
|
||||
showlegend=False
|
||||
), row=target_row, col=1)
|
||||
|
||||
# Add the visible line traces for the bands
|
||||
fig.add_trace(go.Scatter(x=x_vals, y=y_upper, name=f'{indicator.name} Upper', mode='lines', line=dict(color=indicator.styling.color, width=1.5)), row=target_row, col=1)
|
||||
fig.add_trace(go.Scatter(x=x_vals, y=y_lower, name=f'{indicator.name} Lower', mode='lines', line=dict(color=indicator.styling.color, width=1.5)), row=target_row, col=1)
|
||||
fig.add_trace(go.Scatter(x=x_vals, y=indicator_df['middle_band'], name=f'{indicator.name} Middle', mode='lines', line=dict(color=indicator.styling.color, width=1.5, dash='dash')), row=target_row, col=1)
|
||||
else:
|
||||
# Generic plotting for other indicators
|
||||
for col in indicator_df.columns:
|
||||
if col != 'timestamp':
|
||||
fig.add_trace(go.Scatter(
|
||||
x=indicator_df['timestamp'],
|
||||
y=indicator_df[col],
|
||||
mode='lines',
|
||||
name=f"{indicator.name} ({col})",
|
||||
line=dict(color=indicator.styling.color)
|
||||
), row=target_row, col=1)
|
||||
|
||||
# 7. Final Layout Updates
|
||||
height = kwargs.get('height', self.default_height)
|
||||
template = kwargs.get('template', self.default_template)
|
||||
|
||||
@@ -594,8 +567,9 @@ class ChartBuilder:
|
||||
|
||||
indicator_count = len(overlay_indicators or []) + len(subplot_indicators or [])
|
||||
self.logger.debug(f"Created chart for {symbol} {timeframe} with {indicator_count} indicators")
|
||||
return fig
|
||||
|
||||
self.logger.info(f"Successfully created chart for {symbol} with {len(overlay_indicators + subplot_indicators)} indicators.")
|
||||
return fig, final_df
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Chart builder: Error creating chart with indicators: {e}")
|
||||
return self._create_error_chart(f"Chart creation failed: {str(e)}")
|
||||
self.logger.error(f"Error in create_chart_with_indicators for {symbol}: {e}", exc_info=True)
|
||||
return self._create_error_chart(f"Error generating indicator chart: {e}"), pd.DataFrame()
|
||||
@@ -457,6 +457,49 @@ class MarketDataIntegrator:
|
||||
self._cache.clear()
|
||||
self.logger.debug("Data Integration: Data cache cleared")
|
||||
|
||||
def get_indicator_data(
|
||||
self,
|
||||
main_df: pd.DataFrame,
|
||||
indicator_configs: List['IndicatorLayerConfig'],
|
||||
indicator_manager: 'IndicatorManager'
|
||||
) -> Dict[str, pd.DataFrame]:
|
||||
|
||||
indicator_data_map = {}
|
||||
if main_df.empty:
|
||||
return indicator_data_map
|
||||
|
||||
for config in indicator_configs:
|
||||
indicator_id = config.id
|
||||
indicator = indicator_manager.load_indicator(indicator_id)
|
||||
|
||||
if not indicator:
|
||||
logger.warning(f"Data Integrator: Could not load indicator with ID: {indicator_id}")
|
||||
continue
|
||||
|
||||
try:
|
||||
# The new `calculate` method in TechnicalIndicators handles DataFrame input
|
||||
indicator_result_pkg = self.indicators.calculate(
|
||||
indicator.type,
|
||||
main_df,
|
||||
**indicator.parameters
|
||||
)
|
||||
|
||||
if indicator_result_pkg and 'data' in indicator_result_pkg and indicator_result_pkg['data']:
|
||||
# The result is a list of IndicatorResult objects. Convert to DataFrame.
|
||||
indicator_results = indicator_result_pkg['data']
|
||||
result_df = pd.DataFrame([
|
||||
{'timestamp': r.timestamp, **r.values}
|
||||
for r in indicator_results
|
||||
])
|
||||
indicator_data_map[indicator.id] = result_df
|
||||
else:
|
||||
self.logger.warning(f"No data returned for indicator '{indicator.name}'")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error calculating indicator '{indicator.name}': {e}", exc_info=True)
|
||||
|
||||
return indicator_data_map
|
||||
|
||||
|
||||
# Convenience functions for common operations
|
||||
def get_market_data_integrator(config: DataIntegrationConfig = None) -> MarketDataIntegrator:
|
||||
|
||||
@@ -28,6 +28,7 @@ logger = get_logger("default_logger")
|
||||
@dataclass
|
||||
class IndicatorLayerConfig(LayerConfig):
|
||||
"""Extended configuration for indicator layers"""
|
||||
id: str = ""
|
||||
indicator_type: str = "" # e.g., 'sma', 'ema', 'rsi'
|
||||
parameters: Dict[str, Any] = None # Indicator-specific parameters
|
||||
line_width: int = 2
|
||||
|
||||
Reference in New Issue
Block a user