3.4 Enhance logging and modular chart system for Crypto Trading Bot Dashboard
- Suppressed SQLAlchemy logging in `app.py` and `main.py` to reduce console verbosity. - Introduced a new modular chart system in `components/charts/` with a `ChartBuilder` class for flexible chart creation. - Added utility functions for data processing and validation in `components/charts/utils.py`. - Implemented indicator definitions and configurations in `components/charts/config/indicator_defs.py`. - Created a comprehensive documentation structure for the new chart system, ensuring clarity and maintainability. - Added unit tests for the `ChartBuilder` class to verify functionality and robustness. - Updated existing components to integrate with the new chart system, enhancing overall architecture and user experience.
This commit is contained in:
200
components/charts/__init__.py
Normal file
200
components/charts/__init__.py
Normal file
@@ -0,0 +1,200 @@
|
||||
"""
|
||||
Modular Chart System for Crypto Trading Bot Dashboard
|
||||
|
||||
This package provides a flexible, strategy-driven chart system that supports:
|
||||
- Technical indicator overlays (SMA, EMA, Bollinger Bands)
|
||||
- Subplot management (RSI, MACD)
|
||||
- Strategy-specific configurations
|
||||
- Future bot signal integration
|
||||
|
||||
Main Components:
|
||||
- ChartBuilder: Main orchestrator for chart creation
|
||||
- Layer System: Modular rendering components
|
||||
- Configuration System: Strategy-driven chart configs
|
||||
"""
|
||||
|
||||
from .builder import ChartBuilder
|
||||
from .utils import (
|
||||
validate_market_data,
|
||||
prepare_chart_data,
|
||||
get_indicator_colors
|
||||
)
|
||||
|
||||
# Version information
|
||||
__version__ = "0.1.0"
|
||||
__package_name__ = "charts"
|
||||
|
||||
# Public API exports
|
||||
__all__ = [
|
||||
"ChartBuilder",
|
||||
"validate_market_data",
|
||||
"prepare_chart_data",
|
||||
"get_indicator_colors",
|
||||
"create_candlestick_chart",
|
||||
"create_strategy_chart",
|
||||
"get_supported_symbols",
|
||||
"get_supported_timeframes",
|
||||
"get_market_statistics",
|
||||
"check_data_availability",
|
||||
"create_data_status_indicator",
|
||||
"create_error_chart"
|
||||
]
|
||||
|
||||
def create_candlestick_chart(symbol: str, timeframe: str, days_back: int = 7, **kwargs):
|
||||
"""
|
||||
Convenience function to create a basic candlestick chart.
|
||||
|
||||
Args:
|
||||
symbol: Trading pair (e.g., 'BTC-USDT')
|
||||
timeframe: Timeframe (e.g., '1h', '1d')
|
||||
days_back: Number of days to look back
|
||||
**kwargs: Additional parameters for chart customization
|
||||
|
||||
Returns:
|
||||
Plotly Figure object
|
||||
"""
|
||||
builder = ChartBuilder()
|
||||
return builder.create_candlestick_chart(symbol, timeframe, days_back, **kwargs)
|
||||
|
||||
def create_strategy_chart(symbol: str, timeframe: str, strategy_name: str, **kwargs):
|
||||
"""
|
||||
Convenience function to create a strategy-specific chart.
|
||||
|
||||
Args:
|
||||
symbol: Trading pair
|
||||
timeframe: Timeframe
|
||||
strategy_name: Name of the strategy configuration
|
||||
**kwargs: Additional parameters
|
||||
|
||||
Returns:
|
||||
Plotly Figure object with strategy indicators
|
||||
"""
|
||||
builder = ChartBuilder()
|
||||
return builder.create_strategy_chart(symbol, timeframe, strategy_name, **kwargs)
|
||||
|
||||
def get_supported_symbols():
|
||||
"""Get list of symbols that have data in the database."""
|
||||
builder = ChartBuilder()
|
||||
candles = builder.fetch_market_data("BTC-USDT", "1m", days_back=1) # Test query
|
||||
if candles:
|
||||
from database.operations import get_database_operations
|
||||
from utils.logger import get_logger
|
||||
logger = get_logger("charts_symbols")
|
||||
|
||||
try:
|
||||
db = get_database_operations(logger)
|
||||
with db.market_data.get_session() as session:
|
||||
from sqlalchemy import text
|
||||
result = session.execute(text("SELECT DISTINCT symbol FROM market_data ORDER BY symbol"))
|
||||
return [row[0] for row in result]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return ['BTC-USDT', 'ETH-USDT'] # Fallback
|
||||
|
||||
def get_supported_timeframes():
|
||||
"""Get list of timeframes that have data in the database."""
|
||||
builder = ChartBuilder()
|
||||
candles = builder.fetch_market_data("BTC-USDT", "1m", days_back=1) # Test query
|
||||
if candles:
|
||||
from database.operations import get_database_operations
|
||||
from utils.logger import get_logger
|
||||
logger = get_logger("charts_timeframes")
|
||||
|
||||
try:
|
||||
db = get_database_operations(logger)
|
||||
with db.market_data.get_session() as session:
|
||||
from sqlalchemy import text
|
||||
result = session.execute(text("SELECT DISTINCT timeframe FROM market_data ORDER BY timeframe"))
|
||||
return [row[0] for row in result]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return ['5s', '1m', '15m', '1h'] # Fallback
|
||||
|
||||
def get_market_statistics(symbol: str, timeframe: str = "1h"):
|
||||
"""Calculate market statistics from recent data."""
|
||||
builder = ChartBuilder()
|
||||
candles = builder.fetch_market_data(symbol, timeframe, days_back=1)
|
||||
|
||||
if not candles:
|
||||
return {'Price': 'N/A', '24h Change': 'N/A', '24h Volume': 'N/A', 'High 24h': 'N/A', 'Low 24h': 'N/A'}
|
||||
|
||||
import pandas as pd
|
||||
df = pd.DataFrame(candles)
|
||||
latest = df.iloc[-1]
|
||||
current_price = float(latest['close'])
|
||||
|
||||
# Calculate 24h change
|
||||
if len(df) > 1:
|
||||
price_24h_ago = float(df.iloc[0]['open'])
|
||||
change_percent = ((current_price - price_24h_ago) / price_24h_ago) * 100
|
||||
else:
|
||||
change_percent = 0
|
||||
|
||||
from .utils import format_price, format_volume
|
||||
return {
|
||||
'Price': format_price(current_price, decimals=2),
|
||||
'24h Change': f"{'+' if change_percent >= 0 else ''}{change_percent:.2f}%",
|
||||
'24h Volume': format_volume(df['volume'].sum()),
|
||||
'High 24h': format_price(df['high'].max(), decimals=2),
|
||||
'Low 24h': format_price(df['low'].min(), decimals=2)
|
||||
}
|
||||
|
||||
def check_data_availability(symbol: str, timeframe: str):
|
||||
"""Check data availability for a symbol and timeframe."""
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from database.operations import get_database_operations
|
||||
from utils.logger import get_logger
|
||||
|
||||
try:
|
||||
logger = get_logger("charts_data_check")
|
||||
db = get_database_operations(logger)
|
||||
latest_candle = db.market_data.get_latest_candle(symbol, timeframe)
|
||||
|
||||
if latest_candle:
|
||||
latest_time = latest_candle['timestamp']
|
||||
time_diff = datetime.now(timezone.utc) - latest_time.replace(tzinfo=timezone.utc)
|
||||
|
||||
return {
|
||||
'has_data': True,
|
||||
'latest_timestamp': latest_time,
|
||||
'time_since_last': time_diff,
|
||||
'is_recent': time_diff < timedelta(hours=1),
|
||||
'message': f"Latest data: {latest_time.strftime('%Y-%m-%d %H:%M:%S UTC')}"
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'has_data': False,
|
||||
'latest_timestamp': None,
|
||||
'time_since_last': None,
|
||||
'is_recent': False,
|
||||
'message': f"No data available for {symbol} {timeframe}"
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
'has_data': False,
|
||||
'latest_timestamp': None,
|
||||
'time_since_last': None,
|
||||
'is_recent': False,
|
||||
'message': f"Error checking data: {str(e)}"
|
||||
}
|
||||
|
||||
def create_data_status_indicator(symbol: str, timeframe: str):
|
||||
"""Create a data status indicator for the dashboard."""
|
||||
status = check_data_availability(symbol, timeframe)
|
||||
|
||||
if status['has_data']:
|
||||
if status['is_recent']:
|
||||
icon, color, status_text = "🟢", "#27ae60", "Real-time Data"
|
||||
else:
|
||||
icon, color, status_text = "🟡", "#f39c12", "Delayed Data"
|
||||
else:
|
||||
icon, color, status_text = "🔴", "#e74c3c", "No Data"
|
||||
|
||||
return f'<span style="color: {color}; font-weight: bold;">{icon} {status_text}</span><br><small>{status["message"]}</small>'
|
||||
|
||||
def create_error_chart(error_message: str):
|
||||
"""Create an error chart with error message."""
|
||||
builder = ChartBuilder()
|
||||
return builder._create_error_chart(error_message)
|
||||
291
components/charts/builder.py
Normal file
291
components/charts/builder.py
Normal file
@@ -0,0 +1,291 @@
|
||||
"""
|
||||
ChartBuilder - Main orchestrator for chart creation
|
||||
|
||||
This module contains the ChartBuilder class which serves as the main entry point
|
||||
for creating charts with various configurations, indicators, and layers.
|
||||
"""
|
||||
|
||||
import plotly.graph_objects as go
|
||||
from plotly.subplots import make_subplots
|
||||
import pandas as pd
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import List, Dict, Any, Optional, Union
|
||||
from decimal import Decimal
|
||||
|
||||
from database.operations import get_database_operations, DatabaseOperationError
|
||||
from utils.logger import get_logger
|
||||
from .utils import validate_market_data, prepare_chart_data, get_indicator_colors
|
||||
|
||||
# Initialize logger
|
||||
logger = get_logger("chart_builder")
|
||||
|
||||
|
||||
class ChartBuilder:
|
||||
"""
|
||||
Main chart builder class for creating modular, configurable charts.
|
||||
|
||||
This class orchestrates the creation of charts by coordinating between
|
||||
data fetching, layer rendering, and configuration management.
|
||||
"""
|
||||
|
||||
def __init__(self, logger_instance: Optional = None):
|
||||
"""
|
||||
Initialize the ChartBuilder.
|
||||
|
||||
Args:
|
||||
logger_instance: Optional logger instance
|
||||
"""
|
||||
self.logger = logger_instance or logger
|
||||
self.db_ops = get_database_operations(self.logger)
|
||||
|
||||
# Chart styling defaults
|
||||
self.default_colors = get_indicator_colors()
|
||||
self.default_height = 600
|
||||
self.default_template = "plotly_white"
|
||||
|
||||
def fetch_market_data(self, symbol: str, timeframe: str,
|
||||
days_back: int = 7, exchange: str = "okx") -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Fetch market data from the database.
|
||||
|
||||
Args:
|
||||
symbol: Trading pair (e.g., 'BTC-USDT')
|
||||
timeframe: Timeframe (e.g., '1h', '1d')
|
||||
days_back: Number of days to look back
|
||||
exchange: Exchange name
|
||||
|
||||
Returns:
|
||||
List of candle data dictionaries
|
||||
"""
|
||||
try:
|
||||
# Calculate time range
|
||||
end_time = datetime.now(timezone.utc)
|
||||
start_time = end_time - timedelta(days=days_back)
|
||||
|
||||
# Fetch candles using the database operations API
|
||||
candles = self.db_ops.market_data.get_candles(
|
||||
symbol=symbol,
|
||||
timeframe=timeframe,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
exchange=exchange
|
||||
)
|
||||
|
||||
self.logger.debug(f"Fetched {len(candles)} candles for {symbol} {timeframe}")
|
||||
return candles
|
||||
|
||||
except DatabaseOperationError as e:
|
||||
self.logger.error(f"Database error fetching market data: {e}")
|
||||
return []
|
||||
except Exception as e:
|
||||
self.logger.error(f"Unexpected error fetching market data: {e}")
|
||||
return []
|
||||
|
||||
def create_candlestick_chart(self, symbol: str, timeframe: str,
|
||||
days_back: int = 7, **kwargs) -> go.Figure:
|
||||
"""
|
||||
Create a basic candlestick chart.
|
||||
|
||||
Args:
|
||||
symbol: Trading pair
|
||||
timeframe: Timeframe
|
||||
days_back: Number of days to look back
|
||||
**kwargs: Additional chart parameters
|
||||
|
||||
Returns:
|
||||
Plotly Figure object with candlestick chart
|
||||
"""
|
||||
try:
|
||||
# Fetch market data
|
||||
candles = self.fetch_market_data(symbol, timeframe, days_back)
|
||||
|
||||
# Handle empty data
|
||||
if not candles:
|
||||
self.logger.warning(f"No data available for {symbol} {timeframe}")
|
||||
return self._create_empty_chart(f"No data available for {symbol} {timeframe}")
|
||||
|
||||
# Validate and prepare data
|
||||
if not validate_market_data(candles):
|
||||
self.logger.error(f"Invalid market data for {symbol} {timeframe}")
|
||||
return self._create_error_chart("Invalid market data format")
|
||||
|
||||
# Prepare chart data
|
||||
df = prepare_chart_data(candles)
|
||||
|
||||
# Determine if we need volume subplot
|
||||
has_volume = 'volume' in df.columns and df['volume'].sum() > 0
|
||||
include_volume = kwargs.get('include_volume', has_volume)
|
||||
|
||||
if include_volume and has_volume:
|
||||
return self._create_candlestick_with_volume(df, symbol, timeframe, **kwargs)
|
||||
else:
|
||||
return self._create_basic_candlestick(df, symbol, timeframe, **kwargs)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error creating candlestick chart for {symbol} {timeframe}: {e}")
|
||||
return self._create_error_chart(f"Error loading chart: {str(e)}")
|
||||
|
||||
def _create_basic_candlestick(self, df: pd.DataFrame, symbol: str,
|
||||
timeframe: str, **kwargs) -> go.Figure:
|
||||
"""Create a basic candlestick chart without volume."""
|
||||
|
||||
# Get custom parameters
|
||||
height = kwargs.get('height', self.default_height)
|
||||
template = kwargs.get('template', self.default_template)
|
||||
|
||||
# Create candlestick chart
|
||||
fig = go.Figure(data=go.Candlestick(
|
||||
x=df['timestamp'],
|
||||
open=df['open'],
|
||||
high=df['high'],
|
||||
low=df['low'],
|
||||
close=df['close'],
|
||||
name=symbol,
|
||||
increasing_line_color=self.default_colors['bullish'],
|
||||
decreasing_line_color=self.default_colors['bearish']
|
||||
))
|
||||
|
||||
# Update layout
|
||||
fig.update_layout(
|
||||
title=f"{symbol} - {timeframe} Chart",
|
||||
xaxis_title="Time",
|
||||
yaxis_title="Price (USDT)",
|
||||
template=template,
|
||||
showlegend=False,
|
||||
height=height,
|
||||
xaxis_rangeslider_visible=False,
|
||||
hovermode='x unified'
|
||||
)
|
||||
|
||||
self.logger.debug(f"Created basic candlestick chart for {symbol} {timeframe} with {len(df)} candles")
|
||||
return fig
|
||||
|
||||
def _create_candlestick_with_volume(self, df: pd.DataFrame, symbol: str,
|
||||
timeframe: str, **kwargs) -> go.Figure:
|
||||
"""Create a candlestick chart with volume subplot."""
|
||||
|
||||
# Get custom parameters
|
||||
height = kwargs.get('height', 700) # Taller for volume subplot
|
||||
template = kwargs.get('template', self.default_template)
|
||||
|
||||
# Create subplots
|
||||
fig = make_subplots(
|
||||
rows=2, cols=1,
|
||||
shared_xaxes=True,
|
||||
vertical_spacing=0.03,
|
||||
subplot_titles=(f'{symbol} Price', 'Volume'),
|
||||
row_heights=[0.7, 0.3] # 70% for price, 30% for volume
|
||||
)
|
||||
|
||||
# Add candlestick chart
|
||||
fig.add_trace(
|
||||
go.Candlestick(
|
||||
x=df['timestamp'],
|
||||
open=df['open'],
|
||||
high=df['high'],
|
||||
low=df['low'],
|
||||
close=df['close'],
|
||||
name=symbol,
|
||||
increasing_line_color=self.default_colors['bullish'],
|
||||
decreasing_line_color=self.default_colors['bearish']
|
||||
),
|
||||
row=1, col=1
|
||||
)
|
||||
|
||||
# Add volume bars with color coding
|
||||
colors = [self.default_colors['bullish'] if close >= open else self.default_colors['bearish']
|
||||
for close, open in zip(df['close'], df['open'])]
|
||||
|
||||
fig.add_trace(
|
||||
go.Bar(
|
||||
x=df['timestamp'],
|
||||
y=df['volume'],
|
||||
name='Volume',
|
||||
marker_color=colors,
|
||||
opacity=0.7
|
||||
),
|
||||
row=2, col=1
|
||||
)
|
||||
|
||||
# Update layout
|
||||
fig.update_layout(
|
||||
title=f"{symbol} - {timeframe} Chart with Volume",
|
||||
template=template,
|
||||
showlegend=False,
|
||||
height=height,
|
||||
xaxis_rangeslider_visible=False,
|
||||
hovermode='x unified'
|
||||
)
|
||||
|
||||
# Update axes
|
||||
fig.update_yaxes(title_text="Price (USDT)", row=1, col=1)
|
||||
fig.update_yaxes(title_text="Volume", row=2, col=1)
|
||||
fig.update_xaxes(title_text="Time", row=2, col=1)
|
||||
|
||||
self.logger.debug(f"Created candlestick chart with volume for {symbol} {timeframe}")
|
||||
return fig
|
||||
|
||||
def _create_empty_chart(self, message: str = "No data available") -> go.Figure:
|
||||
"""Create an empty chart with a message."""
|
||||
fig = go.Figure()
|
||||
|
||||
fig.add_annotation(
|
||||
text=message,
|
||||
xref="paper", yref="paper",
|
||||
x=0.5, y=0.5,
|
||||
xanchor='center', yanchor='middle',
|
||||
showarrow=False,
|
||||
font=dict(size=16, color="#7f8c8d")
|
||||
)
|
||||
|
||||
fig.update_layout(
|
||||
template=self.default_template,
|
||||
height=self.default_height,
|
||||
showlegend=False,
|
||||
xaxis=dict(visible=False),
|
||||
yaxis=dict(visible=False)
|
||||
)
|
||||
|
||||
return fig
|
||||
|
||||
def _create_error_chart(self, error_message: str) -> go.Figure:
|
||||
"""Create an error chart with error message."""
|
||||
fig = go.Figure()
|
||||
|
||||
fig.add_annotation(
|
||||
text=f"⚠️ {error_message}",
|
||||
xref="paper", yref="paper",
|
||||
x=0.5, y=0.5,
|
||||
xanchor='center', yanchor='middle',
|
||||
showarrow=False,
|
||||
font=dict(size=16, color="#e74c3c")
|
||||
)
|
||||
|
||||
fig.update_layout(
|
||||
template=self.default_template,
|
||||
height=self.default_height,
|
||||
showlegend=False,
|
||||
xaxis=dict(visible=False),
|
||||
yaxis=dict(visible=False)
|
||||
)
|
||||
|
||||
return fig
|
||||
|
||||
def create_strategy_chart(self, symbol: str, timeframe: str,
|
||||
strategy_name: str, **kwargs) -> go.Figure:
|
||||
"""
|
||||
Create a strategy-specific chart (placeholder for future implementation).
|
||||
|
||||
Args:
|
||||
symbol: Trading pair
|
||||
timeframe: Timeframe
|
||||
strategy_name: Name of the strategy configuration
|
||||
**kwargs: Additional parameters
|
||||
|
||||
Returns:
|
||||
Plotly Figure object
|
||||
"""
|
||||
# For now, return a basic candlestick chart
|
||||
# This will be enhanced in later tasks with strategy configurations
|
||||
self.logger.info(f"Creating strategy chart for {strategy_name} (basic implementation)")
|
||||
return self.create_candlestick_chart(symbol, timeframe, **kwargs)
|
||||
38
components/charts/config/__init__.py
Normal file
38
components/charts/config/__init__.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""
|
||||
Chart Configuration Package
|
||||
|
||||
This package contains configuration management for the modular chart system,
|
||||
including indicator definitions, strategy-specific configurations, and defaults.
|
||||
"""
|
||||
|
||||
from .indicator_defs import (
|
||||
INDICATOR_DEFINITIONS,
|
||||
ChartIndicatorConfig,
|
||||
calculate_indicators,
|
||||
convert_database_candles_to_ohlcv,
|
||||
get_indicator_display_config,
|
||||
get_available_indicators,
|
||||
get_overlay_indicators,
|
||||
get_subplot_indicators,
|
||||
get_default_indicator_params
|
||||
)
|
||||
|
||||
# Package metadata
|
||||
__version__ = "0.1.0"
|
||||
__package_name__ = "config"
|
||||
|
||||
# Public exports
|
||||
__all__ = [
|
||||
"INDICATOR_DEFINITIONS",
|
||||
"ChartIndicatorConfig",
|
||||
"calculate_indicators",
|
||||
"convert_database_candles_to_ohlcv",
|
||||
"get_indicator_display_config",
|
||||
"get_available_indicators",
|
||||
"get_overlay_indicators",
|
||||
"get_subplot_indicators",
|
||||
"get_default_indicator_params"
|
||||
]
|
||||
|
||||
# Legacy function names for backward compatibility
|
||||
validate_indicator_config = get_default_indicator_params # Will be properly implemented in future tasks
|
||||
266
components/charts/config/indicator_defs.py
Normal file
266
components/charts/config/indicator_defs.py
Normal file
@@ -0,0 +1,266 @@
|
||||
"""
|
||||
Indicator Definitions and Configuration
|
||||
|
||||
This module defines indicator configurations and provides integration
|
||||
with the existing data/common/indicators.py technical indicators module.
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Any, Optional, Union
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from decimal import Decimal
|
||||
|
||||
from data.common.indicators import TechnicalIndicators, IndicatorResult, create_default_indicators_config, validate_indicator_config
|
||||
from data.common.data_types import OHLCVCandle
|
||||
from utils.logger import get_logger
|
||||
|
||||
# Initialize logger
|
||||
logger = get_logger("indicator_defs")
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChartIndicatorConfig:
|
||||
"""
|
||||
Configuration for chart indicators with display properties.
|
||||
|
||||
Extends the base indicator config with chart-specific properties
|
||||
like colors, line styles, and subplot placement.
|
||||
"""
|
||||
name: str
|
||||
indicator_type: str
|
||||
parameters: Dict[str, Any]
|
||||
display_type: str # 'overlay', 'subplot'
|
||||
color: str
|
||||
line_style: str = 'solid' # 'solid', 'dash', 'dot'
|
||||
line_width: int = 2
|
||||
opacity: float = 1.0
|
||||
visible: bool = True
|
||||
subplot_height_ratio: float = 0.3 # For subplot indicators
|
||||
|
||||
def to_indicator_config(self) -> Dict[str, Any]:
|
||||
"""Convert to format expected by TechnicalIndicators."""
|
||||
config = {'type': self.indicator_type}
|
||||
config.update(self.parameters)
|
||||
return config
|
||||
|
||||
|
||||
# Built-in indicator definitions with chart display properties
|
||||
INDICATOR_DEFINITIONS = {
|
||||
'sma_20': ChartIndicatorConfig(
|
||||
name='SMA (20)',
|
||||
indicator_type='sma',
|
||||
parameters={'period': 20, 'price_column': 'close'},
|
||||
display_type='overlay',
|
||||
color='#007bff',
|
||||
line_width=2
|
||||
),
|
||||
'sma_50': ChartIndicatorConfig(
|
||||
name='SMA (50)',
|
||||
indicator_type='sma',
|
||||
parameters={'period': 50, 'price_column': 'close'},
|
||||
display_type='overlay',
|
||||
color='#28a745',
|
||||
line_width=2
|
||||
),
|
||||
'ema_12': ChartIndicatorConfig(
|
||||
name='EMA (12)',
|
||||
indicator_type='ema',
|
||||
parameters={'period': 12, 'price_column': 'close'},
|
||||
display_type='overlay',
|
||||
color='#ff6b35',
|
||||
line_width=2
|
||||
),
|
||||
'ema_26': ChartIndicatorConfig(
|
||||
name='EMA (26)',
|
||||
indicator_type='ema',
|
||||
parameters={'period': 26, 'price_column': 'close'},
|
||||
display_type='overlay',
|
||||
color='#dc3545',
|
||||
line_width=2
|
||||
),
|
||||
'rsi_14': ChartIndicatorConfig(
|
||||
name='RSI (14)',
|
||||
indicator_type='rsi',
|
||||
parameters={'period': 14, 'price_column': 'close'},
|
||||
display_type='subplot',
|
||||
color='#20c997',
|
||||
line_width=2,
|
||||
subplot_height_ratio=0.25
|
||||
),
|
||||
'macd_default': ChartIndicatorConfig(
|
||||
name='MACD',
|
||||
indicator_type='macd',
|
||||
parameters={'fast_period': 12, 'slow_period': 26, 'signal_period': 9, 'price_column': 'close'},
|
||||
display_type='subplot',
|
||||
color='#fd7e14',
|
||||
line_width=2,
|
||||
subplot_height_ratio=0.3
|
||||
),
|
||||
'bollinger_bands': ChartIndicatorConfig(
|
||||
name='Bollinger Bands',
|
||||
indicator_type='bollinger_bands',
|
||||
parameters={'period': 20, 'std_dev': 2.0, 'price_column': 'close'},
|
||||
display_type='overlay',
|
||||
color='#6f42c1',
|
||||
line_width=1,
|
||||
opacity=0.7
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def convert_database_candles_to_ohlcv(candles: List[Dict[str, Any]]) -> List[OHLCVCandle]:
|
||||
"""
|
||||
Convert database candle dictionaries to OHLCVCandle objects.
|
||||
|
||||
Args:
|
||||
candles: List of candle dictionaries from database operations
|
||||
|
||||
Returns:
|
||||
List of OHLCVCandle objects for technical indicators
|
||||
"""
|
||||
ohlcv_candles = []
|
||||
|
||||
for candle in candles:
|
||||
try:
|
||||
# Handle timestamp conversion
|
||||
timestamp = candle['timestamp']
|
||||
if isinstance(timestamp, str):
|
||||
timestamp = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
|
||||
elif timestamp.tzinfo is None:
|
||||
timestamp = timestamp.replace(tzinfo=timezone.utc)
|
||||
|
||||
# For database candles, start_time and end_time are the same
|
||||
# as we store right-aligned timestamps
|
||||
ohlcv_candle = OHLCVCandle(
|
||||
symbol=candle['symbol'],
|
||||
timeframe=candle['timeframe'],
|
||||
start_time=timestamp,
|
||||
end_time=timestamp,
|
||||
open=Decimal(str(candle['open'])),
|
||||
high=Decimal(str(candle['high'])),
|
||||
low=Decimal(str(candle['low'])),
|
||||
close=Decimal(str(candle['close'])),
|
||||
volume=Decimal(str(candle.get('volume', 0))),
|
||||
trade_count=candle.get('trades_count', 0),
|
||||
exchange=candle.get('exchange', 'okx'),
|
||||
is_complete=True
|
||||
)
|
||||
ohlcv_candles.append(ohlcv_candle)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error converting candle to OHLCV: {e}")
|
||||
continue
|
||||
|
||||
logger.debug(f"Converted {len(ohlcv_candles)} database candles to OHLCV format")
|
||||
return ohlcv_candles
|
||||
|
||||
|
||||
def calculate_indicators(candles: List[Dict[str, Any]],
|
||||
indicator_configs: List[str],
|
||||
custom_configs: Optional[Dict[str, ChartIndicatorConfig]] = None) -> Dict[str, List[IndicatorResult]]:
|
||||
"""
|
||||
Calculate technical indicators for chart display.
|
||||
|
||||
Args:
|
||||
candles: List of candle dictionaries from database
|
||||
indicator_configs: List of indicator names to calculate
|
||||
custom_configs: Optional custom indicator configurations
|
||||
|
||||
Returns:
|
||||
Dictionary mapping indicator names to their calculation results
|
||||
"""
|
||||
if not candles:
|
||||
logger.warning("No candles provided for indicator calculation")
|
||||
return {}
|
||||
|
||||
# Convert to OHLCV format
|
||||
ohlcv_candles = convert_database_candles_to_ohlcv(candles)
|
||||
if not ohlcv_candles:
|
||||
logger.error("Failed to convert candles to OHLCV format")
|
||||
return {}
|
||||
|
||||
# Initialize technical indicators calculator
|
||||
indicators_calc = TechnicalIndicators(logger)
|
||||
|
||||
# Prepare configurations
|
||||
configs_to_calculate = {}
|
||||
all_configs = {**INDICATOR_DEFINITIONS}
|
||||
if custom_configs:
|
||||
all_configs.update(custom_configs)
|
||||
|
||||
for indicator_name in indicator_configs:
|
||||
if indicator_name in all_configs:
|
||||
chart_config = all_configs[indicator_name]
|
||||
configs_to_calculate[indicator_name] = chart_config.to_indicator_config()
|
||||
else:
|
||||
logger.warning(f"Unknown indicator configuration: {indicator_name}")
|
||||
|
||||
if not configs_to_calculate:
|
||||
logger.warning("No valid indicator configurations found")
|
||||
return {}
|
||||
|
||||
# Calculate indicators
|
||||
try:
|
||||
results = indicators_calc.calculate_multiple_indicators(ohlcv_candles, configs_to_calculate)
|
||||
logger.debug(f"Calculated {len(results)} indicators successfully")
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error calculating indicators: {e}")
|
||||
return {}
|
||||
|
||||
|
||||
def get_indicator_display_config(indicator_name: str) -> Optional[ChartIndicatorConfig]:
|
||||
"""
|
||||
Get display configuration for an indicator.
|
||||
|
||||
Args:
|
||||
indicator_name: Name of the indicator
|
||||
|
||||
Returns:
|
||||
Chart indicator configuration or None if not found
|
||||
"""
|
||||
return INDICATOR_DEFINITIONS.get(indicator_name)
|
||||
|
||||
|
||||
def get_available_indicators() -> Dict[str, str]:
|
||||
"""
|
||||
Get list of available indicators with descriptions.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping indicator names to descriptions
|
||||
"""
|
||||
return {name: config.name for name, config in INDICATOR_DEFINITIONS.items()}
|
||||
|
||||
|
||||
def get_overlay_indicators() -> List[str]:
|
||||
"""Get list of indicators that display as overlays on the price chart."""
|
||||
return [name for name, config in INDICATOR_DEFINITIONS.items()
|
||||
if config.display_type == 'overlay']
|
||||
|
||||
|
||||
def get_subplot_indicators() -> List[str]:
|
||||
"""Get list of indicators that display in separate subplots."""
|
||||
return [name for name, config in INDICATOR_DEFINITIONS.items()
|
||||
if config.display_type == 'subplot']
|
||||
|
||||
|
||||
def get_default_indicator_params(indicator_type: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Get default parameters for an indicator type.
|
||||
|
||||
Args:
|
||||
indicator_type: Type of indicator ('sma', 'ema', 'rsi', etc.)
|
||||
|
||||
Returns:
|
||||
Dictionary of default parameters
|
||||
"""
|
||||
defaults = {
|
||||
'sma': {'period': 20, 'price_column': 'close'},
|
||||
'ema': {'period': 20, 'price_column': 'close'},
|
||||
'rsi': {'period': 14, 'price_column': 'close'},
|
||||
'macd': {'fast_period': 12, 'slow_period': 26, 'signal_period': 9, 'price_column': 'close'},
|
||||
'bollinger_bands': {'period': 20, 'std_dev': 2.0, 'price_column': 'close'}
|
||||
}
|
||||
|
||||
return defaults.get(indicator_type, {})
|
||||
24
components/charts/layers/__init__.py
Normal file
24
components/charts/layers/__init__.py
Normal file
@@ -0,0 +1,24 @@
|
||||
"""
|
||||
Chart Layers Package
|
||||
|
||||
This package contains the modular chart layer system for rendering different
|
||||
chart components including candlesticks, indicators, and signals.
|
||||
"""
|
||||
|
||||
# Package metadata
|
||||
__version__ = "0.1.0"
|
||||
__package_name__ = "layers"
|
||||
|
||||
# Layers will be imported once they are created
|
||||
# from .base import BaseCandlestickLayer
|
||||
# from .indicators import IndicatorLayer
|
||||
# from .subplots import SubplotManager
|
||||
# from .signals import SignalLayer
|
||||
|
||||
# Public exports (will be populated as layers are implemented)
|
||||
__all__ = [
|
||||
# "BaseCandlestickLayer",
|
||||
# "IndicatorLayer",
|
||||
# "SubplotManager",
|
||||
# "SignalLayer"
|
||||
]
|
||||
293
components/charts/utils.py
Normal file
293
components/charts/utils.py
Normal file
@@ -0,0 +1,293 @@
|
||||
"""
|
||||
Chart Utilities and Helper Functions
|
||||
|
||||
This module provides utility functions for data processing, validation,
|
||||
and chart styling used by the ChartBuilder and layer components.
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Dict, Any, Optional, Union
|
||||
from decimal import Decimal
|
||||
|
||||
from utils.logger import get_logger
|
||||
|
||||
# Initialize logger
|
||||
logger = get_logger("chart_utils")
|
||||
|
||||
# Default color scheme for charts
|
||||
DEFAULT_CHART_COLORS = {
|
||||
'bullish': '#00C851', # Green for bullish candles
|
||||
'bearish': '#FF4444', # Red for bearish candles
|
||||
'sma': '#007bff', # Blue for SMA
|
||||
'ema': '#ff6b35', # Orange for EMA
|
||||
'bb_upper': '#6f42c1', # Purple for Bollinger upper
|
||||
'bb_lower': '#6f42c1', # Purple for Bollinger lower
|
||||
'bb_middle': '#6c757d', # Gray for Bollinger middle
|
||||
'rsi': '#20c997', # Teal for RSI
|
||||
'macd': '#fd7e14', # Orange for MACD
|
||||
'macd_signal': '#e83e8c', # Pink for MACD signal
|
||||
'volume': '#6c757d', # Gray for volume
|
||||
'support': '#17a2b8', # Light blue for support
|
||||
'resistance': '#dc3545' # Red for resistance
|
||||
}
|
||||
|
||||
|
||||
def validate_market_data(candles: List[Dict[str, Any]]) -> bool:
|
||||
"""
|
||||
Validate market data structure and content.
|
||||
|
||||
Args:
|
||||
candles: List of candle dictionaries from database
|
||||
|
||||
Returns:
|
||||
True if data is valid, False otherwise
|
||||
"""
|
||||
if not candles:
|
||||
logger.warning("Empty candles data")
|
||||
return False
|
||||
|
||||
# Check required fields in first candle
|
||||
required_fields = ['timestamp', 'open', 'high', 'low', 'close']
|
||||
first_candle = candles[0]
|
||||
|
||||
for field in required_fields:
|
||||
if field not in first_candle:
|
||||
logger.error(f"Missing required field: {field}")
|
||||
return False
|
||||
|
||||
# Validate data types and values
|
||||
for i, candle in enumerate(candles[:5]): # Check first 5 candles
|
||||
try:
|
||||
# Validate timestamp
|
||||
if not isinstance(candle['timestamp'], (datetime, str)):
|
||||
logger.error(f"Invalid timestamp type at index {i}")
|
||||
return False
|
||||
|
||||
# Validate OHLC values
|
||||
for field in ['open', 'high', 'low', 'close']:
|
||||
value = candle[field]
|
||||
if value is None:
|
||||
logger.error(f"Null value for {field} at index {i}")
|
||||
return False
|
||||
|
||||
# Convert to float for validation
|
||||
try:
|
||||
float_val = float(value)
|
||||
if float_val <= 0:
|
||||
logger.error(f"Non-positive value for {field} at index {i}: {float_val}")
|
||||
return False
|
||||
except (ValueError, TypeError):
|
||||
logger.error(f"Invalid numeric value for {field} at index {i}: {value}")
|
||||
return False
|
||||
|
||||
# Validate OHLC relationships (high >= low, etc.)
|
||||
try:
|
||||
o, h, l, c = float(candle['open']), float(candle['high']), float(candle['low']), float(candle['close'])
|
||||
if not (h >= max(o, c) and l <= min(o, c)):
|
||||
logger.warning(f"Invalid OHLC relationship at index {i}: O={o}, H={h}, L={l}, C={c}")
|
||||
# Don't fail validation for this, just warn
|
||||
|
||||
except (ValueError, TypeError):
|
||||
logger.error(f"Error validating OHLC relationships at index {i}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error validating candle at index {i}: {e}")
|
||||
return False
|
||||
|
||||
logger.debug(f"Market data validation passed for {len(candles)} candles")
|
||||
return True
|
||||
|
||||
|
||||
def prepare_chart_data(candles: List[Dict[str, Any]]) -> pd.DataFrame:
|
||||
"""
|
||||
Convert candle data to pandas DataFrame suitable for charting.
|
||||
|
||||
Args:
|
||||
candles: List of candle dictionaries from database
|
||||
|
||||
Returns:
|
||||
Prepared pandas DataFrame
|
||||
"""
|
||||
try:
|
||||
# Convert to DataFrame
|
||||
df = pd.DataFrame(candles)
|
||||
|
||||
# Ensure timestamp is datetime
|
||||
if 'timestamp' in df.columns:
|
||||
df['timestamp'] = pd.to_datetime(df['timestamp'])
|
||||
|
||||
# Convert OHLCV columns to numeric
|
||||
numeric_columns = ['open', 'high', 'low', 'close']
|
||||
if 'volume' in df.columns:
|
||||
numeric_columns.append('volume')
|
||||
|
||||
for col in numeric_columns:
|
||||
if col in df.columns:
|
||||
df[col] = pd.to_numeric(df[col], errors='coerce')
|
||||
|
||||
# Sort by timestamp
|
||||
df = df.sort_values('timestamp').reset_index(drop=True)
|
||||
|
||||
# Handle missing volume data
|
||||
if 'volume' not in df.columns:
|
||||
df['volume'] = 0
|
||||
|
||||
# Fill any NaN values with forward fill, then backward fill
|
||||
df = df.ffill().bfill()
|
||||
|
||||
logger.debug(f"Prepared chart data: {len(df)} rows, columns: {list(df.columns)}")
|
||||
return df
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error preparing chart data: {e}")
|
||||
# Return empty DataFrame with expected structure
|
||||
return pd.DataFrame(columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
|
||||
|
||||
|
||||
def get_indicator_colors() -> Dict[str, str]:
|
||||
"""
|
||||
Get the default color scheme for chart indicators.
|
||||
|
||||
Returns:
|
||||
Dictionary of color mappings
|
||||
"""
|
||||
return DEFAULT_CHART_COLORS.copy()
|
||||
|
||||
|
||||
def format_price(price: Union[float, Decimal, str], decimals: int = 4) -> str:
|
||||
"""
|
||||
Format price value for display.
|
||||
|
||||
Args:
|
||||
price: Price value to format
|
||||
decimals: Number of decimal places
|
||||
|
||||
Returns:
|
||||
Formatted price string
|
||||
"""
|
||||
try:
|
||||
return f"{float(price):.{decimals}f}"
|
||||
except (ValueError, TypeError):
|
||||
return "N/A"
|
||||
|
||||
|
||||
def format_volume(volume: Union[float, int, str]) -> str:
|
||||
"""
|
||||
Format volume value for display with K/M/B suffixes.
|
||||
|
||||
Args:
|
||||
volume: Volume value to format
|
||||
|
||||
Returns:
|
||||
Formatted volume string
|
||||
"""
|
||||
try:
|
||||
vol = float(volume)
|
||||
if vol >= 1e9:
|
||||
return f"{vol/1e9:.2f}B"
|
||||
elif vol >= 1e6:
|
||||
return f"{vol/1e6:.2f}M"
|
||||
elif vol >= 1e3:
|
||||
return f"{vol/1e3:.2f}K"
|
||||
else:
|
||||
return f"{vol:.0f}"
|
||||
except (ValueError, TypeError):
|
||||
return "N/A"
|
||||
|
||||
|
||||
def calculate_price_change(current: Union[float, Decimal], previous: Union[float, Decimal]) -> Dict[str, Any]:
|
||||
"""
|
||||
Calculate price change and percentage change.
|
||||
|
||||
Args:
|
||||
current: Current price
|
||||
previous: Previous price
|
||||
|
||||
Returns:
|
||||
Dictionary with change, change_percent, and direction
|
||||
"""
|
||||
try:
|
||||
curr = float(current)
|
||||
prev = float(previous)
|
||||
|
||||
if prev == 0:
|
||||
return {'change': 0, 'change_percent': 0, 'direction': 'neutral'}
|
||||
|
||||
change = curr - prev
|
||||
change_percent = (change / prev) * 100
|
||||
|
||||
direction = 'up' if change > 0 else 'down' if change < 0 else 'neutral'
|
||||
|
||||
return {
|
||||
'change': change,
|
||||
'change_percent': change_percent,
|
||||
'direction': direction
|
||||
}
|
||||
|
||||
except (ValueError, TypeError):
|
||||
return {'change': 0, 'change_percent': 0, 'direction': 'neutral'}
|
||||
|
||||
|
||||
def get_chart_height(include_volume: bool = False, num_subplots: int = 0) -> int:
|
||||
"""
|
||||
Calculate appropriate chart height based on components.
|
||||
|
||||
Args:
|
||||
include_volume: Whether volume subplot is included
|
||||
num_subplots: Number of additional subplots (for indicators)
|
||||
|
||||
Returns:
|
||||
Recommended chart height in pixels
|
||||
"""
|
||||
base_height = 500
|
||||
volume_height = 150 if include_volume else 0
|
||||
subplot_height = num_subplots * 120
|
||||
|
||||
return base_height + volume_height + subplot_height
|
||||
|
||||
|
||||
def validate_timeframe(timeframe: str) -> bool:
|
||||
"""
|
||||
Validate if timeframe string is supported.
|
||||
|
||||
Args:
|
||||
timeframe: Timeframe string (e.g., '1m', '5m', '1h', '1d')
|
||||
|
||||
Returns:
|
||||
True if valid, False otherwise
|
||||
"""
|
||||
valid_timeframes = [
|
||||
'1s', '5s', '15s', '30s', # Seconds
|
||||
'1m', '5m', '15m', '30m', # Minutes
|
||||
'1h', '2h', '4h', '6h', '12h', # Hours
|
||||
'1d', '3d', '1w', '1M' # Days, weeks, months
|
||||
]
|
||||
|
||||
return timeframe in valid_timeframes
|
||||
|
||||
|
||||
def validate_symbol(symbol: str) -> bool:
|
||||
"""
|
||||
Validate trading symbol format.
|
||||
|
||||
Args:
|
||||
symbol: Trading symbol (e.g., 'BTC-USDT')
|
||||
|
||||
Returns:
|
||||
True if valid format, False otherwise
|
||||
"""
|
||||
if not symbol or not isinstance(symbol, str):
|
||||
return False
|
||||
|
||||
# Basic validation: should contain a dash and have reasonable length
|
||||
parts = symbol.split('-')
|
||||
if len(parts) != 2:
|
||||
return False
|
||||
|
||||
base, quote = parts
|
||||
if len(base) < 2 or len(quote) < 3 or len(base) > 10 or len(quote) > 10:
|
||||
return False
|
||||
|
||||
return True
|
||||
Reference in New Issue
Block a user