3.4 Enhance logging and modular chart system for Crypto Trading Bot Dashboard

- Suppressed SQLAlchemy logging in `app.py` and `main.py` to reduce console verbosity.
- Introduced a new modular chart system in `components/charts/` with a `ChartBuilder` class for flexible chart creation.
- Added utility functions for data processing and validation in `components/charts/utils.py`.
- Implemented indicator definitions and configurations in `components/charts/config/indicator_defs.py`.
- Created a comprehensive documentation structure for the new chart system, ensuring clarity and maintainability.
- Added unit tests for the `ChartBuilder` class to verify functionality and robustness.
- Updated existing components to integrate with the new chart system, enhancing overall architecture and user experience.
This commit is contained in:
Vasily.onl
2025-06-03 12:49:46 +08:00
parent 720002a441
commit c4ec3fac9f
12 changed files with 1637 additions and 411 deletions

View File

@@ -1,347 +1,116 @@
"""
Chart and Visualization Components
Chart and Visualization Components - Redirect to New System
This module provides chart components for market data visualization,
including candlestick charts, technical indicators, and real-time updates.
This module redirects to the new modular chart system in components/charts/.
For new development, use the ChartBuilder class directly from components.charts.
"""
import plotly.graph_objects as go
import plotly.express as px
from plotly.subplots import make_subplots
import pandas as pd
from datetime import datetime, timedelta, timezone
from typing import List, Dict, Any, Optional
from decimal import Decimal
# Import and re-export the new modular chart system for simple migration
from .charts import (
ChartBuilder,
create_candlestick_chart,
create_strategy_chart,
validate_market_data,
prepare_chart_data,
get_indicator_colors
)
from database.operations import get_database_operations, DatabaseOperationError
from utils.logger import get_logger
from .charts.config import (
get_available_indicators,
calculate_indicators,
get_overlay_indicators,
get_subplot_indicators,
get_indicator_display_config
)
# Initialize logger
logger = get_logger("charts_component")
def fetch_market_data(symbol: str, timeframe: str,
days_back: int = 7, exchange: str = "okx") -> List[Dict[str, Any]]:
"""
Fetch market data from the database for chart display.
Args:
symbol: Trading pair (e.g., 'BTC-USDT')
timeframe: Timeframe (e.g., '1h', '1d')
days_back: Number of days to look back
exchange: Exchange name
# Convenience functions for common operations
def get_supported_symbols():
"""Get list of symbols that have data in the database."""
builder = ChartBuilder()
candles = builder.fetch_market_data("BTC-USDT", "1m", days_back=1) # Test query
if candles:
from database.operations import get_database_operations
from utils.logger import get_logger
logger = get_logger("charts_symbols")
Returns:
List of candle data dictionaries
"""
try:
db = get_database_operations(logger)
with db.market_data.get_session() as session:
from sqlalchemy import text
result = session.execute(text("SELECT DISTINCT symbol FROM market_data ORDER BY symbol"))
return [row[0] for row in result]
except Exception:
pass
return ['BTC-USDT', 'ETH-USDT'] # Fallback
def get_supported_timeframes():
"""Get list of timeframes that have data in the database."""
builder = ChartBuilder()
candles = builder.fetch_market_data("BTC-USDT", "1m", days_back=1) # Test query
if candles:
from database.operations import get_database_operations
from utils.logger import get_logger
logger = get_logger("charts_timeframes")
try:
db = get_database_operations(logger)
with db.market_data.get_session() as session:
from sqlalchemy import text
result = session.execute(text("SELECT DISTINCT timeframe FROM market_data ORDER BY timeframe"))
return [row[0] for row in result]
except Exception:
pass
return ['5s', '1m', '15m', '1h'] # Fallback
# Legacy function names for compatibility during transition
get_available_technical_indicators = get_available_indicators
fetch_market_data = lambda symbol, timeframe, days_back=7, exchange="okx": ChartBuilder().fetch_market_data(symbol, timeframe, days_back, exchange)
create_candlestick_with_volume = lambda df, symbol, timeframe: create_candlestick_chart(symbol, timeframe)
create_empty_chart = lambda message="No data available": ChartBuilder()._create_empty_chart(message)
create_error_chart = lambda error_message: ChartBuilder()._create_error_chart(error_message)
def get_market_statistics(symbol: str, timeframe: str = "1h"):
"""Calculate market statistics from recent data."""
builder = ChartBuilder()
candles = builder.fetch_market_data(symbol, timeframe, days_back=1)
if not candles:
return {'Price': 'N/A', '24h Change': 'N/A', '24h Volume': 'N/A', 'High 24h': 'N/A', 'Low 24h': 'N/A'}
import pandas as pd
df = pd.DataFrame(candles)
latest = df.iloc[-1]
current_price = float(latest['close'])
# Calculate 24h change
if len(df) > 1:
price_24h_ago = float(df.iloc[0]['open'])
change_percent = ((current_price - price_24h_ago) / price_24h_ago) * 100
else:
change_percent = 0
from .charts.utils import format_price, format_volume
return {
'Price': format_price(current_price, decimals=2),
'24h Change': f"{'+' if change_percent >= 0 else ''}{change_percent:.2f}%",
'24h Volume': format_volume(df['volume'].sum()),
'High 24h': format_price(df['high'].max(), decimals=2),
'Low 24h': format_price(df['low'].min(), decimals=2)
}
def check_data_availability(symbol: str, timeframe: str):
"""Check data availability for a symbol and timeframe."""
from datetime import datetime, timezone, timedelta
from database.operations import get_database_operations
from utils.logger import get_logger
try:
logger = get_logger("charts_data_check")
db = get_database_operations(logger)
# Calculate time range
end_time = datetime.now(timezone.utc)
start_time = end_time - timedelta(days=days_back)
# Fetch candles from database using the proper API
candles = db.market_data.get_candles(
symbol=symbol,
timeframe=timeframe,
start_time=start_time,
end_time=end_time,
exchange=exchange
)
logger.debug(f"Fetched {len(candles)} candles for {symbol} {timeframe}")
return candles
except DatabaseOperationError as e:
logger.error(f"Database error fetching market data: {e}")
return []
except Exception as e:
logger.error(f"Unexpected error fetching market data: {e}")
return []
def create_candlestick_chart(symbol: str, timeframe: str,
candles: Optional[List[Dict[str, Any]]] = None) -> go.Figure:
"""
Create a candlestick chart with real market data.
Args:
symbol: Trading pair
timeframe: Timeframe
candles: Optional pre-fetched candle data
Returns:
Plotly Figure object
"""
try:
# Fetch data if not provided
if candles is None:
candles = fetch_market_data(symbol, timeframe)
# Handle empty data
if not candles:
logger.warning(f"No data available for {symbol} {timeframe}")
return create_empty_chart(f"No data available for {symbol} {timeframe}")
# Convert to DataFrame for easier manipulation
df = pd.DataFrame(candles)
# Ensure timestamp column is datetime
df['timestamp'] = pd.to_datetime(df['timestamp'])
# Sort by timestamp
df = df.sort_values('timestamp')
# Create candlestick chart
fig = go.Figure(data=go.Candlestick(
x=df['timestamp'],
open=df['open'],
high=df['high'],
low=df['low'],
close=df['close'],
name=symbol,
increasing_line_color='#26a69a',
decreasing_line_color='#ef5350'
))
# Update layout
fig.update_layout(
title=f"{symbol} - {timeframe} Chart",
xaxis_title="Time",
yaxis_title="Price (USDT)",
template="plotly_white",
showlegend=False,
height=600,
xaxis_rangeslider_visible=False,
hovermode='x unified'
)
# Add volume subplot if volume data exists
if 'volume' in df.columns and df['volume'].sum() > 0:
fig = create_candlestick_with_volume(df, symbol, timeframe)
logger.debug(f"Created candlestick chart for {symbol} {timeframe} with {len(df)} candles")
return fig
except Exception as e:
logger.error(f"Error creating candlestick chart for {symbol} {timeframe}: {e}")
return create_error_chart(f"Error loading chart: {str(e)}")
def create_candlestick_with_volume(df: pd.DataFrame, symbol: str, timeframe: str) -> go.Figure:
"""
Create a candlestick chart with volume subplot.
Args:
df: DataFrame with OHLCV data
symbol: Trading pair
timeframe: Timeframe
Returns:
Plotly Figure with candlestick and volume
"""
# Create subplots
fig = make_subplots(
rows=2, cols=1,
shared_xaxes=True,
vertical_spacing=0.03,
subplot_titles=(f'{symbol} Price', 'Volume'),
row_width=[0.7, 0.3]
)
# Add candlestick chart
fig.add_trace(
go.Candlestick(
x=df['timestamp'],
open=df['open'],
high=df['high'],
low=df['low'],
close=df['close'],
name=symbol,
increasing_line_color='#26a69a',
decreasing_line_color='#ef5350'
),
row=1, col=1
)
# Add volume bars
colors = ['#26a69a' if close >= open else '#ef5350'
for close, open in zip(df['close'], df['open'])]
fig.add_trace(
go.Bar(
x=df['timestamp'],
y=df['volume'],
name='Volume',
marker_color=colors,
opacity=0.7
),
row=2, col=1
)
# Update layout
fig.update_layout(
title=f"{symbol} - {timeframe} Chart with Volume",
template="plotly_white",
showlegend=False,
height=700,
xaxis_rangeslider_visible=False,
hovermode='x unified'
)
# Update axes
fig.update_yaxes(title_text="Price (USDT)", row=1, col=1)
fig.update_yaxes(title_text="Volume", row=2, col=1)
fig.update_xaxes(title_text="Time", row=2, col=1)
return fig
def create_empty_chart(message: str = "No data available") -> go.Figure:
"""
Create an empty chart with a message.
Args:
message: Message to display
Returns:
Empty Plotly Figure
"""
fig = go.Figure()
fig.add_annotation(
text=message,
xref="paper", yref="paper",
x=0.5, y=0.5,
xanchor='center', yanchor='middle',
showarrow=False,
font=dict(size=16, color="#7f8c8d")
)
fig.update_layout(
template="plotly_white",
height=600,
showlegend=False,
xaxis=dict(visible=False),
yaxis=dict(visible=False)
)
return fig
def create_error_chart(error_message: str) -> go.Figure:
"""
Create an error chart with error message.
Args:
error_message: Error message to display
Returns:
Error Plotly Figure
"""
fig = go.Figure()
fig.add_annotation(
text=f"⚠️ {error_message}",
xref="paper", yref="paper",
x=0.5, y=0.5,
xanchor='center', yanchor='middle',
showarrow=False,
font=dict(size=16, color="#e74c3c")
)
fig.update_layout(
template="plotly_white",
height=600,
showlegend=False,
xaxis=dict(visible=False),
yaxis=dict(visible=False)
)
return fig
def get_market_statistics(symbol: str, timeframe: str = "1h") -> Dict[str, str]:
"""
Calculate market statistics from recent data.
Args:
symbol: Trading pair
timeframe: Timeframe for calculations
Returns:
Dictionary of market statistics
"""
try:
# Fetch recent data for statistics
candles = fetch_market_data(symbol, timeframe, days_back=1)
if not candles:
return {
'Price': 'N/A',
'24h Change': 'N/A',
'24h Volume': 'N/A',
'High 24h': 'N/A',
'Low 24h': 'N/A'
}
# Convert to DataFrame
df = pd.DataFrame(candles)
# Get latest and 24h ago prices
latest_candle = df.iloc[-1]
current_price = float(latest_candle['close'])
# Calculate 24h change
if len(df) > 1:
price_24h_ago = float(df.iloc[0]['open'])
change_24h = current_price - price_24h_ago
change_percent = (change_24h / price_24h_ago) * 100
else:
change_24h = 0
change_percent = 0
# Calculate volume and high/low
total_volume = df['volume'].sum()
high_24h = df['high'].max()
low_24h = df['low'].min()
# Format statistics
return {
'Price': f"${current_price:,.2f}",
'24h Change': f"{'+' if change_24h >= 0 else ''}{change_percent:.2f}%",
'24h Volume': f"{total_volume:,.2f}",
'High 24h': f"${float(high_24h):,.2f}",
'Low 24h': f"${float(low_24h):,.2f}"
}
except Exception as e:
logger.error(f"Error calculating market statistics for {symbol}: {e}")
return {
'Price': 'Error',
'24h Change': 'Error',
'24h Volume': 'Error',
'High 24h': 'Error',
'Low 24h': 'Error'
}
def check_data_availability(symbol: str, timeframe: str) -> Dict[str, Any]:
"""
Check data availability for a symbol and timeframe.
Args:
symbol: Trading pair
timeframe: Timeframe
Returns:
Dictionary with data availability information
"""
try:
db = get_database_operations(logger)
# Get latest candle using the proper API
latest_candle = db.market_data.get_latest_candle(symbol, timeframe)
if latest_candle:
@@ -363,93 +132,25 @@ def check_data_availability(symbol: str, timeframe: str) -> Dict[str, Any]:
'is_recent': False,
'message': f"No data available for {symbol} {timeframe}"
}
except Exception as e:
logger.error(f"Error checking data availability for {symbol} {timeframe}: {e}")
return {
'has_data': False,
'latest_timestamp': None,
'latest_timestamp': None,
'time_since_last': None,
'is_recent': False,
'message': f"Error checking data: {str(e)}"
}
def create_data_status_indicator(symbol: str, timeframe: str) -> str:
"""
Create a data status indicator for the dashboard.
Args:
symbol: Trading pair
timeframe: Timeframe
Returns:
HTML string for status indicator
"""
def create_data_status_indicator(symbol: str, timeframe: str):
"""Create a data status indicator for the dashboard."""
status = check_data_availability(symbol, timeframe)
if status['has_data']:
if status['is_recent']:
icon = "🟢"
color = "#27ae60"
status_text = "Real-time Data"
icon, color, status_text = "🟢", "#27ae60", "Real-time Data"
else:
icon = "🟡"
color = "#f39c12"
status_text = "Delayed Data"
icon, color, status_text = "🟡", "#f39c12", "Delayed Data"
else:
icon = "🔴"
color = "#e74c3c"
status_text = "No Data"
icon, color, status_text = "🔴", "#e74c3c", "No Data"
return f'<span style="color: {color}; font-weight: bold;">{icon} {status_text}</span><br><small>{status["message"]}</small>'
def get_supported_symbols() -> List[str]:
"""
Get list of symbols that have data in the database.
Returns:
List of available trading pairs
"""
try:
db = get_database_operations(logger)
with db.market_data.get_session() as session:
# Query distinct symbols from market_data table
from sqlalchemy import text
result = session.execute(text("SELECT DISTINCT symbol FROM market_data ORDER BY symbol"))
symbols = [row[0] for row in result]
logger.debug(f"Found {len(symbols)} symbols in database: {symbols}")
return symbols
except Exception as e:
logger.error(f"Error fetching supported symbols: {e}")
# Return default symbols if database query fails
return ['BTC-USDT', 'ETH-USDT', 'LTC-USDT']
def get_supported_timeframes() -> List[str]:
"""
Get list of timeframes that have data in the database.
Returns:
List of available timeframes
"""
try:
db = get_database_operations(logger)
with db.market_data.get_session() as session:
# Query distinct timeframes from market_data table
from sqlalchemy import text
result = session.execute(text("SELECT DISTINCT timeframe FROM market_data ORDER BY timeframe"))
timeframes = [row[0] for row in result]
logger.debug(f"Found {len(timeframes)} timeframes in database: {timeframes}")
return timeframes
except Exception as e:
logger.error(f"Error fetching supported timeframes: {e}")
# Return default timeframes if database query fails
return ['1m', '5m', '15m', '1h', '4h', '1d']
return f'<span style="color: {color}; font-weight: bold;">{icon} {status_text}</span><br><small>{status["message"]}</small>'

View File

@@ -0,0 +1,200 @@
"""
Modular Chart System for Crypto Trading Bot Dashboard
This package provides a flexible, strategy-driven chart system that supports:
- Technical indicator overlays (SMA, EMA, Bollinger Bands)
- Subplot management (RSI, MACD)
- Strategy-specific configurations
- Future bot signal integration
Main Components:
- ChartBuilder: Main orchestrator for chart creation
- Layer System: Modular rendering components
- Configuration System: Strategy-driven chart configs
"""
from .builder import ChartBuilder
from .utils import (
validate_market_data,
prepare_chart_data,
get_indicator_colors
)
# Version information
__version__ = "0.1.0"
__package_name__ = "charts"
# Public API exports
__all__ = [
"ChartBuilder",
"validate_market_data",
"prepare_chart_data",
"get_indicator_colors",
"create_candlestick_chart",
"create_strategy_chart",
"get_supported_symbols",
"get_supported_timeframes",
"get_market_statistics",
"check_data_availability",
"create_data_status_indicator",
"create_error_chart"
]
def create_candlestick_chart(symbol: str, timeframe: str, days_back: int = 7, **kwargs):
"""
Convenience function to create a basic candlestick chart.
Args:
symbol: Trading pair (e.g., 'BTC-USDT')
timeframe: Timeframe (e.g., '1h', '1d')
days_back: Number of days to look back
**kwargs: Additional parameters for chart customization
Returns:
Plotly Figure object
"""
builder = ChartBuilder()
return builder.create_candlestick_chart(symbol, timeframe, days_back, **kwargs)
def create_strategy_chart(symbol: str, timeframe: str, strategy_name: str, **kwargs):
"""
Convenience function to create a strategy-specific chart.
Args:
symbol: Trading pair
timeframe: Timeframe
strategy_name: Name of the strategy configuration
**kwargs: Additional parameters
Returns:
Plotly Figure object with strategy indicators
"""
builder = ChartBuilder()
return builder.create_strategy_chart(symbol, timeframe, strategy_name, **kwargs)
def get_supported_symbols():
"""Get list of symbols that have data in the database."""
builder = ChartBuilder()
candles = builder.fetch_market_data("BTC-USDT", "1m", days_back=1) # Test query
if candles:
from database.operations import get_database_operations
from utils.logger import get_logger
logger = get_logger("charts_symbols")
try:
db = get_database_operations(logger)
with db.market_data.get_session() as session:
from sqlalchemy import text
result = session.execute(text("SELECT DISTINCT symbol FROM market_data ORDER BY symbol"))
return [row[0] for row in result]
except Exception:
pass
return ['BTC-USDT', 'ETH-USDT'] # Fallback
def get_supported_timeframes():
"""Get list of timeframes that have data in the database."""
builder = ChartBuilder()
candles = builder.fetch_market_data("BTC-USDT", "1m", days_back=1) # Test query
if candles:
from database.operations import get_database_operations
from utils.logger import get_logger
logger = get_logger("charts_timeframes")
try:
db = get_database_operations(logger)
with db.market_data.get_session() as session:
from sqlalchemy import text
result = session.execute(text("SELECT DISTINCT timeframe FROM market_data ORDER BY timeframe"))
return [row[0] for row in result]
except Exception:
pass
return ['5s', '1m', '15m', '1h'] # Fallback
def get_market_statistics(symbol: str, timeframe: str = "1h"):
"""Calculate market statistics from recent data."""
builder = ChartBuilder()
candles = builder.fetch_market_data(symbol, timeframe, days_back=1)
if not candles:
return {'Price': 'N/A', '24h Change': 'N/A', '24h Volume': 'N/A', 'High 24h': 'N/A', 'Low 24h': 'N/A'}
import pandas as pd
df = pd.DataFrame(candles)
latest = df.iloc[-1]
current_price = float(latest['close'])
# Calculate 24h change
if len(df) > 1:
price_24h_ago = float(df.iloc[0]['open'])
change_percent = ((current_price - price_24h_ago) / price_24h_ago) * 100
else:
change_percent = 0
from .utils import format_price, format_volume
return {
'Price': format_price(current_price, decimals=2),
'24h Change': f"{'+' if change_percent >= 0 else ''}{change_percent:.2f}%",
'24h Volume': format_volume(df['volume'].sum()),
'High 24h': format_price(df['high'].max(), decimals=2),
'Low 24h': format_price(df['low'].min(), decimals=2)
}
def check_data_availability(symbol: str, timeframe: str):
"""Check data availability for a symbol and timeframe."""
from datetime import datetime, timezone, timedelta
from database.operations import get_database_operations
from utils.logger import get_logger
try:
logger = get_logger("charts_data_check")
db = get_database_operations(logger)
latest_candle = db.market_data.get_latest_candle(symbol, timeframe)
if latest_candle:
latest_time = latest_candle['timestamp']
time_diff = datetime.now(timezone.utc) - latest_time.replace(tzinfo=timezone.utc)
return {
'has_data': True,
'latest_timestamp': latest_time,
'time_since_last': time_diff,
'is_recent': time_diff < timedelta(hours=1),
'message': f"Latest data: {latest_time.strftime('%Y-%m-%d %H:%M:%S UTC')}"
}
else:
return {
'has_data': False,
'latest_timestamp': None,
'time_since_last': None,
'is_recent': False,
'message': f"No data available for {symbol} {timeframe}"
}
except Exception as e:
return {
'has_data': False,
'latest_timestamp': None,
'time_since_last': None,
'is_recent': False,
'message': f"Error checking data: {str(e)}"
}
def create_data_status_indicator(symbol: str, timeframe: str):
"""Create a data status indicator for the dashboard."""
status = check_data_availability(symbol, timeframe)
if status['has_data']:
if status['is_recent']:
icon, color, status_text = "🟢", "#27ae60", "Real-time Data"
else:
icon, color, status_text = "🟡", "#f39c12", "Delayed Data"
else:
icon, color, status_text = "🔴", "#e74c3c", "No Data"
return f'<span style="color: {color}; font-weight: bold;">{icon} {status_text}</span><br><small>{status["message"]}</small>'
def create_error_chart(error_message: str):
"""Create an error chart with error message."""
builder = ChartBuilder()
return builder._create_error_chart(error_message)

View File

@@ -0,0 +1,291 @@
"""
ChartBuilder - Main orchestrator for chart creation
This module contains the ChartBuilder class which serves as the main entry point
for creating charts with various configurations, indicators, and layers.
"""
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import pandas as pd
from datetime import datetime, timedelta, timezone
from typing import List, Dict, Any, Optional, Union
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
# Initialize logger
logger = get_logger("chart_builder")
class ChartBuilder:
"""
Main chart builder class for creating modular, configurable charts.
This class orchestrates the creation of charts by coordinating between
data fetching, layer rendering, and configuration management.
"""
def __init__(self, logger_instance: Optional = None):
"""
Initialize the ChartBuilder.
Args:
logger_instance: Optional logger instance
"""
self.logger = logger_instance or logger
self.db_ops = get_database_operations(self.logger)
# Chart styling defaults
self.default_colors = get_indicator_colors()
self.default_height = 600
self.default_template = "plotly_white"
def fetch_market_data(self, symbol: str, timeframe: str,
days_back: int = 7, exchange: str = "okx") -> List[Dict[str, Any]]:
"""
Fetch market data from the database.
Args:
symbol: Trading pair (e.g., 'BTC-USDT')
timeframe: Timeframe (e.g., '1h', '1d')
days_back: Number of days to look back
exchange: Exchange name
Returns:
List of candle data dictionaries
"""
try:
# Calculate time range
end_time = datetime.now(timezone.utc)
start_time = end_time - timedelta(days=days_back)
# Fetch candles using the database operations API
candles = self.db_ops.market_data.get_candles(
symbol=symbol,
timeframe=timeframe,
start_time=start_time,
end_time=end_time,
exchange=exchange
)
self.logger.debug(f"Fetched {len(candles)} candles for {symbol} {timeframe}")
return candles
except DatabaseOperationError as e:
self.logger.error(f"Database error fetching market data: {e}")
return []
except Exception as e:
self.logger.error(f"Unexpected error fetching market data: {e}")
return []
def create_candlestick_chart(self, symbol: str, timeframe: str,
days_back: int = 7, **kwargs) -> go.Figure:
"""
Create a basic candlestick chart.
Args:
symbol: Trading pair
timeframe: Timeframe
days_back: Number of days to look back
**kwargs: Additional chart parameters
Returns:
Plotly Figure object with candlestick chart
"""
try:
# Fetch market data
candles = self.fetch_market_data(symbol, timeframe, days_back)
# Handle empty data
if not candles:
self.logger.warning(f"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"Invalid market data for {symbol} {timeframe}")
return self._create_error_chart("Invalid market data format")
# Prepare chart data
df = prepare_chart_data(candles)
# Determine if we need volume subplot
has_volume = 'volume' in df.columns and df['volume'].sum() > 0
include_volume = kwargs.get('include_volume', has_volume)
if include_volume and has_volume:
return self._create_candlestick_with_volume(df, symbol, timeframe, **kwargs)
else:
return self._create_basic_candlestick(df, symbol, timeframe, **kwargs)
except Exception as e:
self.logger.error(f"Error creating candlestick chart for {symbol} {timeframe}: {e}")
return self._create_error_chart(f"Error loading chart: {str(e)}")
def _create_basic_candlestick(self, df: pd.DataFrame, symbol: str,
timeframe: str, **kwargs) -> go.Figure:
"""Create a basic candlestick chart without volume."""
# Get custom parameters
height = kwargs.get('height', self.default_height)
template = kwargs.get('template', self.default_template)
# Create candlestick chart
fig = go.Figure(data=go.Candlestick(
x=df['timestamp'],
open=df['open'],
high=df['high'],
low=df['low'],
close=df['close'],
name=symbol,
increasing_line_color=self.default_colors['bullish'],
decreasing_line_color=self.default_colors['bearish']
))
# Update layout
fig.update_layout(
title=f"{symbol} - {timeframe} Chart",
xaxis_title="Time",
yaxis_title="Price (USDT)",
template=template,
showlegend=False,
height=height,
xaxis_rangeslider_visible=False,
hovermode='x unified'
)
self.logger.debug(f"Created basic candlestick chart for {symbol} {timeframe} with {len(df)} candles")
return fig
def _create_candlestick_with_volume(self, df: pd.DataFrame, symbol: str,
timeframe: str, **kwargs) -> go.Figure:
"""Create a candlestick chart with volume subplot."""
# Get custom parameters
height = kwargs.get('height', 700) # Taller for volume subplot
template = kwargs.get('template', self.default_template)
# Create subplots
fig = make_subplots(
rows=2, cols=1,
shared_xaxes=True,
vertical_spacing=0.03,
subplot_titles=(f'{symbol} Price', 'Volume'),
row_heights=[0.7, 0.3] # 70% for price, 30% for volume
)
# Add candlestick chart
fig.add_trace(
go.Candlestick(
x=df['timestamp'],
open=df['open'],
high=df['high'],
low=df['low'],
close=df['close'],
name=symbol,
increasing_line_color=self.default_colors['bullish'],
decreasing_line_color=self.default_colors['bearish']
),
row=1, col=1
)
# Add volume bars with color coding
colors = [self.default_colors['bullish'] if close >= open else self.default_colors['bearish']
for close, open in zip(df['close'], df['open'])]
fig.add_trace(
go.Bar(
x=df['timestamp'],
y=df['volume'],
name='Volume',
marker_color=colors,
opacity=0.7
),
row=2, col=1
)
# Update layout
fig.update_layout(
title=f"{symbol} - {timeframe} Chart with Volume",
template=template,
showlegend=False,
height=height,
xaxis_rangeslider_visible=False,
hovermode='x unified'
)
# Update axes
fig.update_yaxes(title_text="Price (USDT)", row=1, col=1)
fig.update_yaxes(title_text="Volume", row=2, col=1)
fig.update_xaxes(title_text="Time", row=2, col=1)
self.logger.debug(f"Created candlestick chart with volume for {symbol} {timeframe}")
return fig
def _create_empty_chart(self, message: str = "No data available") -> go.Figure:
"""Create an empty chart with a message."""
fig = go.Figure()
fig.add_annotation(
text=message,
xref="paper", yref="paper",
x=0.5, y=0.5,
xanchor='center', yanchor='middle',
showarrow=False,
font=dict(size=16, color="#7f8c8d")
)
fig.update_layout(
template=self.default_template,
height=self.default_height,
showlegend=False,
xaxis=dict(visible=False),
yaxis=dict(visible=False)
)
return fig
def _create_error_chart(self, error_message: str) -> go.Figure:
"""Create an error chart with error message."""
fig = go.Figure()
fig.add_annotation(
text=f"⚠️ {error_message}",
xref="paper", yref="paper",
x=0.5, y=0.5,
xanchor='center', yanchor='middle',
showarrow=False,
font=dict(size=16, color="#e74c3c")
)
fig.update_layout(
template=self.default_template,
height=self.default_height,
showlegend=False,
xaxis=dict(visible=False),
yaxis=dict(visible=False)
)
return fig
def create_strategy_chart(self, symbol: str, timeframe: str,
strategy_name: str, **kwargs) -> go.Figure:
"""
Create a strategy-specific chart (placeholder for future implementation).
Args:
symbol: Trading pair
timeframe: Timeframe
strategy_name: Name of the strategy configuration
**kwargs: Additional parameters
Returns:
Plotly Figure object
"""
# For now, return a basic candlestick chart
# This will be enhanced in later tasks with strategy configurations
self.logger.info(f"Creating strategy chart for {strategy_name} (basic implementation)")
return self.create_candlestick_chart(symbol, timeframe, **kwargs)

View File

@@ -0,0 +1,38 @@
"""
Chart Configuration Package
This package contains configuration management for the modular chart system,
including indicator definitions, strategy-specific configurations, and defaults.
"""
from .indicator_defs import (
INDICATOR_DEFINITIONS,
ChartIndicatorConfig,
calculate_indicators,
convert_database_candles_to_ohlcv,
get_indicator_display_config,
get_available_indicators,
get_overlay_indicators,
get_subplot_indicators,
get_default_indicator_params
)
# Package metadata
__version__ = "0.1.0"
__package_name__ = "config"
# Public exports
__all__ = [
"INDICATOR_DEFINITIONS",
"ChartIndicatorConfig",
"calculate_indicators",
"convert_database_candles_to_ohlcv",
"get_indicator_display_config",
"get_available_indicators",
"get_overlay_indicators",
"get_subplot_indicators",
"get_default_indicator_params"
]
# Legacy function names for backward compatibility
validate_indicator_config = get_default_indicator_params # Will be properly implemented in future tasks

View File

@@ -0,0 +1,266 @@
"""
Indicator Definitions and Configuration
This module defines indicator configurations and provides integration
with the existing data/common/indicators.py technical indicators module.
"""
from typing import Dict, List, Any, Optional, Union
from dataclasses import dataclass
from datetime import datetime, timezone
from decimal import Decimal
from data.common.indicators import TechnicalIndicators, IndicatorResult, create_default_indicators_config, validate_indicator_config
from data.common.data_types import OHLCVCandle
from utils.logger import get_logger
# Initialize logger
logger = get_logger("indicator_defs")
@dataclass
class ChartIndicatorConfig:
"""
Configuration for chart indicators with display properties.
Extends the base indicator config with chart-specific properties
like colors, line styles, and subplot placement.
"""
name: str
indicator_type: str
parameters: Dict[str, Any]
display_type: str # 'overlay', 'subplot'
color: str
line_style: str = 'solid' # 'solid', 'dash', 'dot'
line_width: int = 2
opacity: float = 1.0
visible: bool = True
subplot_height_ratio: float = 0.3 # For subplot indicators
def to_indicator_config(self) -> Dict[str, Any]:
"""Convert to format expected by TechnicalIndicators."""
config = {'type': self.indicator_type}
config.update(self.parameters)
return config
# Built-in indicator definitions with chart display properties
INDICATOR_DEFINITIONS = {
'sma_20': ChartIndicatorConfig(
name='SMA (20)',
indicator_type='sma',
parameters={'period': 20, 'price_column': 'close'},
display_type='overlay',
color='#007bff',
line_width=2
),
'sma_50': ChartIndicatorConfig(
name='SMA (50)',
indicator_type='sma',
parameters={'period': 50, 'price_column': 'close'},
display_type='overlay',
color='#28a745',
line_width=2
),
'ema_12': ChartIndicatorConfig(
name='EMA (12)',
indicator_type='ema',
parameters={'period': 12, 'price_column': 'close'},
display_type='overlay',
color='#ff6b35',
line_width=2
),
'ema_26': ChartIndicatorConfig(
name='EMA (26)',
indicator_type='ema',
parameters={'period': 26, 'price_column': 'close'},
display_type='overlay',
color='#dc3545',
line_width=2
),
'rsi_14': ChartIndicatorConfig(
name='RSI (14)',
indicator_type='rsi',
parameters={'period': 14, 'price_column': 'close'},
display_type='subplot',
color='#20c997',
line_width=2,
subplot_height_ratio=0.25
),
'macd_default': ChartIndicatorConfig(
name='MACD',
indicator_type='macd',
parameters={'fast_period': 12, 'slow_period': 26, 'signal_period': 9, 'price_column': 'close'},
display_type='subplot',
color='#fd7e14',
line_width=2,
subplot_height_ratio=0.3
),
'bollinger_bands': ChartIndicatorConfig(
name='Bollinger Bands',
indicator_type='bollinger_bands',
parameters={'period': 20, 'std_dev': 2.0, 'price_column': 'close'},
display_type='overlay',
color='#6f42c1',
line_width=1,
opacity=0.7
)
}
def convert_database_candles_to_ohlcv(candles: List[Dict[str, Any]]) -> List[OHLCVCandle]:
"""
Convert database candle dictionaries to OHLCVCandle objects.
Args:
candles: List of candle dictionaries from database operations
Returns:
List of OHLCVCandle objects for technical indicators
"""
ohlcv_candles = []
for candle in candles:
try:
# Handle timestamp conversion
timestamp = candle['timestamp']
if isinstance(timestamp, str):
timestamp = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
elif timestamp.tzinfo is None:
timestamp = timestamp.replace(tzinfo=timezone.utc)
# For database candles, start_time and end_time are the same
# as we store right-aligned timestamps
ohlcv_candle = OHLCVCandle(
symbol=candle['symbol'],
timeframe=candle['timeframe'],
start_time=timestamp,
end_time=timestamp,
open=Decimal(str(candle['open'])),
high=Decimal(str(candle['high'])),
low=Decimal(str(candle['low'])),
close=Decimal(str(candle['close'])),
volume=Decimal(str(candle.get('volume', 0))),
trade_count=candle.get('trades_count', 0),
exchange=candle.get('exchange', 'okx'),
is_complete=True
)
ohlcv_candles.append(ohlcv_candle)
except Exception as e:
logger.error(f"Error converting candle to OHLCV: {e}")
continue
logger.debug(f"Converted {len(ohlcv_candles)} database candles to OHLCV format")
return ohlcv_candles
def calculate_indicators(candles: List[Dict[str, Any]],
indicator_configs: List[str],
custom_configs: Optional[Dict[str, ChartIndicatorConfig]] = None) -> Dict[str, List[IndicatorResult]]:
"""
Calculate technical indicators for chart display.
Args:
candles: List of candle dictionaries from database
indicator_configs: List of indicator names to calculate
custom_configs: Optional custom indicator configurations
Returns:
Dictionary mapping indicator names to their calculation results
"""
if not candles:
logger.warning("No candles provided for indicator calculation")
return {}
# Convert to OHLCV format
ohlcv_candles = convert_database_candles_to_ohlcv(candles)
if not ohlcv_candles:
logger.error("Failed to convert candles to OHLCV format")
return {}
# Initialize technical indicators calculator
indicators_calc = TechnicalIndicators(logger)
# Prepare configurations
configs_to_calculate = {}
all_configs = {**INDICATOR_DEFINITIONS}
if custom_configs:
all_configs.update(custom_configs)
for indicator_name in indicator_configs:
if indicator_name in all_configs:
chart_config = all_configs[indicator_name]
configs_to_calculate[indicator_name] = chart_config.to_indicator_config()
else:
logger.warning(f"Unknown indicator configuration: {indicator_name}")
if not configs_to_calculate:
logger.warning("No valid indicator configurations found")
return {}
# Calculate indicators
try:
results = indicators_calc.calculate_multiple_indicators(ohlcv_candles, configs_to_calculate)
logger.debug(f"Calculated {len(results)} indicators successfully")
return results
except Exception as e:
logger.error(f"Error calculating indicators: {e}")
return {}
def get_indicator_display_config(indicator_name: str) -> Optional[ChartIndicatorConfig]:
"""
Get display configuration for an indicator.
Args:
indicator_name: Name of the indicator
Returns:
Chart indicator configuration or None if not found
"""
return INDICATOR_DEFINITIONS.get(indicator_name)
def get_available_indicators() -> Dict[str, str]:
"""
Get list of available indicators with descriptions.
Returns:
Dictionary mapping indicator names to descriptions
"""
return {name: config.name for name, config in INDICATOR_DEFINITIONS.items()}
def get_overlay_indicators() -> List[str]:
"""Get list of indicators that display as overlays on the price chart."""
return [name for name, config in INDICATOR_DEFINITIONS.items()
if config.display_type == 'overlay']
def get_subplot_indicators() -> List[str]:
"""Get list of indicators that display in separate subplots."""
return [name for name, config in INDICATOR_DEFINITIONS.items()
if config.display_type == 'subplot']
def get_default_indicator_params(indicator_type: str) -> Dict[str, Any]:
"""
Get default parameters for an indicator type.
Args:
indicator_type: Type of indicator ('sma', 'ema', 'rsi', etc.)
Returns:
Dictionary of default parameters
"""
defaults = {
'sma': {'period': 20, 'price_column': 'close'},
'ema': {'period': 20, 'price_column': 'close'},
'rsi': {'period': 14, 'price_column': 'close'},
'macd': {'fast_period': 12, 'slow_period': 26, 'signal_period': 9, 'price_column': 'close'},
'bollinger_bands': {'period': 20, 'std_dev': 2.0, 'price_column': 'close'}
}
return defaults.get(indicator_type, {})

View File

@@ -0,0 +1,24 @@
"""
Chart Layers Package
This package contains the modular chart layer system for rendering different
chart components including candlesticks, indicators, and signals.
"""
# Package metadata
__version__ = "0.1.0"
__package_name__ = "layers"
# Layers will be imported once they are created
# from .base import BaseCandlestickLayer
# from .indicators import IndicatorLayer
# from .subplots import SubplotManager
# from .signals import SignalLayer
# Public exports (will be populated as layers are implemented)
__all__ = [
# "BaseCandlestickLayer",
# "IndicatorLayer",
# "SubplotManager",
# "SignalLayer"
]

293
components/charts/utils.py Normal file
View File

@@ -0,0 +1,293 @@
"""
Chart Utilities and Helper Functions
This module provides utility functions for data processing, validation,
and chart styling used by the ChartBuilder and layer components.
"""
import pandas as pd
from datetime import datetime, timezone
from typing import List, Dict, Any, Optional, Union
from decimal import Decimal
from utils.logger import get_logger
# Initialize logger
logger = get_logger("chart_utils")
# Default color scheme for charts
DEFAULT_CHART_COLORS = {
'bullish': '#00C851', # Green for bullish candles
'bearish': '#FF4444', # Red for bearish candles
'sma': '#007bff', # Blue for SMA
'ema': '#ff6b35', # Orange for EMA
'bb_upper': '#6f42c1', # Purple for Bollinger upper
'bb_lower': '#6f42c1', # Purple for Bollinger lower
'bb_middle': '#6c757d', # Gray for Bollinger middle
'rsi': '#20c997', # Teal for RSI
'macd': '#fd7e14', # Orange for MACD
'macd_signal': '#e83e8c', # Pink for MACD signal
'volume': '#6c757d', # Gray for volume
'support': '#17a2b8', # Light blue for support
'resistance': '#dc3545' # Red for resistance
}
def validate_market_data(candles: List[Dict[str, Any]]) -> bool:
"""
Validate market data structure and content.
Args:
candles: List of candle dictionaries from database
Returns:
True if data is valid, False otherwise
"""
if not candles:
logger.warning("Empty candles data")
return False
# Check required fields in first candle
required_fields = ['timestamp', 'open', 'high', 'low', 'close']
first_candle = candles[0]
for field in required_fields:
if field not in first_candle:
logger.error(f"Missing required field: {field}")
return False
# Validate data types and values
for i, candle in enumerate(candles[:5]): # Check first 5 candles
try:
# Validate timestamp
if not isinstance(candle['timestamp'], (datetime, str)):
logger.error(f"Invalid timestamp type at index {i}")
return False
# Validate OHLC values
for field in ['open', 'high', 'low', 'close']:
value = candle[field]
if value is None:
logger.error(f"Null value for {field} at index {i}")
return False
# Convert to float for validation
try:
float_val = float(value)
if float_val <= 0:
logger.error(f"Non-positive value for {field} at index {i}: {float_val}")
return False
except (ValueError, TypeError):
logger.error(f"Invalid numeric value for {field} at index {i}: {value}")
return False
# Validate OHLC relationships (high >= low, etc.)
try:
o, h, l, c = float(candle['open']), float(candle['high']), float(candle['low']), float(candle['close'])
if not (h >= max(o, c) and l <= min(o, c)):
logger.warning(f"Invalid OHLC relationship at index {i}: O={o}, H={h}, L={l}, C={c}")
# Don't fail validation for this, just warn
except (ValueError, TypeError):
logger.error(f"Error validating OHLC relationships at index {i}")
return False
except Exception as e:
logger.error(f"Error validating candle at index {i}: {e}")
return False
logger.debug(f"Market data validation passed for {len(candles)} candles")
return True
def prepare_chart_data(candles: List[Dict[str, Any]]) -> pd.DataFrame:
"""
Convert candle data to pandas DataFrame suitable for charting.
Args:
candles: List of candle dictionaries from database
Returns:
Prepared pandas DataFrame
"""
try:
# Convert to DataFrame
df = pd.DataFrame(candles)
# Ensure timestamp is datetime
if 'timestamp' in df.columns:
df['timestamp'] = pd.to_datetime(df['timestamp'])
# Convert OHLCV columns to numeric
numeric_columns = ['open', 'high', 'low', 'close']
if 'volume' in df.columns:
numeric_columns.append('volume')
for col in numeric_columns:
if col in df.columns:
df[col] = pd.to_numeric(df[col], errors='coerce')
# Sort by timestamp
df = df.sort_values('timestamp').reset_index(drop=True)
# Handle missing volume data
if 'volume' not in df.columns:
df['volume'] = 0
# Fill any NaN values with forward fill, then backward fill
df = df.ffill().bfill()
logger.debug(f"Prepared chart data: {len(df)} rows, columns: {list(df.columns)}")
return df
except Exception as e:
logger.error(f"Error preparing chart data: {e}")
# Return empty DataFrame with expected structure
return pd.DataFrame(columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
def get_indicator_colors() -> Dict[str, str]:
"""
Get the default color scheme for chart indicators.
Returns:
Dictionary of color mappings
"""
return DEFAULT_CHART_COLORS.copy()
def format_price(price: Union[float, Decimal, str], decimals: int = 4) -> str:
"""
Format price value for display.
Args:
price: Price value to format
decimals: Number of decimal places
Returns:
Formatted price string
"""
try:
return f"{float(price):.{decimals}f}"
except (ValueError, TypeError):
return "N/A"
def format_volume(volume: Union[float, int, str]) -> str:
"""
Format volume value for display with K/M/B suffixes.
Args:
volume: Volume value to format
Returns:
Formatted volume string
"""
try:
vol = float(volume)
if vol >= 1e9:
return f"{vol/1e9:.2f}B"
elif vol >= 1e6:
return f"{vol/1e6:.2f}M"
elif vol >= 1e3:
return f"{vol/1e3:.2f}K"
else:
return f"{vol:.0f}"
except (ValueError, TypeError):
return "N/A"
def calculate_price_change(current: Union[float, Decimal], previous: Union[float, Decimal]) -> Dict[str, Any]:
"""
Calculate price change and percentage change.
Args:
current: Current price
previous: Previous price
Returns:
Dictionary with change, change_percent, and direction
"""
try:
curr = float(current)
prev = float(previous)
if prev == 0:
return {'change': 0, 'change_percent': 0, 'direction': 'neutral'}
change = curr - prev
change_percent = (change / prev) * 100
direction = 'up' if change > 0 else 'down' if change < 0 else 'neutral'
return {
'change': change,
'change_percent': change_percent,
'direction': direction
}
except (ValueError, TypeError):
return {'change': 0, 'change_percent': 0, 'direction': 'neutral'}
def get_chart_height(include_volume: bool = False, num_subplots: int = 0) -> int:
"""
Calculate appropriate chart height based on components.
Args:
include_volume: Whether volume subplot is included
num_subplots: Number of additional subplots (for indicators)
Returns:
Recommended chart height in pixels
"""
base_height = 500
volume_height = 150 if include_volume else 0
subplot_height = num_subplots * 120
return base_height + volume_height + subplot_height
def validate_timeframe(timeframe: str) -> bool:
"""
Validate if timeframe string is supported.
Args:
timeframe: Timeframe string (e.g., '1m', '5m', '1h', '1d')
Returns:
True if valid, False otherwise
"""
valid_timeframes = [
'1s', '5s', '15s', '30s', # Seconds
'1m', '5m', '15m', '30m', # Minutes
'1h', '2h', '4h', '6h', '12h', # Hours
'1d', '3d', '1w', '1M' # Days, weeks, months
]
return timeframe in valid_timeframes
def validate_symbol(symbol: str) -> bool:
"""
Validate trading symbol format.
Args:
symbol: Trading symbol (e.g., 'BTC-USDT')
Returns:
True if valid format, False otherwise
"""
if not symbol or not isinstance(symbol, str):
return False
# Basic validation: should contain a dash and have reasonable length
parts = symbol.split('-')
if len(parts) != 2:
return False
base, quote = parts
if len(base) < 2 or len(quote) < 3 or len(base) > 10 or len(quote) > 10:
return False
return True