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

8
app.py
View File

@ -11,6 +11,14 @@ from pathlib import Path
project_root = Path(__file__).parent project_root = Path(__file__).parent
sys.path.insert(0, str(project_root)) sys.path.insert(0, str(project_root))
# Suppress SQLAlchemy logging to reduce verbosity
import logging
logging.getLogger('sqlalchemy').setLevel(logging.WARNING)
logging.getLogger('sqlalchemy.engine').setLevel(logging.WARNING)
logging.getLogger('sqlalchemy.pool').setLevel(logging.WARNING)
logging.getLogger('sqlalchemy.dialects').setLevel(logging.WARNING)
logging.getLogger('sqlalchemy.orm').setLevel(logging.WARNING)
import dash import dash
from dash import dcc, html, Input, Output, callback from dash import dcc, html, Input, Output, callback
import plotly.graph_objects as go import plotly.graph_objects as go

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, This module redirects to the new modular chart system in components/charts/.
including candlestick charts, technical indicators, and real-time updates. For new development, use the ChartBuilder class directly from components.charts.
""" """
import plotly.graph_objects as go # Import and re-export the new modular chart system for simple migration
import plotly.express as px from .charts import (
from plotly.subplots import make_subplots ChartBuilder,
import pandas as pd create_candlestick_chart,
from datetime import datetime, timedelta, timezone create_strategy_chart,
from typing import List, Dict, Any, Optional validate_market_data,
from decimal import Decimal prepare_chart_data,
get_indicator_colors
)
from database.operations import get_database_operations, DatabaseOperationError from .charts.config import (
from utils.logger import get_logger get_available_indicators,
calculate_indicators,
get_overlay_indicators,
get_subplot_indicators,
get_indicator_display_config
)
# Initialize logger # Convenience functions for common operations
logger = get_logger("charts_component") 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")
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
Returns:
List of candle data dictionaries
"""
try: try:
db = get_database_operations(logger) 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
# Calculate time range return ['BTC-USDT', 'ETH-USDT'] # Fallback
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, def get_supported_timeframes():
candles: Optional[List[Dict[str, Any]]] = None) -> go.Figure: """Get list of timeframes that have data in the database."""
""" builder = ChartBuilder()
Create a candlestick chart with real market data. 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")
Args:
symbol: Trading pair
timeframe: Timeframe
candles: Optional pre-fetched candle data
Returns:
Plotly Figure object
"""
try: try:
# Fetch data if not provided db = get_database_operations(logger)
if candles is None: with db.market_data.get_session() as session:
candles = fetch_market_data(symbol, timeframe) 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
# Handle empty data return ['5s', '1m', '15m', '1h'] # Fallback
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: # Legacy function names for compatibility during transition
""" get_available_technical_indicators = get_available_indicators
Create a candlestick chart with volume subplot. 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)
Args: def get_market_statistics(symbol: str, timeframe: str = "1h"):
df: DataFrame with OHLCV data """Calculate market statistics from recent data."""
symbol: Trading pair builder = ChartBuilder()
timeframe: Timeframe candles = builder.fetch_market_data(symbol, timeframe, days_back=1)
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: if not candles:
return { return {'Price': 'N/A', '24h Change': 'N/A', '24h Volume': 'N/A', 'High 24h': 'N/A', 'Low 24h': 'N/A'}
'Price': 'N/A',
'24h Change': 'N/A',
'24h Volume': 'N/A',
'High 24h': 'N/A',
'Low 24h': 'N/A'
}
# Convert to DataFrame import pandas as pd
df = pd.DataFrame(candles) df = pd.DataFrame(candles)
latest = df.iloc[-1]
# Get latest and 24h ago prices current_price = float(latest['close'])
latest_candle = df.iloc[-1]
current_price = float(latest_candle['close'])
# Calculate 24h change # Calculate 24h change
if len(df) > 1: if len(df) > 1:
price_24h_ago = float(df.iloc[0]['open']) price_24h_ago = float(df.iloc[0]['open'])
change_24h = current_price - price_24h_ago change_percent = ((current_price - price_24h_ago) / price_24h_ago) * 100
change_percent = (change_24h / price_24h_ago) * 100
else: else:
change_24h = 0
change_percent = 0 change_percent = 0
# Calculate volume and high/low from .charts.utils import format_price, format_volume
total_volume = df['volume'].sum()
high_24h = df['high'].max()
low_24h = df['low'].min()
# Format statistics
return { return {
'Price': f"${current_price:,.2f}", 'Price': format_price(current_price, decimals=2),
'24h Change': f"{'+' if change_24h >= 0 else ''}{change_percent:.2f}%", '24h Change': f"{'+' if change_percent >= 0 else ''}{change_percent:.2f}%",
'24h Volume': f"{total_volume:,.2f}", '24h Volume': format_volume(df['volume'].sum()),
'High 24h': f"${float(high_24h):,.2f}", 'High 24h': format_price(df['high'].max(), decimals=2),
'Low 24h': f"${float(low_24h):,.2f}" 'Low 24h': format_price(df['low'].min(), decimals=2)
} }
except Exception as e: def check_data_availability(symbol: str, timeframe: str):
logger.error(f"Error calculating market statistics for {symbol}: {e}") """Check data availability for a symbol and timeframe."""
return { from datetime import datetime, timezone, timedelta
'Price': 'Error', from database.operations import get_database_operations
'24h Change': 'Error', from utils.logger import get_logger
'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: try:
logger = get_logger("charts_data_check")
db = get_database_operations(logger) db = get_database_operations(logger)
# Get latest candle using the proper API
latest_candle = db.market_data.get_latest_candle(symbol, timeframe) latest_candle = db.market_data.get_latest_candle(symbol, timeframe)
if latest_candle: if latest_candle:
@ -363,9 +132,7 @@ def check_data_availability(symbol: str, timeframe: str) -> Dict[str, Any]:
'is_recent': False, 'is_recent': False,
'message': f"No data available for {symbol} {timeframe}" 'message': f"No data available for {symbol} {timeframe}"
} }
except Exception as e: except Exception as e:
logger.error(f"Error checking data availability for {symbol} {timeframe}: {e}")
return { return {
'has_data': False, 'has_data': False,
'latest_timestamp': None, 'latest_timestamp': None,
@ -374,82 +141,16 @@ def check_data_availability(symbol: str, timeframe: str) -> Dict[str, Any]:
'message': f"Error checking data: {str(e)}" 'message': f"Error checking data: {str(e)}"
} }
def create_data_status_indicator(symbol: str, timeframe: str):
def create_data_status_indicator(symbol: str, timeframe: str) -> str: """Create a data status indicator for the dashboard."""
"""
Create a data status indicator for the dashboard.
Args:
symbol: Trading pair
timeframe: Timeframe
Returns:
HTML string for status indicator
"""
status = check_data_availability(symbol, timeframe) status = check_data_availability(symbol, timeframe)
if status['has_data']: if status['has_data']:
if status['is_recent']: if status['is_recent']:
icon = "🟢" icon, color, status_text = "🟢", "#27ae60", "Real-time Data"
color = "#27ae60"
status_text = "Real-time Data"
else: else:
icon = "🟡" icon, color, status_text = "🟡", "#f39c12", "Delayed Data"
color = "#f39c12"
status_text = "Delayed Data"
else: else:
icon = "🔴" icon, color, status_text = "🔴", "#e74c3c", "No Data"
color = "#e74c3c"
status_text = "No Data"
return f'<span style="color: {color}; font-weight: bold;">{icon} {status_text}</span><br><small>{status["message"]}</small>' 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']

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

View File

@ -82,7 +82,7 @@ class DatabaseConfig:
'options': f'-c statement_timeout={self.statement_timeout}', 'options': f'-c statement_timeout={self.statement_timeout}',
'sslmode': self.ssl_mode, 'sslmode': self.ssl_mode,
}, },
'echo': os.getenv('DEBUG', 'false').lower() == 'true', 'echo': False, # Disable SQL logging to reduce verbosity
'future': True, # Use SQLAlchemy 2.0 style 'future': True, # Use SQLAlchemy 2.0 style
} }

View File

@ -4,6 +4,7 @@ Main entry point for the Crypto Trading Bot Dashboard.
""" """
import sys import sys
import logging
from pathlib import Path from pathlib import Path
# Add project root to path # Add project root to path
@ -16,6 +17,13 @@ def main():
print("🚀 Crypto Trading Bot Dashboard") print("🚀 Crypto Trading Bot Dashboard")
print("=" * 40) print("=" * 40)
# Suppress SQLAlchemy database logging for cleaner console output
logging.getLogger('sqlalchemy').setLevel(logging.WARNING)
logging.getLogger('sqlalchemy.engine').setLevel(logging.WARNING)
logging.getLogger('sqlalchemy.pool').setLevel(logging.WARNING)
logging.getLogger('sqlalchemy.dialects').setLevel(logging.WARNING)
logging.getLogger('sqlalchemy.orm').setLevel(logging.WARNING)
try: try:
from config.settings import app, dashboard from config.settings import app, dashboard
print(f"Environment: {app.environment}") print(f"Environment: {app.environment}")

View File

@ -0,0 +1,91 @@
# Task 3.4: Modular Chart Layers System
## Overview
Implementation of a flexible, strategy-driven chart system that supports technical indicator overlays, subplot management, and future bot signal integration. This system will replace the basic chart functionality with a modular architecture that can adapt to different trading strategies and their specific indicator requirements.
## Relevant Files
- `components/charts/__init__.py` - Public API exports for the new modular chart system
- `components/charts/builder.py` - Main ChartBuilder class orchestrating chart creation and layer management
- `components/charts/utils.py` - Chart utilities and helper functions for data processing and validation
- `components/charts/config/__init__.py` - Configuration package initialization
- `components/charts/config/indicator_defs.py` - Base indicator definitions, schemas, and default parameters
- `components/charts/config/strategy_charts.py` - Strategy-specific chart configurations and presets
- `components/charts/config/defaults.py` - Default chart configurations and fallback settings
- `components/charts/layers/__init__.py` - Chart layers package initialization
- `components/charts/layers/base.py` - Base candlestick chart layer implementation
- `components/charts/layers/indicators.py` - Indicator overlay rendering (SMA, EMA, Bollinger Bands)
- `components/charts/layers/subplots.py` - Subplot management for indicators like RSI and MACD
- `components/charts/layers/signals.py` - Strategy signal overlays and trade markers (future bot integration)
- `app.py` - Updated dashboard integration with indicator selection controls
- `components/dashboard.py` - Enhanced dashboard layout with chart configuration UI
- `tests/test_chart_builder.py` - Unit tests for ChartBuilder class functionality
- `tests/test_chart_layers.py` - Unit tests for individual chart layer components
- `tests/test_chart_integration.py` - Integration tests for full chart creation workflow
### Notes
- The modular design allows each chart layer to be tested independently
- Strategy configurations are JSON-based for easy modification without code changes
- Integration with existing `data/common/indicators.py` for technical indicator calculations
- Backward compatibility maintained with existing `components/charts.py` API
- Use `uv run pytest tests/test_chart_*.py` to run chart-specific tests
- create documentation with importand components in ./docs/components/charts/ folder without redundancy
## Tasks
- [x] 1.0 Foundation Infrastructure Setup
- [x] 1.1 Create components/charts directory structure and package files
- [x] 1.2 Implement ChartBuilder class with basic candlestick chart creation
- [x] 1.3 Create chart utilities for data processing and validation
- [x] 1.4 Integrate with existing data/common/indicators.py module
- [x] 1.5 Setup backward compatibility with existing components/charts.py API
- [x] 1.6 Create basic unit tests for ChartBuilder class
- [ ] 2.0 Indicator Layer System Implementation
- [ ] 2.1 Create base candlestick chart layer with volume subplot
- [ ] 2.2 Implement overlay indicator rendering (SMA, EMA)
- [ ] 2.3 Add Bollinger Bands overlay functionality
- [ ] 2.4 Create subplot management system for secondary indicators
- [ ] 2.5 Implement RSI subplot with proper scaling and styling
- [ ] 2.6 Add MACD subplot with signal line and histogram
- [ ] 2.7 Create indicator calculation integration with market data
- [ ] 2.8 Add error handling for insufficient data scenarios
- [ ] 2.9 Unit test all indicator layer components
- [ ] 3.0 Strategy Configuration System
- [ ] 3.1 Design indicator definition schema and validation
- [ ] 3.2 Create default indicator configurations and parameters
- [ ] 3.3 Implement strategy-specific chart configuration system
- [ ] 3.4 Add configuration validation and error handling
- [ ] 3.5 Create example strategy configurations (EMA crossover, momentum)
- [ ] 3.6 Add configuration fallback mechanisms for missing strategies
- [ ] 3.7 Unit test configuration system and validation
- [ ] 4.0 Dashboard Integration and UI Controls
- [ ] 4.1 Add indicator selection checkboxes to dashboard layout
- [ ] 4.2 Create real-time chart updates with indicator toggling
- [ ] 4.3 Implement parameter adjustment controls for indicators
- [ ] 4.4 Add strategy selection dropdown for predefined configurations
- [ ] 4.5 Update chart callback functions to handle new layer system
- [ ] 4.6 Ensure backward compatibility with existing dashboard features
- [ ] 4.7 Test dashboard integration with real market data
- [ ] 5.0 Signal Layer Foundation for Future Bot Integration
- [ ] 5.1 Create signal layer architecture for buy/sell markers
- [ ] 5.2 Implement trade entry/exit point visualization
- [ ] 5.3 Add support/resistance line drawing capabilities
- [ ] 5.4 Create extensible interface for custom strategy signals
- [ ] 5.5 Add signal color and style customization options
- [ ] 5.6 Prepare integration points for bot management system
- [ ] 5.7 Create foundation tests for signal layer functionality
- [ ] 6.0 Documentation
- [ ] 6.1 Create documentation for the chart layers system
- [ ] 6.2 Add documentation to the README
- [ ] 6.3 Create documentation for the ChartBuilder class
- [ ] 6.4 Create documentation for the ChartUtils class
- [ ] 6.5 Create documentation for the ChartConfig package
- [ ] 6.6 Create documentation how to add new indicators
- [ ] 6.7 Create documentation how to add new strategies

306
tests/test_chart_builder.py Normal file
View File

@ -0,0 +1,306 @@
#!/usr/bin/env python3
"""
Unit Tests for ChartBuilder Class
Tests for the core ChartBuilder functionality including:
- Chart creation
- Data fetching
- Error handling
- Market data integration
"""
import pytest
import pandas as pd
from datetime import datetime, timezone, timedelta
from unittest.mock import Mock, patch, MagicMock
from typing import List, Dict, Any
import sys
from pathlib import Path
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
from components.charts.builder import ChartBuilder
from components.charts.utils import validate_market_data, prepare_chart_data
class TestChartBuilder:
"""Test suite for ChartBuilder class"""
@pytest.fixture
def mock_logger(self):
"""Mock logger for testing"""
return Mock()
@pytest.fixture
def chart_builder(self, mock_logger):
"""Create ChartBuilder instance for testing"""
return ChartBuilder(mock_logger)
@pytest.fixture
def sample_candles(self):
"""Sample candle data for testing"""
base_time = datetime.now(timezone.utc) - timedelta(hours=24)
return [
{
'timestamp': base_time + timedelta(minutes=i),
'open': 50000 + i * 10,
'high': 50100 + i * 10,
'low': 49900 + i * 10,
'close': 50050 + i * 10,
'volume': 1000 + i * 5,
'exchange': 'okx',
'symbol': 'BTC-USDT',
'timeframe': '1m'
}
for i in range(100)
]
def test_chart_builder_initialization(self, mock_logger):
"""Test ChartBuilder initialization"""
builder = ChartBuilder(mock_logger)
assert builder.logger == mock_logger
assert builder.db_ops is not None
assert builder.default_colors is not None
assert builder.default_height == 600
assert builder.default_template == "plotly_white"
def test_chart_builder_default_logger(self):
"""Test ChartBuilder initialization with default logger"""
builder = ChartBuilder()
assert builder.logger is not None
@patch('components.charts.builder.get_database_operations')
def test_fetch_market_data_success(self, mock_db_ops, chart_builder, sample_candles):
"""Test successful market data fetching"""
# Mock database operations
mock_db = Mock()
mock_db.market_data.get_candles.return_value = sample_candles
mock_db_ops.return_value = mock_db
# Replace the db_ops attribute with our mock
chart_builder.db_ops = mock_db
# Test fetch
result = chart_builder.fetch_market_data('BTC-USDT', '1m', days_back=1)
assert result == sample_candles
mock_db.market_data.get_candles.assert_called_once()
@patch('components.charts.builder.get_database_operations')
def test_fetch_market_data_empty(self, mock_db_ops, chart_builder):
"""Test market data fetching with empty result"""
# Mock empty database result
mock_db = Mock()
mock_db.market_data.get_candles.return_value = []
mock_db_ops.return_value = mock_db
# Replace the db_ops attribute with our mock
chart_builder.db_ops = mock_db
result = chart_builder.fetch_market_data('BTC-USDT', '1m')
assert result == []
@patch('components.charts.builder.get_database_operations')
def test_fetch_market_data_exception(self, mock_db_ops, chart_builder):
"""Test market data fetching with database exception"""
# Mock database exception
mock_db = Mock()
mock_db.market_data.get_candles.side_effect = Exception("Database error")
mock_db_ops.return_value = mock_db
# Replace the db_ops attribute with our mock
chart_builder.db_ops = mock_db
result = chart_builder.fetch_market_data('BTC-USDT', '1m')
assert result == []
chart_builder.logger.error.assert_called()
def test_create_candlestick_chart_with_data(self, chart_builder, sample_candles):
"""Test candlestick chart creation with valid data"""
# Mock fetch_market_data to return sample data
chart_builder.fetch_market_data = Mock(return_value=sample_candles)
fig = chart_builder.create_candlestick_chart('BTC-USDT', '1m')
assert fig is not None
assert len(fig.data) >= 1 # Should have at least candlestick trace
assert 'BTC-USDT' in fig.layout.title.text
def test_create_candlestick_chart_with_volume(self, chart_builder, sample_candles):
"""Test candlestick chart creation with volume subplot"""
chart_builder.fetch_market_data = Mock(return_value=sample_candles)
fig = chart_builder.create_candlestick_chart('BTC-USDT', '1m', include_volume=True)
assert fig is not None
assert len(fig.data) >= 2 # Should have candlestick + volume traces
def test_create_candlestick_chart_no_data(self, chart_builder):
"""Test candlestick chart creation with no data"""
chart_builder.fetch_market_data = Mock(return_value=[])
fig = chart_builder.create_candlestick_chart('BTC-USDT', '1m')
assert fig is not None
# Check for annotation with message instead of title
assert len(fig.layout.annotations) > 0
assert "No data available" in fig.layout.annotations[0].text
def test_create_candlestick_chart_invalid_data(self, chart_builder):
"""Test candlestick chart creation with invalid data"""
invalid_data = [{'invalid': 'data'}]
chart_builder.fetch_market_data = Mock(return_value=invalid_data)
fig = chart_builder.create_candlestick_chart('BTC-USDT', '1m')
assert fig is not None
# Should show error chart
assert len(fig.layout.annotations) > 0
assert "Invalid market data" in fig.layout.annotations[0].text
def test_create_strategy_chart_basic_implementation(self, chart_builder, sample_candles):
"""Test strategy chart creation (currently returns basic chart)"""
chart_builder.fetch_market_data = Mock(return_value=sample_candles)
result = chart_builder.create_strategy_chart('BTC-USDT', '1m', 'test_strategy')
assert result is not None
# Should currently return a basic candlestick chart
assert 'BTC-USDT' in result.layout.title.text
def test_create_empty_chart(self, chart_builder):
"""Test empty chart creation"""
fig = chart_builder._create_empty_chart("Test message")
assert fig is not None
assert len(fig.layout.annotations) > 0
assert "Test message" in fig.layout.annotations[0].text
assert len(fig.data) == 0
def test_create_error_chart(self, chart_builder):
"""Test error chart creation"""
fig = chart_builder._create_error_chart("Test error")
assert fig is not None
assert len(fig.layout.annotations) > 0
assert "Test error" in fig.layout.annotations[0].text
class TestChartBuilderIntegration:
"""Integration tests for ChartBuilder with real components"""
@pytest.fixture
def chart_builder(self):
"""Create ChartBuilder for integration testing"""
return ChartBuilder()
def test_market_data_validation_integration(self, chart_builder):
"""Test integration with market data validation"""
# Test with valid data structure
valid_data = [
{
'timestamp': datetime.now(timezone.utc),
'open': 50000,
'high': 50100,
'low': 49900,
'close': 50050,
'volume': 1000
}
]
assert validate_market_data(valid_data) is True
def test_chart_data_preparation_integration(self, chart_builder):
"""Test integration with chart data preparation"""
raw_data = [
{
'timestamp': datetime.now(timezone.utc) - timedelta(hours=1),
'open': '50000', # String values to test conversion
'high': '50100',
'low': '49900',
'close': '50050',
'volume': '1000'
},
{
'timestamp': datetime.now(timezone.utc),
'open': '50050',
'high': '50150',
'low': '49950',
'close': '50100',
'volume': '1200'
}
]
df = prepare_chart_data(raw_data)
assert isinstance(df, pd.DataFrame)
assert len(df) == 2
assert all(col in df.columns for col in ['timestamp', 'open', 'high', 'low', 'close', 'volume'])
assert df['open'].dtype.kind in 'fi' # Float or integer
class TestChartBuilderEdgeCases:
"""Test edge cases and error conditions"""
@pytest.fixture
def chart_builder(self):
return ChartBuilder()
def test_chart_creation_with_single_candle(self, chart_builder):
"""Test chart creation with only one candle"""
single_candle = [{
'timestamp': datetime.now(timezone.utc),
'open': 50000,
'high': 50100,
'low': 49900,
'close': 50050,
'volume': 1000
}]
chart_builder.fetch_market_data = Mock(return_value=single_candle)
fig = chart_builder.create_candlestick_chart('BTC-USDT', '1m')
assert fig is not None
assert len(fig.data) >= 1
def test_chart_creation_with_missing_volume(self, chart_builder):
"""Test chart creation with missing volume data"""
no_volume_data = [{
'timestamp': datetime.now(timezone.utc),
'open': 50000,
'high': 50100,
'low': 49900,
'close': 50050
# No volume field
}]
chart_builder.fetch_market_data = Mock(return_value=no_volume_data)
fig = chart_builder.create_candlestick_chart('BTC-USDT', '1m', include_volume=True)
assert fig is not None
# Should handle missing volume gracefully
def test_chart_creation_with_none_values(self, chart_builder):
"""Test chart creation with None values in data"""
data_with_nulls = [{
'timestamp': datetime.now(timezone.utc),
'open': 50000,
'high': None, # Null value
'low': 49900,
'close': 50050,
'volume': 1000
}]
chart_builder.fetch_market_data = Mock(return_value=data_with_nulls)
fig = chart_builder.create_candlestick_chart('BTC-USDT', '1m')
assert fig is not None
# Should handle null values gracefully
if __name__ == '__main__':
# Run tests if executed directly
pytest.main([__file__, '-v'])