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