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:
Vasily.onl
2025-06-06 12:57:35 +08:00
parent 8572a7a387
commit c121b469f0
10 changed files with 512 additions and 654 deletions

View File

@@ -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()

View File

@@ -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:

View File

@@ -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