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:
parent
720002a441
commit
c4ec3fac9f
8
app.py
8
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
|
||||
|
||||
@ -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")
|
||||
# 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")
|
||||
|
||||
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 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.
|
||||
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")
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
Returns:
|
||||
List of candle data dictionaries
|
||||
"""
|
||||
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,9 +132,7 @@ 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,
|
||||
@ -374,82 +141,16 @@ def check_data_availability(symbol: str, timeframe: str) -> Dict[str, Any]:
|
||||
'message': f"Error checking data: {str(e)}"
|
||||
}
|
||||
|
||||
|
||||
def create_data_status_indicator(symbol: str, timeframe: str) -> str:
|
||||
"""
|
||||
Create a data status indicator for the dashboard.
|
||||
|
||||
Args:
|
||||
symbol: Trading pair
|
||||
timeframe: Timeframe
|
||||
|
||||
Returns:
|
||||
HTML string for status indicator
|
||||
"""
|
||||
def create_data_status_indicator(symbol: str, timeframe: str):
|
||||
"""Create a data status indicator for the dashboard."""
|
||||
status = check_data_availability(symbol, timeframe)
|
||||
|
||||
if status['has_data']:
|
||||
if status['is_recent']:
|
||||
icon = "🟢"
|
||||
color = "#27ae60"
|
||||
status_text = "Real-time Data"
|
||||
icon, color, status_text = "🟢", "#27ae60", "Real-time Data"
|
||||
else:
|
||||
icon = "🟡"
|
||||
color = "#f39c12"
|
||||
status_text = "Delayed Data"
|
||||
icon, color, status_text = "🟡", "#f39c12", "Delayed Data"
|
||||
else:
|
||||
icon = "🔴"
|
||||
color = "#e74c3c"
|
||||
status_text = "No Data"
|
||||
icon, color, status_text = "🔴", "#e74c3c", "No Data"
|
||||
|
||||
return f'<span style="color: {color}; font-weight: bold;">{icon} {status_text}</span><br><small>{status["message"]}</small>'
|
||||
|
||||
|
||||
def get_supported_symbols() -> List[str]:
|
||||
"""
|
||||
Get list of symbols that have data in the database.
|
||||
|
||||
Returns:
|
||||
List of available trading pairs
|
||||
"""
|
||||
try:
|
||||
db = get_database_operations(logger)
|
||||
|
||||
with db.market_data.get_session() as session:
|
||||
# Query distinct symbols from market_data table
|
||||
from sqlalchemy import text
|
||||
result = session.execute(text("SELECT DISTINCT symbol FROM market_data ORDER BY symbol"))
|
||||
symbols = [row[0] for row in result]
|
||||
|
||||
logger.debug(f"Found {len(symbols)} symbols in database: {symbols}")
|
||||
return symbols
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching supported symbols: {e}")
|
||||
# Return default symbols if database query fails
|
||||
return ['BTC-USDT', 'ETH-USDT', 'LTC-USDT']
|
||||
|
||||
|
||||
def get_supported_timeframes() -> List[str]:
|
||||
"""
|
||||
Get list of timeframes that have data in the database.
|
||||
|
||||
Returns:
|
||||
List of available timeframes
|
||||
"""
|
||||
try:
|
||||
db = get_database_operations(logger)
|
||||
|
||||
with db.market_data.get_session() as session:
|
||||
# Query distinct timeframes from market_data table
|
||||
from sqlalchemy import text
|
||||
result = session.execute(text("SELECT DISTINCT timeframe FROM market_data ORDER BY timeframe"))
|
||||
timeframes = [row[0] for row in result]
|
||||
|
||||
logger.debug(f"Found {len(timeframes)} timeframes in database: {timeframes}")
|
||||
return timeframes
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching supported timeframes: {e}")
|
||||
# Return default timeframes if database query fails
|
||||
return ['1m', '5m', '15m', '1h', '4h', '1d']
|
||||
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
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
8
main.py
8
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}")
|
||||
|
||||
91
tasks/3.4. Chart layers.md
Normal file
91
tasks/3.4. Chart layers.md
Normal file
@ -0,0 +1,91 @@
|
||||
# Task 3.4: Modular Chart Layers System
|
||||
|
||||
## Overview
|
||||
Implementation of a flexible, strategy-driven chart system that supports technical indicator overlays, subplot management, and future bot signal integration. This system will replace the basic chart functionality with a modular architecture that can adapt to different trading strategies and their specific indicator requirements.
|
||||
|
||||
## Relevant Files
|
||||
|
||||
- `components/charts/__init__.py` - Public API exports for the new modular chart system
|
||||
- `components/charts/builder.py` - Main ChartBuilder class orchestrating chart creation and layer management
|
||||
- `components/charts/utils.py` - Chart utilities and helper functions for data processing and validation
|
||||
- `components/charts/config/__init__.py` - Configuration package initialization
|
||||
- `components/charts/config/indicator_defs.py` - Base indicator definitions, schemas, and default parameters
|
||||
- `components/charts/config/strategy_charts.py` - Strategy-specific chart configurations and presets
|
||||
- `components/charts/config/defaults.py` - Default chart configurations and fallback settings
|
||||
- `components/charts/layers/__init__.py` - Chart layers package initialization
|
||||
- `components/charts/layers/base.py` - Base candlestick chart layer implementation
|
||||
- `components/charts/layers/indicators.py` - Indicator overlay rendering (SMA, EMA, Bollinger Bands)
|
||||
- `components/charts/layers/subplots.py` - Subplot management for indicators like RSI and MACD
|
||||
- `components/charts/layers/signals.py` - Strategy signal overlays and trade markers (future bot integration)
|
||||
- `app.py` - Updated dashboard integration with indicator selection controls
|
||||
- `components/dashboard.py` - Enhanced dashboard layout with chart configuration UI
|
||||
- `tests/test_chart_builder.py` - Unit tests for ChartBuilder class functionality
|
||||
- `tests/test_chart_layers.py` - Unit tests for individual chart layer components
|
||||
- `tests/test_chart_integration.py` - Integration tests for full chart creation workflow
|
||||
|
||||
### Notes
|
||||
|
||||
- The modular design allows each chart layer to be tested independently
|
||||
- Strategy configurations are JSON-based for easy modification without code changes
|
||||
- Integration with existing `data/common/indicators.py` for technical indicator calculations
|
||||
- Backward compatibility maintained with existing `components/charts.py` API
|
||||
- Use `uv run pytest tests/test_chart_*.py` to run chart-specific tests
|
||||
- create documentation with importand components in ./docs/components/charts/ folder without redundancy
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] 1.0 Foundation Infrastructure Setup
|
||||
- [x] 1.1 Create components/charts directory structure and package files
|
||||
- [x] 1.2 Implement ChartBuilder class with basic candlestick chart creation
|
||||
- [x] 1.3 Create chart utilities for data processing and validation
|
||||
- [x] 1.4 Integrate with existing data/common/indicators.py module
|
||||
- [x] 1.5 Setup backward compatibility with existing components/charts.py API
|
||||
- [x] 1.6 Create basic unit tests for ChartBuilder class
|
||||
|
||||
- [ ] 2.0 Indicator Layer System Implementation
|
||||
- [ ] 2.1 Create base candlestick chart layer with volume subplot
|
||||
- [ ] 2.2 Implement overlay indicator rendering (SMA, EMA)
|
||||
- [ ] 2.3 Add Bollinger Bands overlay functionality
|
||||
- [ ] 2.4 Create subplot management system for secondary indicators
|
||||
- [ ] 2.5 Implement RSI subplot with proper scaling and styling
|
||||
- [ ] 2.6 Add MACD subplot with signal line and histogram
|
||||
- [ ] 2.7 Create indicator calculation integration with market data
|
||||
- [ ] 2.8 Add error handling for insufficient data scenarios
|
||||
- [ ] 2.9 Unit test all indicator layer components
|
||||
|
||||
- [ ] 3.0 Strategy Configuration System
|
||||
- [ ] 3.1 Design indicator definition schema and validation
|
||||
- [ ] 3.2 Create default indicator configurations and parameters
|
||||
- [ ] 3.3 Implement strategy-specific chart configuration system
|
||||
- [ ] 3.4 Add configuration validation and error handling
|
||||
- [ ] 3.5 Create example strategy configurations (EMA crossover, momentum)
|
||||
- [ ] 3.6 Add configuration fallback mechanisms for missing strategies
|
||||
- [ ] 3.7 Unit test configuration system and validation
|
||||
|
||||
- [ ] 4.0 Dashboard Integration and UI Controls
|
||||
- [ ] 4.1 Add indicator selection checkboxes to dashboard layout
|
||||
- [ ] 4.2 Create real-time chart updates with indicator toggling
|
||||
- [ ] 4.3 Implement parameter adjustment controls for indicators
|
||||
- [ ] 4.4 Add strategy selection dropdown for predefined configurations
|
||||
- [ ] 4.5 Update chart callback functions to handle new layer system
|
||||
- [ ] 4.6 Ensure backward compatibility with existing dashboard features
|
||||
- [ ] 4.7 Test dashboard integration with real market data
|
||||
|
||||
- [ ] 5.0 Signal Layer Foundation for Future Bot Integration
|
||||
- [ ] 5.1 Create signal layer architecture for buy/sell markers
|
||||
- [ ] 5.2 Implement trade entry/exit point visualization
|
||||
- [ ] 5.3 Add support/resistance line drawing capabilities
|
||||
- [ ] 5.4 Create extensible interface for custom strategy signals
|
||||
- [ ] 5.5 Add signal color and style customization options
|
||||
- [ ] 5.6 Prepare integration points for bot management system
|
||||
- [ ] 5.7 Create foundation tests for signal layer functionality
|
||||
|
||||
- [ ] 6.0 Documentation
|
||||
- [ ] 6.1 Create documentation for the chart layers system
|
||||
- [ ] 6.2 Add documentation to the README
|
||||
- [ ] 6.3 Create documentation for the ChartBuilder class
|
||||
- [ ] 6.4 Create documentation for the ChartUtils class
|
||||
- [ ] 6.5 Create documentation for the ChartConfig package
|
||||
- [ ] 6.6 Create documentation how to add new indicators
|
||||
- [ ] 6.7 Create documentation how to add new strategies
|
||||
|
||||
306
tests/test_chart_builder.py
Normal file
306
tests/test_chart_builder.py
Normal file
@ -0,0 +1,306 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Unit Tests for ChartBuilder Class
|
||||
|
||||
Tests for the core ChartBuilder functionality including:
|
||||
- Chart creation
|
||||
- Data fetching
|
||||
- Error handling
|
||||
- Market data integration
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import pandas as pd
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
from typing import List, Dict, Any
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from components.charts.builder import ChartBuilder
|
||||
from components.charts.utils import validate_market_data, prepare_chart_data
|
||||
|
||||
|
||||
class TestChartBuilder:
|
||||
"""Test suite for ChartBuilder class"""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_logger(self):
|
||||
"""Mock logger for testing"""
|
||||
return Mock()
|
||||
|
||||
@pytest.fixture
|
||||
def chart_builder(self, mock_logger):
|
||||
"""Create ChartBuilder instance for testing"""
|
||||
return ChartBuilder(mock_logger)
|
||||
|
||||
@pytest.fixture
|
||||
def sample_candles(self):
|
||||
"""Sample candle data for testing"""
|
||||
base_time = datetime.now(timezone.utc) - timedelta(hours=24)
|
||||
return [
|
||||
{
|
||||
'timestamp': base_time + timedelta(minutes=i),
|
||||
'open': 50000 + i * 10,
|
||||
'high': 50100 + i * 10,
|
||||
'low': 49900 + i * 10,
|
||||
'close': 50050 + i * 10,
|
||||
'volume': 1000 + i * 5,
|
||||
'exchange': 'okx',
|
||||
'symbol': 'BTC-USDT',
|
||||
'timeframe': '1m'
|
||||
}
|
||||
for i in range(100)
|
||||
]
|
||||
|
||||
def test_chart_builder_initialization(self, mock_logger):
|
||||
"""Test ChartBuilder initialization"""
|
||||
builder = ChartBuilder(mock_logger)
|
||||
assert builder.logger == mock_logger
|
||||
assert builder.db_ops is not None
|
||||
assert builder.default_colors is not None
|
||||
assert builder.default_height == 600
|
||||
assert builder.default_template == "plotly_white"
|
||||
|
||||
def test_chart_builder_default_logger(self):
|
||||
"""Test ChartBuilder initialization with default logger"""
|
||||
builder = ChartBuilder()
|
||||
assert builder.logger is not None
|
||||
|
||||
@patch('components.charts.builder.get_database_operations')
|
||||
def test_fetch_market_data_success(self, mock_db_ops, chart_builder, sample_candles):
|
||||
"""Test successful market data fetching"""
|
||||
# Mock database operations
|
||||
mock_db = Mock()
|
||||
mock_db.market_data.get_candles.return_value = sample_candles
|
||||
mock_db_ops.return_value = mock_db
|
||||
|
||||
# Replace the db_ops attribute with our mock
|
||||
chart_builder.db_ops = mock_db
|
||||
|
||||
# Test fetch
|
||||
result = chart_builder.fetch_market_data('BTC-USDT', '1m', days_back=1)
|
||||
|
||||
assert result == sample_candles
|
||||
mock_db.market_data.get_candles.assert_called_once()
|
||||
|
||||
@patch('components.charts.builder.get_database_operations')
|
||||
def test_fetch_market_data_empty(self, mock_db_ops, chart_builder):
|
||||
"""Test market data fetching with empty result"""
|
||||
# Mock empty database result
|
||||
mock_db = Mock()
|
||||
mock_db.market_data.get_candles.return_value = []
|
||||
mock_db_ops.return_value = mock_db
|
||||
|
||||
# Replace the db_ops attribute with our mock
|
||||
chart_builder.db_ops = mock_db
|
||||
|
||||
result = chart_builder.fetch_market_data('BTC-USDT', '1m')
|
||||
|
||||
assert result == []
|
||||
|
||||
@patch('components.charts.builder.get_database_operations')
|
||||
def test_fetch_market_data_exception(self, mock_db_ops, chart_builder):
|
||||
"""Test market data fetching with database exception"""
|
||||
# Mock database exception
|
||||
mock_db = Mock()
|
||||
mock_db.market_data.get_candles.side_effect = Exception("Database error")
|
||||
mock_db_ops.return_value = mock_db
|
||||
|
||||
# Replace the db_ops attribute with our mock
|
||||
chart_builder.db_ops = mock_db
|
||||
|
||||
result = chart_builder.fetch_market_data('BTC-USDT', '1m')
|
||||
|
||||
assert result == []
|
||||
chart_builder.logger.error.assert_called()
|
||||
|
||||
def test_create_candlestick_chart_with_data(self, chart_builder, sample_candles):
|
||||
"""Test candlestick chart creation with valid data"""
|
||||
# Mock fetch_market_data to return sample data
|
||||
chart_builder.fetch_market_data = Mock(return_value=sample_candles)
|
||||
|
||||
fig = chart_builder.create_candlestick_chart('BTC-USDT', '1m')
|
||||
|
||||
assert fig is not None
|
||||
assert len(fig.data) >= 1 # Should have at least candlestick trace
|
||||
assert 'BTC-USDT' in fig.layout.title.text
|
||||
|
||||
def test_create_candlestick_chart_with_volume(self, chart_builder, sample_candles):
|
||||
"""Test candlestick chart creation with volume subplot"""
|
||||
chart_builder.fetch_market_data = Mock(return_value=sample_candles)
|
||||
|
||||
fig = chart_builder.create_candlestick_chart('BTC-USDT', '1m', include_volume=True)
|
||||
|
||||
assert fig is not None
|
||||
assert len(fig.data) >= 2 # Should have candlestick + volume traces
|
||||
|
||||
def test_create_candlestick_chart_no_data(self, chart_builder):
|
||||
"""Test candlestick chart creation with no data"""
|
||||
chart_builder.fetch_market_data = Mock(return_value=[])
|
||||
|
||||
fig = chart_builder.create_candlestick_chart('BTC-USDT', '1m')
|
||||
|
||||
assert fig is not None
|
||||
# Check for annotation with message instead of title
|
||||
assert len(fig.layout.annotations) > 0
|
||||
assert "No data available" in fig.layout.annotations[0].text
|
||||
|
||||
def test_create_candlestick_chart_invalid_data(self, chart_builder):
|
||||
"""Test candlestick chart creation with invalid data"""
|
||||
invalid_data = [{'invalid': 'data'}]
|
||||
chart_builder.fetch_market_data = Mock(return_value=invalid_data)
|
||||
|
||||
fig = chart_builder.create_candlestick_chart('BTC-USDT', '1m')
|
||||
|
||||
assert fig is not None
|
||||
# Should show error chart
|
||||
assert len(fig.layout.annotations) > 0
|
||||
assert "Invalid market data" in fig.layout.annotations[0].text
|
||||
|
||||
def test_create_strategy_chart_basic_implementation(self, chart_builder, sample_candles):
|
||||
"""Test strategy chart creation (currently returns basic chart)"""
|
||||
chart_builder.fetch_market_data = Mock(return_value=sample_candles)
|
||||
|
||||
result = chart_builder.create_strategy_chart('BTC-USDT', '1m', 'test_strategy')
|
||||
|
||||
assert result is not None
|
||||
# Should currently return a basic candlestick chart
|
||||
assert 'BTC-USDT' in result.layout.title.text
|
||||
|
||||
def test_create_empty_chart(self, chart_builder):
|
||||
"""Test empty chart creation"""
|
||||
fig = chart_builder._create_empty_chart("Test message")
|
||||
|
||||
assert fig is not None
|
||||
assert len(fig.layout.annotations) > 0
|
||||
assert "Test message" in fig.layout.annotations[0].text
|
||||
assert len(fig.data) == 0
|
||||
|
||||
def test_create_error_chart(self, chart_builder):
|
||||
"""Test error chart creation"""
|
||||
fig = chart_builder._create_error_chart("Test error")
|
||||
|
||||
assert fig is not None
|
||||
assert len(fig.layout.annotations) > 0
|
||||
assert "Test error" in fig.layout.annotations[0].text
|
||||
|
||||
|
||||
class TestChartBuilderIntegration:
|
||||
"""Integration tests for ChartBuilder with real components"""
|
||||
|
||||
@pytest.fixture
|
||||
def chart_builder(self):
|
||||
"""Create ChartBuilder for integration testing"""
|
||||
return ChartBuilder()
|
||||
|
||||
def test_market_data_validation_integration(self, chart_builder):
|
||||
"""Test integration with market data validation"""
|
||||
# Test with valid data structure
|
||||
valid_data = [
|
||||
{
|
||||
'timestamp': datetime.now(timezone.utc),
|
||||
'open': 50000,
|
||||
'high': 50100,
|
||||
'low': 49900,
|
||||
'close': 50050,
|
||||
'volume': 1000
|
||||
}
|
||||
]
|
||||
|
||||
assert validate_market_data(valid_data) is True
|
||||
|
||||
def test_chart_data_preparation_integration(self, chart_builder):
|
||||
"""Test integration with chart data preparation"""
|
||||
raw_data = [
|
||||
{
|
||||
'timestamp': datetime.now(timezone.utc) - timedelta(hours=1),
|
||||
'open': '50000', # String values to test conversion
|
||||
'high': '50100',
|
||||
'low': '49900',
|
||||
'close': '50050',
|
||||
'volume': '1000'
|
||||
},
|
||||
{
|
||||
'timestamp': datetime.now(timezone.utc),
|
||||
'open': '50050',
|
||||
'high': '50150',
|
||||
'low': '49950',
|
||||
'close': '50100',
|
||||
'volume': '1200'
|
||||
}
|
||||
]
|
||||
|
||||
df = prepare_chart_data(raw_data)
|
||||
|
||||
assert isinstance(df, pd.DataFrame)
|
||||
assert len(df) == 2
|
||||
assert all(col in df.columns for col in ['timestamp', 'open', 'high', 'low', 'close', 'volume'])
|
||||
assert df['open'].dtype.kind in 'fi' # Float or integer
|
||||
|
||||
|
||||
class TestChartBuilderEdgeCases:
|
||||
"""Test edge cases and error conditions"""
|
||||
|
||||
@pytest.fixture
|
||||
def chart_builder(self):
|
||||
return ChartBuilder()
|
||||
|
||||
def test_chart_creation_with_single_candle(self, chart_builder):
|
||||
"""Test chart creation with only one candle"""
|
||||
single_candle = [{
|
||||
'timestamp': datetime.now(timezone.utc),
|
||||
'open': 50000,
|
||||
'high': 50100,
|
||||
'low': 49900,
|
||||
'close': 50050,
|
||||
'volume': 1000
|
||||
}]
|
||||
|
||||
chart_builder.fetch_market_data = Mock(return_value=single_candle)
|
||||
fig = chart_builder.create_candlestick_chart('BTC-USDT', '1m')
|
||||
|
||||
assert fig is not None
|
||||
assert len(fig.data) >= 1
|
||||
|
||||
def test_chart_creation_with_missing_volume(self, chart_builder):
|
||||
"""Test chart creation with missing volume data"""
|
||||
no_volume_data = [{
|
||||
'timestamp': datetime.now(timezone.utc),
|
||||
'open': 50000,
|
||||
'high': 50100,
|
||||
'low': 49900,
|
||||
'close': 50050
|
||||
# No volume field
|
||||
}]
|
||||
|
||||
chart_builder.fetch_market_data = Mock(return_value=no_volume_data)
|
||||
fig = chart_builder.create_candlestick_chart('BTC-USDT', '1m', include_volume=True)
|
||||
|
||||
assert fig is not None
|
||||
# Should handle missing volume gracefully
|
||||
|
||||
def test_chart_creation_with_none_values(self, chart_builder):
|
||||
"""Test chart creation with None values in data"""
|
||||
data_with_nulls = [{
|
||||
'timestamp': datetime.now(timezone.utc),
|
||||
'open': 50000,
|
||||
'high': None, # Null value
|
||||
'low': 49900,
|
||||
'close': 50050,
|
||||
'volume': 1000
|
||||
}]
|
||||
|
||||
chart_builder.fetch_market_data = Mock(return_value=data_with_nulls)
|
||||
fig = chart_builder.create_candlestick_chart('BTC-USDT', '1m')
|
||||
|
||||
assert fig is not None
|
||||
# Should handle null values gracefully
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Run tests if executed directly
|
||||
pytest.main([__file__, '-v'])
|
||||
Loading…
x
Reference in New Issue
Block a user