Implement modular chart layers and error handling for Crypto Trading Bot Dashboard - Introduced a comprehensive chart layer system in `components/charts/layers/` to support various technical indicators and subplots. - Added base layer components including `BaseLayer`, `CandlestickLayer`, and `VolumeLayer` for flexible chart rendering. - Implemented overlay indicators such as `SMALayer`, `EMALayer`, and `BollingerBandsLayer` with robust error handling. - Created subplot layers for indicators like `RSILayer` and `MACDLayer`, enhancing visualization capabilities. - Developed a `MarketDataIntegrator` for seamless data fetching and validation, improving data quality assurance. - Enhanced error handling utilities in `components/charts/error_handling.py` to manage insufficient data scenarios effectively. - Updated documentation to reflect the new chart layer architecture and usage guidelines. - Added unit tests for all chart layer components to ensure functionality and reliability.
711 lines
25 KiB
Python
711 lines
25 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Comprehensive Unit Tests for Chart Layer Components
|
|
|
|
Tests for all chart layer functionality including:
|
|
- Error handling system
|
|
- Base layer components (CandlestickLayer, VolumeLayer, LayerManager)
|
|
- Indicator layers (SMA, EMA, Bollinger Bands)
|
|
- Subplot layers (RSI, MACD)
|
|
- Integration and error recovery
|
|
"""
|
|
|
|
import pytest
|
|
import pandas as pd
|
|
import plotly.graph_objects as go
|
|
from datetime import datetime, timezone, timedelta
|
|
from unittest.mock import Mock, patch, MagicMock
|
|
from typing import List, Dict, Any
|
|
from decimal import Decimal
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
project_root = Path(__file__).parent.parent
|
|
sys.path.insert(0, str(project_root))
|
|
|
|
# Import components to test
|
|
from components.charts.error_handling import (
|
|
ChartErrorHandler, ChartError, ErrorSeverity, DataRequirements,
|
|
ErrorRecoveryStrategies, check_data_sufficiency, get_error_message
|
|
)
|
|
|
|
from components.charts.layers.base import (
|
|
LayerConfig, BaseLayer, CandlestickLayer, VolumeLayer, LayerManager
|
|
)
|
|
|
|
from components.charts.layers.indicators import (
|
|
IndicatorLayerConfig, BaseIndicatorLayer, SMALayer, EMALayer, BollingerBandsLayer
|
|
)
|
|
|
|
from components.charts.layers.subplots import (
|
|
SubplotLayerConfig, BaseSubplotLayer, RSILayer, MACDLayer
|
|
)
|
|
|
|
|
|
class TestErrorHandlingSystem:
|
|
"""Test suite for chart error handling system"""
|
|
|
|
@pytest.fixture
|
|
def sample_data(self):
|
|
"""Sample market 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
|
|
}
|
|
for i in range(50) # 50 data points
|
|
]
|
|
|
|
@pytest.fixture
|
|
def insufficient_data(self):
|
|
"""Insufficient market data for testing"""
|
|
base_time = datetime.now(timezone.utc)
|
|
return [
|
|
{
|
|
'timestamp': base_time + timedelta(minutes=i),
|
|
'open': 50000,
|
|
'high': 50100,
|
|
'low': 49900,
|
|
'close': 50050,
|
|
'volume': 1000
|
|
}
|
|
for i in range(5) # Only 5 data points
|
|
]
|
|
|
|
def test_chart_error_creation(self):
|
|
"""Test ChartError dataclass creation"""
|
|
error = ChartError(
|
|
code='TEST_ERROR',
|
|
message='Test error message',
|
|
severity=ErrorSeverity.ERROR,
|
|
context={'test': 'value'},
|
|
recovery_suggestion='Fix the test'
|
|
)
|
|
|
|
assert error.code == 'TEST_ERROR'
|
|
assert error.message == 'Test error message'
|
|
assert error.severity == ErrorSeverity.ERROR
|
|
assert error.context == {'test': 'value'}
|
|
assert error.recovery_suggestion == 'Fix the test'
|
|
|
|
# Test dict conversion
|
|
error_dict = error.to_dict()
|
|
assert error_dict['code'] == 'TEST_ERROR'
|
|
assert error_dict['severity'] == 'error'
|
|
|
|
def test_data_requirements_candlestick(self):
|
|
"""Test data requirements checking for candlestick charts"""
|
|
# Test sufficient data
|
|
error = DataRequirements.check_candlestick_requirements(50)
|
|
assert error.severity == ErrorSeverity.INFO
|
|
assert error.code == 'SUFFICIENT_DATA'
|
|
|
|
# Test insufficient data
|
|
error = DataRequirements.check_candlestick_requirements(5)
|
|
assert error.severity == ErrorSeverity.WARNING
|
|
assert error.code == 'INSUFFICIENT_CANDLESTICK_DATA'
|
|
|
|
# Test no data
|
|
error = DataRequirements.check_candlestick_requirements(0)
|
|
assert error.severity == ErrorSeverity.CRITICAL
|
|
assert error.code == 'NO_DATA'
|
|
|
|
def test_data_requirements_indicators(self):
|
|
"""Test data requirements checking for indicators"""
|
|
# Test SMA with sufficient data
|
|
error = DataRequirements.check_indicator_requirements('sma', 50, {'period': 20})
|
|
assert error.severity == ErrorSeverity.INFO
|
|
|
|
# Test SMA with insufficient data
|
|
error = DataRequirements.check_indicator_requirements('sma', 15, {'period': 20})
|
|
assert error.severity == ErrorSeverity.WARNING
|
|
assert error.code == 'INSUFFICIENT_INDICATOR_DATA'
|
|
|
|
# Test unknown indicator
|
|
error = DataRequirements.check_indicator_requirements('unknown', 50, {})
|
|
assert error.severity == ErrorSeverity.ERROR
|
|
assert error.code == 'UNKNOWN_INDICATOR'
|
|
|
|
def test_chart_error_handler(self, sample_data, insufficient_data):
|
|
"""Test ChartErrorHandler functionality"""
|
|
handler = ChartErrorHandler()
|
|
|
|
# Test with sufficient data
|
|
is_valid = handler.validate_data_sufficiency(sample_data)
|
|
assert is_valid == True
|
|
assert len(handler.errors) == 0
|
|
|
|
# Test with insufficient data and indicators
|
|
indicators = [{'type': 'sma', 'parameters': {'period': 30}}]
|
|
is_valid = handler.validate_data_sufficiency(insufficient_data, indicators=indicators)
|
|
assert is_valid == False
|
|
assert len(handler.errors) > 0 or len(handler.warnings) > 0
|
|
|
|
# Test error summary
|
|
summary = handler.get_error_summary()
|
|
assert 'has_errors' in summary
|
|
assert 'can_proceed' in summary
|
|
|
|
def test_convenience_functions(self, sample_data, insufficient_data):
|
|
"""Test convenience functions for error handling"""
|
|
# Test check_data_sufficiency
|
|
is_sufficient, summary = check_data_sufficiency(sample_data)
|
|
assert is_sufficient == True
|
|
assert summary['can_proceed'] == True
|
|
|
|
# Test with insufficient data
|
|
indicators = [{'type': 'sma', 'parameters': {'period': 100}}]
|
|
is_sufficient, summary = check_data_sufficiency(insufficient_data, indicators)
|
|
assert is_sufficient == False
|
|
|
|
# Test get_error_message
|
|
error_msg = get_error_message(insufficient_data, indicators)
|
|
assert isinstance(error_msg, str)
|
|
assert len(error_msg) > 0
|
|
|
|
|
|
class TestBaseLayerSystem:
|
|
"""Test suite for base layer components"""
|
|
|
|
@pytest.fixture
|
|
def sample_df(self):
|
|
"""Sample DataFrame for testing"""
|
|
base_time = datetime.now(timezone.utc) - timedelta(hours=24)
|
|
data = []
|
|
for i in range(100):
|
|
data.append({
|
|
'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
|
|
})
|
|
return pd.DataFrame(data)
|
|
|
|
@pytest.fixture
|
|
def invalid_df(self):
|
|
"""Invalid DataFrame for testing error handling"""
|
|
return pd.DataFrame([
|
|
{'timestamp': datetime.now(), 'open': -100, 'high': 50, 'low': 60, 'close': 40, 'volume': -50},
|
|
{'timestamp': datetime.now(), 'open': None, 'high': None, 'low': None, 'close': None, 'volume': None}
|
|
])
|
|
|
|
def test_layer_config(self):
|
|
"""Test LayerConfig creation"""
|
|
config = LayerConfig(name="test", enabled=True, color="#FF0000")
|
|
assert config.name == "test"
|
|
assert config.enabled == True
|
|
assert config.color == "#FF0000"
|
|
assert config.style == {}
|
|
assert config.subplot_row is None
|
|
|
|
def test_base_layer(self):
|
|
"""Test BaseLayer functionality"""
|
|
config = LayerConfig(name="test_layer")
|
|
layer = BaseLayer(config)
|
|
|
|
assert layer.config.name == "test_layer"
|
|
assert hasattr(layer, 'error_handler')
|
|
assert hasattr(layer, 'logger')
|
|
|
|
def test_candlestick_layer_validation(self, sample_df, invalid_df):
|
|
"""Test CandlestickLayer data validation"""
|
|
layer = CandlestickLayer()
|
|
|
|
# Test valid data
|
|
is_valid = layer.validate_data(sample_df)
|
|
assert is_valid == True
|
|
|
|
# Test invalid data
|
|
is_valid = layer.validate_data(invalid_df)
|
|
assert is_valid == False
|
|
assert len(layer.error_handler.errors) > 0
|
|
|
|
def test_candlestick_layer_render(self, sample_df):
|
|
"""Test CandlestickLayer rendering"""
|
|
layer = CandlestickLayer()
|
|
fig = go.Figure()
|
|
|
|
result_fig = layer.render(fig, sample_df)
|
|
assert result_fig is not None
|
|
assert len(result_fig.data) >= 1 # Should have candlestick trace
|
|
|
|
def test_volume_layer_validation(self, sample_df, invalid_df):
|
|
"""Test VolumeLayer data validation"""
|
|
layer = VolumeLayer()
|
|
|
|
# Test valid data
|
|
is_valid = layer.validate_data(sample_df)
|
|
assert is_valid == True
|
|
|
|
# Test invalid data (some volume issues)
|
|
is_valid = layer.validate_data(invalid_df)
|
|
# Volume layer should handle invalid data gracefully
|
|
assert len(layer.error_handler.warnings) >= 0 # May have warnings
|
|
|
|
def test_volume_layer_render(self, sample_df):
|
|
"""Test VolumeLayer rendering"""
|
|
layer = VolumeLayer()
|
|
fig = go.Figure()
|
|
|
|
result_fig = layer.render(fig, sample_df)
|
|
assert result_fig is not None
|
|
|
|
def test_layer_manager(self, sample_df):
|
|
"""Test LayerManager functionality"""
|
|
manager = LayerManager()
|
|
|
|
# Add layers
|
|
candlestick_layer = CandlestickLayer()
|
|
volume_layer = VolumeLayer()
|
|
manager.add_layer(candlestick_layer)
|
|
manager.add_layer(volume_layer)
|
|
|
|
assert len(manager.layers) == 2
|
|
|
|
# Test enabled layers
|
|
enabled = manager.get_enabled_layers()
|
|
assert len(enabled) == 2
|
|
|
|
# Test overlay vs subplot layers
|
|
overlays = manager.get_overlay_layers()
|
|
subplots = manager.get_subplot_layers()
|
|
|
|
assert len(overlays) == 1 # Candlestick is overlay
|
|
assert len(subplots) >= 1 # Volume is subplot
|
|
|
|
# Test layout calculation
|
|
layout_config = manager.calculate_subplot_layout()
|
|
assert 'rows' in layout_config
|
|
assert 'cols' in layout_config
|
|
assert layout_config['rows'] >= 2 # Main chart + volume subplot
|
|
|
|
# Test rendering all layers
|
|
fig = manager.render_all_layers(sample_df)
|
|
assert fig is not None
|
|
assert len(fig.data) >= 2 # Candlestick + volume
|
|
|
|
|
|
class TestIndicatorLayers:
|
|
"""Test suite for indicator layer components"""
|
|
|
|
@pytest.fixture
|
|
def sample_df(self):
|
|
"""Sample DataFrame with trend for indicator testing"""
|
|
base_time = datetime.now(timezone.utc) - timedelta(hours=24)
|
|
data = []
|
|
for i in range(100):
|
|
# Create trending data for better indicator calculation
|
|
trend = i * 0.1
|
|
base_price = 50000 + trend
|
|
data.append({
|
|
'timestamp': base_time + timedelta(minutes=i),
|
|
'open': base_price + (i % 3) * 10,
|
|
'high': base_price + 50 + (i % 3) * 10,
|
|
'low': base_price - 50 + (i % 3) * 10,
|
|
'close': base_price + (i % 2) * 10,
|
|
'volume': 1000 + i * 5
|
|
})
|
|
return pd.DataFrame(data)
|
|
|
|
@pytest.fixture
|
|
def insufficient_df(self):
|
|
"""Insufficient data for indicator testing"""
|
|
base_time = datetime.now(timezone.utc)
|
|
data = []
|
|
for i in range(10): # Only 10 data points
|
|
data.append({
|
|
'timestamp': base_time + timedelta(minutes=i),
|
|
'open': 50000,
|
|
'high': 50100,
|
|
'low': 49900,
|
|
'close': 50050,
|
|
'volume': 1000
|
|
})
|
|
return pd.DataFrame(data)
|
|
|
|
def test_indicator_layer_config(self):
|
|
"""Test IndicatorLayerConfig creation"""
|
|
config = IndicatorLayerConfig(
|
|
name="test_indicator",
|
|
indicator_type="sma",
|
|
parameters={'period': 20}
|
|
)
|
|
|
|
assert config.name == "test_indicator"
|
|
assert config.indicator_type == "sma"
|
|
assert config.parameters == {'period': 20}
|
|
assert config.line_width == 2
|
|
assert config.opacity == 1.0
|
|
|
|
def test_sma_layer(self, sample_df, insufficient_df):
|
|
"""Test SMALayer functionality"""
|
|
config = IndicatorLayerConfig(
|
|
name="SMA(20)",
|
|
indicator_type='sma',
|
|
parameters={'period': 20}
|
|
)
|
|
layer = SMALayer(config)
|
|
|
|
# Test with sufficient data
|
|
is_valid = layer.validate_indicator_data(sample_df, required_columns=['close', 'timestamp'])
|
|
assert is_valid == True
|
|
|
|
# Test calculation
|
|
sma_data = layer._calculate_sma(sample_df, 20)
|
|
assert sma_data is not None
|
|
assert 'sma' in sma_data.columns
|
|
assert len(sma_data) > 0
|
|
|
|
# Test with insufficient data
|
|
is_valid = layer.validate_indicator_data(insufficient_df, required_columns=['close', 'timestamp'])
|
|
# Should have warnings but may still be valid for short periods
|
|
assert len(layer.error_handler.warnings) >= 0
|
|
|
|
def test_ema_layer(self, sample_df):
|
|
"""Test EMALayer functionality"""
|
|
config = IndicatorLayerConfig(
|
|
name="EMA(12)",
|
|
indicator_type='ema',
|
|
parameters={'period': 12}
|
|
)
|
|
layer = EMALayer(config)
|
|
|
|
# Test validation
|
|
is_valid = layer.validate_indicator_data(sample_df, required_columns=['close', 'timestamp'])
|
|
assert is_valid == True
|
|
|
|
# Test calculation
|
|
ema_data = layer._calculate_ema(sample_df, 12)
|
|
assert ema_data is not None
|
|
assert 'ema' in ema_data.columns
|
|
assert len(ema_data) > 0
|
|
|
|
def test_bollinger_bands_layer(self, sample_df):
|
|
"""Test BollingerBandsLayer functionality"""
|
|
config = IndicatorLayerConfig(
|
|
name="BB(20,2)",
|
|
indicator_type='bollinger_bands',
|
|
parameters={'period': 20, 'std_dev': 2}
|
|
)
|
|
layer = BollingerBandsLayer(config)
|
|
|
|
# Test validation
|
|
is_valid = layer.validate_indicator_data(sample_df, required_columns=['close', 'timestamp'])
|
|
assert is_valid == True
|
|
|
|
# Test calculation
|
|
bb_data = layer._calculate_bollinger_bands(sample_df, 20, 2)
|
|
assert bb_data is not None
|
|
assert 'upper_band' in bb_data.columns
|
|
assert 'middle_band' in bb_data.columns
|
|
assert 'lower_band' in bb_data.columns
|
|
assert len(bb_data) > 0
|
|
|
|
def test_safe_calculate_indicator(self, sample_df, insufficient_df):
|
|
"""Test safe indicator calculation with error handling"""
|
|
config = IndicatorLayerConfig(
|
|
name="SMA(20)",
|
|
indicator_type='sma',
|
|
parameters={'period': 20}
|
|
)
|
|
layer = SMALayer(config)
|
|
|
|
# Test successful calculation
|
|
result = layer.safe_calculate_indicator(
|
|
sample_df,
|
|
layer._calculate_sma,
|
|
period=20
|
|
)
|
|
assert result is not None
|
|
|
|
# Test with insufficient data - should attempt recovery
|
|
result = layer.safe_calculate_indicator(
|
|
insufficient_df,
|
|
layer._calculate_sma,
|
|
period=50 # Too large for data
|
|
)
|
|
# Should either return adjusted result or None
|
|
assert result is None or len(result) > 0
|
|
|
|
|
|
class TestSubplotLayers:
|
|
"""Test suite for subplot layer components"""
|
|
|
|
@pytest.fixture
|
|
def sample_df(self):
|
|
"""Sample DataFrame for RSI/MACD testing"""
|
|
base_time = datetime.now(timezone.utc) - timedelta(hours=24)
|
|
data = []
|
|
|
|
# Create more realistic price data for RSI/MACD
|
|
prices = [50000]
|
|
for i in range(100):
|
|
# Random walk with trend
|
|
change = (i % 7 - 3) * 50 # Some volatility
|
|
new_price = prices[-1] + change
|
|
prices.append(new_price)
|
|
|
|
data.append({
|
|
'timestamp': base_time + timedelta(minutes=i),
|
|
'open': prices[i],
|
|
'high': prices[i] + abs(change) + 20,
|
|
'low': prices[i] - abs(change) - 20,
|
|
'close': prices[i+1],
|
|
'volume': 1000 + i * 5
|
|
})
|
|
|
|
return pd.DataFrame(data)
|
|
|
|
def test_subplot_layer_config(self):
|
|
"""Test SubplotLayerConfig creation"""
|
|
config = SubplotLayerConfig(
|
|
name="RSI(14)",
|
|
indicator_type="rsi",
|
|
parameters={'period': 14},
|
|
subplot_height_ratio=0.25,
|
|
y_axis_range=(0, 100),
|
|
reference_lines=[30, 70]
|
|
)
|
|
|
|
assert config.name == "RSI(14)"
|
|
assert config.indicator_type == "rsi"
|
|
assert config.subplot_height_ratio == 0.25
|
|
assert config.y_axis_range == (0, 100)
|
|
assert config.reference_lines == [30, 70]
|
|
|
|
def test_rsi_layer(self, sample_df):
|
|
"""Test RSILayer functionality"""
|
|
layer = RSILayer(period=14)
|
|
|
|
# Test validation
|
|
is_valid = layer.validate_indicator_data(sample_df, required_columns=['close', 'timestamp'])
|
|
assert is_valid == True
|
|
|
|
# Test RSI calculation
|
|
rsi_data = layer._calculate_rsi(sample_df, 14)
|
|
assert rsi_data is not None
|
|
assert 'rsi' in rsi_data.columns
|
|
assert len(rsi_data) > 0
|
|
|
|
# Validate RSI values are in correct range
|
|
assert (rsi_data['rsi'] >= 0).all()
|
|
assert (rsi_data['rsi'] <= 100).all()
|
|
|
|
# Test subplot properties
|
|
assert layer.has_fixed_range() == True
|
|
assert layer.get_y_axis_range() == (0, 100)
|
|
assert 30 in layer.get_reference_lines()
|
|
assert 70 in layer.get_reference_lines()
|
|
|
|
def test_macd_layer(self, sample_df):
|
|
"""Test MACDLayer functionality"""
|
|
layer = MACDLayer(fast_period=12, slow_period=26, signal_period=9)
|
|
|
|
# Test validation
|
|
is_valid = layer.validate_indicator_data(sample_df, required_columns=['close', 'timestamp'])
|
|
assert is_valid == True
|
|
|
|
# Test MACD calculation
|
|
macd_data = layer._calculate_macd(sample_df, 12, 26, 9)
|
|
assert macd_data is not None
|
|
assert 'macd' in macd_data.columns
|
|
assert 'signal' in macd_data.columns
|
|
assert 'histogram' in macd_data.columns
|
|
assert len(macd_data) > 0
|
|
|
|
# Test subplot properties
|
|
assert layer.should_show_zero_line() == True
|
|
assert layer.get_subplot_height_ratio() == 0.3
|
|
|
|
def test_rsi_calculation_edge_cases(self, sample_df):
|
|
"""Test RSI calculation with edge cases"""
|
|
layer = RSILayer(period=14)
|
|
|
|
# Test with very short period
|
|
short_data = sample_df.head(20)
|
|
rsi_data = layer._calculate_rsi(short_data, 5) # Short period
|
|
assert rsi_data is not None
|
|
assert len(rsi_data) > 0
|
|
|
|
# Test with period too large for data
|
|
try:
|
|
layer._calculate_rsi(sample_df.head(10), 20) # Period larger than data
|
|
assert False, "Should have raised an error"
|
|
except Exception:
|
|
pass # Expected to fail
|
|
|
|
def test_macd_calculation_edge_cases(self, sample_df):
|
|
"""Test MACD calculation with edge cases"""
|
|
layer = MACDLayer(fast_period=12, slow_period=26, signal_period=9)
|
|
|
|
# Test with invalid periods (fast >= slow)
|
|
try:
|
|
layer._calculate_macd(sample_df, 26, 12, 9) # fast >= slow
|
|
assert False, "Should have raised an error"
|
|
except Exception:
|
|
pass # Expected to fail
|
|
|
|
|
|
class TestLayerIntegration:
|
|
"""Test suite for layer integration and complex scenarios"""
|
|
|
|
@pytest.fixture
|
|
def sample_df(self):
|
|
"""Sample DataFrame for integration testing"""
|
|
base_time = datetime.now(timezone.utc) - timedelta(hours=24)
|
|
data = []
|
|
for i in range(150): # Enough data for all indicators
|
|
trend = i * 0.1
|
|
base_price = 50000 + trend
|
|
volatility = (i % 10) * 20
|
|
|
|
data.append({
|
|
'timestamp': base_time + timedelta(minutes=i),
|
|
'open': base_price + volatility,
|
|
'high': base_price + volatility + 50,
|
|
'low': base_price + volatility - 50,
|
|
'close': base_price + volatility + (i % 3 - 1) * 10,
|
|
'volume': 1000 + i * 5
|
|
})
|
|
return pd.DataFrame(data)
|
|
|
|
def test_full_chart_creation(self, sample_df):
|
|
"""Test creating a full chart with multiple layers"""
|
|
manager = LayerManager()
|
|
|
|
# Add base layers
|
|
manager.add_layer(CandlestickLayer())
|
|
manager.add_layer(VolumeLayer())
|
|
|
|
# Add indicator layers
|
|
manager.add_layer(SMALayer(IndicatorLayerConfig(
|
|
name="SMA(20)",
|
|
indicator_type='sma',
|
|
parameters={'period': 20}
|
|
)))
|
|
manager.add_layer(EMALayer(IndicatorLayerConfig(
|
|
name="EMA(12)",
|
|
indicator_type='ema',
|
|
parameters={'period': 12}
|
|
)))
|
|
|
|
# Add subplot layers
|
|
manager.add_layer(RSILayer(period=14))
|
|
manager.add_layer(MACDLayer(fast_period=12, slow_period=26, signal_period=9))
|
|
|
|
# Calculate layout
|
|
layout_config = manager.calculate_subplot_layout()
|
|
assert layout_config['rows'] >= 4 # Main + volume + RSI + MACD
|
|
|
|
# Render all layers
|
|
fig = manager.render_all_layers(sample_df)
|
|
assert fig is not None
|
|
assert len(fig.data) >= 6 # Candlestick + volume + SMA + EMA + RSI + MACD components
|
|
|
|
def test_error_recovery_integration(self):
|
|
"""Test error recovery with insufficient data"""
|
|
manager = LayerManager()
|
|
|
|
# Create insufficient data
|
|
base_time = datetime.now(timezone.utc)
|
|
insufficient_data = pd.DataFrame([
|
|
{
|
|
'timestamp': base_time + timedelta(minutes=i),
|
|
'open': 50000,
|
|
'high': 50100,
|
|
'low': 49900,
|
|
'close': 50050,
|
|
'volume': 1000
|
|
}
|
|
for i in range(15) # Only 15 data points
|
|
])
|
|
|
|
# Add layers that require more data
|
|
manager.add_layer(CandlestickLayer())
|
|
manager.add_layer(SMALayer(IndicatorLayerConfig(
|
|
name="SMA(50)", # Requires too much data
|
|
indicator_type='sma',
|
|
parameters={'period': 50}
|
|
)))
|
|
|
|
# Should still create a chart (graceful degradation)
|
|
fig = manager.render_all_layers(insufficient_data)
|
|
assert fig is not None
|
|
# Should have at least candlestick layer
|
|
assert len(fig.data) >= 1
|
|
|
|
def test_mixed_valid_invalid_data(self):
|
|
"""Test handling mixed valid and invalid data"""
|
|
# Create data with some invalid entries
|
|
base_time = datetime.now(timezone.utc)
|
|
mixed_data = []
|
|
|
|
for i in range(50):
|
|
if i % 10 == 0: # Every 10th entry is invalid
|
|
data_point = {
|
|
'timestamp': base_time + timedelta(minutes=i),
|
|
'open': -100, # Invalid negative price
|
|
'high': None, # Missing data
|
|
'low': None,
|
|
'close': None,
|
|
'volume': -50 # Invalid negative volume
|
|
}
|
|
else:
|
|
data_point = {
|
|
'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
|
|
}
|
|
mixed_data.append(data_point)
|
|
|
|
df = pd.DataFrame(mixed_data)
|
|
|
|
# Test candlestick layer with mixed data
|
|
candlestick_layer = CandlestickLayer()
|
|
is_valid = candlestick_layer.validate_data(df)
|
|
|
|
# Should handle mixed data gracefully
|
|
if not is_valid:
|
|
# Should have warnings but possibly still proceed
|
|
assert len(candlestick_layer.error_handler.warnings) > 0
|
|
|
|
def test_layer_manager_dynamic_layout(self):
|
|
"""Test LayerManager dynamic layout calculation"""
|
|
manager = LayerManager()
|
|
|
|
# Test with no subplots
|
|
manager.add_layer(CandlestickLayer())
|
|
layout = manager.calculate_subplot_layout()
|
|
assert layout['rows'] == 1
|
|
|
|
# Add one subplot
|
|
manager.add_layer(VolumeLayer())
|
|
layout = manager.calculate_subplot_layout()
|
|
assert layout['rows'] == 2
|
|
|
|
# Add more subplots
|
|
manager.add_layer(RSILayer(period=14))
|
|
manager.add_layer(MACDLayer(fast_period=12, slow_period=26, signal_period=9))
|
|
layout = manager.calculate_subplot_layout()
|
|
assert layout['rows'] == 4 # Main + volume + RSI + MACD
|
|
assert layout['cols'] == 1
|
|
assert len(layout['subplot_titles']) == 4
|
|
assert len(layout['row_heights']) == 4
|
|
|
|
# Test row height calculation
|
|
total_height = sum(layout['row_heights'])
|
|
assert abs(total_height - 1.0) < 0.01 # Should sum to approximately 1.0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"]) |