- 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.
306 lines
11 KiB
Python
306 lines
11 KiB
Python
#!/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']) |