diff --git a/app.py b/app.py
index aa3229b..6c889d9 100644
--- a/app.py
+++ b/app.py
@@ -11,6 +11,14 @@ from pathlib import Path
project_root = Path(__file__).parent
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
from dash import dcc, html, Input, Output, callback
import plotly.graph_objects as go
diff --git a/components/charts.py b/components/charts.py
index 10cbfe0..4449e8f 100644
--- a/components/charts.py
+++ b/components/charts.py
@@ -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'{icon} {status_text}
{status["message"]}'
-
-
-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']
\ No newline at end of file
+ return f'{icon} {status_text}
{status["message"]}'
\ No newline at end of file
diff --git a/components/charts/__init__.py b/components/charts/__init__.py
new file mode 100644
index 0000000..a890339
--- /dev/null
+++ b/components/charts/__init__.py
@@ -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'{icon} {status_text}
{status["message"]}'
+
+def create_error_chart(error_message: str):
+ """Create an error chart with error message."""
+ builder = ChartBuilder()
+ return builder._create_error_chart(error_message)
\ No newline at end of file
diff --git a/components/charts/builder.py b/components/charts/builder.py
new file mode 100644
index 0000000..5854b91
--- /dev/null
+++ b/components/charts/builder.py
@@ -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)
\ No newline at end of file
diff --git a/components/charts/config/__init__.py b/components/charts/config/__init__.py
new file mode 100644
index 0000000..4eff156
--- /dev/null
+++ b/components/charts/config/__init__.py
@@ -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
\ No newline at end of file
diff --git a/components/charts/config/indicator_defs.py b/components/charts/config/indicator_defs.py
new file mode 100644
index 0000000..bdfb134
--- /dev/null
+++ b/components/charts/config/indicator_defs.py
@@ -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, {})
\ No newline at end of file
diff --git a/components/charts/layers/__init__.py b/components/charts/layers/__init__.py
new file mode 100644
index 0000000..7209ebd
--- /dev/null
+++ b/components/charts/layers/__init__.py
@@ -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"
+]
\ No newline at end of file
diff --git a/components/charts/utils.py b/components/charts/utils.py
new file mode 100644
index 0000000..64414c2
--- /dev/null
+++ b/components/charts/utils.py
@@ -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
\ No newline at end of file
diff --git a/database/connection.py b/database/connection.py
index 79beb68..e88f248 100644
--- a/database/connection.py
+++ b/database/connection.py
@@ -82,7 +82,7 @@ class DatabaseConfig:
'options': f'-c statement_timeout={self.statement_timeout}',
'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
}
diff --git a/main.py b/main.py
index e49d050..932e405 100644
--- a/main.py
+++ b/main.py
@@ -4,6 +4,7 @@ Main entry point for the Crypto Trading Bot Dashboard.
"""
import sys
+import logging
from pathlib import Path
# Add project root to path
@@ -16,6 +17,13 @@ def main():
print("🚀 Crypto Trading Bot Dashboard")
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:
from config.settings import app, dashboard
print(f"Environment: {app.environment}")
diff --git a/tasks/3.4. Chart layers.md b/tasks/3.4. Chart layers.md
new file mode 100644
index 0000000..e9e62d5
--- /dev/null
+++ b/tasks/3.4. Chart layers.md
@@ -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
+
diff --git a/tests/test_chart_builder.py b/tests/test_chart_builder.py
new file mode 100644
index 0000000..68653df
--- /dev/null
+++ b/tests/test_chart_builder.py
@@ -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'])
\ No newline at end of file