TCPDashboard/tests/strategies/test_strategy_manager.py

469 lines
18 KiB
Python
Raw Normal View History

"""
Tests for the StrategyManager class.
"""
import pytest
import json
import tempfile
import uuid
from pathlib import Path
from unittest.mock import patch, mock_open, MagicMock
import builtins
from strategies.manager import (
StrategyManager,
StrategyConfig,
StrategyType,
StrategyCategory,
get_strategy_manager
)
@pytest.fixture
def temp_strategy_manager():
"""Create a StrategyManager instance with temporary directories."""
with tempfile.TemporaryDirectory() as temp_dir:
with patch('strategies.manager.STRATEGIES_DIR', Path(temp_dir)):
with patch('strategies.manager.USER_STRATEGIES_DIR', Path(temp_dir) / 'user_strategies'):
with patch('strategies.manager.TEMPLATES_DIR', Path(temp_dir) / 'templates'):
manager = StrategyManager()
yield manager
@pytest.fixture
def sample_strategy_config():
"""Create a sample strategy configuration for testing."""
return StrategyConfig(
id=str(uuid.uuid4()),
name="Test EMA Strategy",
description="A test EMA crossover strategy",
strategy_type=StrategyType.EMA_CROSSOVER.value,
category=StrategyCategory.TREND_FOLLOWING.value,
parameters={"fast_period": 12, "slow_period": 26},
timeframes=["1h", "4h", "1d"],
enabled=True
)
class TestStrategyConfig:
"""Tests for the StrategyConfig dataclass."""
def test_strategy_config_creation(self):
"""Test StrategyConfig creation and initialization."""
config = StrategyConfig(
id="test-id",
name="Test Strategy",
description="Test description",
strategy_type="ema_crossover",
category="trend_following",
parameters={"param1": "value1"},
timeframes=["1h", "4h"]
)
assert config.id == "test-id"
assert config.name == "Test Strategy"
assert config.enabled is True # Default value
assert config.created_date != "" # Should be set automatically
assert config.modified_date != "" # Should be set automatically
def test_strategy_config_to_dict(self, sample_strategy_config):
"""Test StrategyConfig serialization to dictionary."""
config_dict = sample_strategy_config.to_dict()
assert config_dict['name'] == "Test EMA Strategy"
assert config_dict['strategy_type'] == StrategyType.EMA_CROSSOVER.value
assert config_dict['parameters'] == {"fast_period": 12, "slow_period": 26}
assert 'created_date' in config_dict
assert 'modified_date' in config_dict
def test_strategy_config_from_dict(self):
"""Test StrategyConfig creation from dictionary."""
data = {
'id': 'test-id',
'name': 'Test Strategy',
'description': 'Test description',
'strategy_type': 'ema_crossover',
'category': 'trend_following',
'parameters': {'fast_period': 12},
'timeframes': ['1h'],
'enabled': True,
'created_date': '2023-01-01T00:00:00Z',
'modified_date': '2023-01-01T00:00:00Z'
}
config = StrategyConfig.from_dict(data)
assert config.id == 'test-id'
assert config.name == 'Test Strategy'
assert config.strategy_type == 'ema_crossover'
assert config.parameters == {'fast_period': 12}
class TestStrategyManager:
"""Tests for the StrategyManager class."""
def test_init(self, temp_strategy_manager):
"""Test StrategyManager initialization."""
manager = temp_strategy_manager
assert manager.logger is not None
# Directories should be created during initialization
assert hasattr(manager, '_ensure_directories')
def test_save_strategy_success(self, temp_strategy_manager, sample_strategy_config):
"""Test successful strategy saving."""
manager = temp_strategy_manager
result = manager.save_strategy(sample_strategy_config)
assert result is True
# Check that file was created
file_path = manager._get_strategy_file_path(sample_strategy_config.id)
assert file_path.exists()
# Check file content
with open(file_path, 'r') as f:
saved_data = json.load(f)
assert saved_data['name'] == sample_strategy_config.name
assert saved_data['strategy_type'] == sample_strategy_config.strategy_type
def test_save_strategy_error(self, temp_strategy_manager, sample_strategy_config):
"""Test strategy saving with file error."""
manager = temp_strategy_manager
# Mock file operation to raise an error
with patch('builtins.open', mock_open()) as mock_file:
mock_file.side_effect = IOError("Permission denied")
result = manager.save_strategy(sample_strategy_config)
assert result is False
def test_load_strategy_success(self, temp_strategy_manager, sample_strategy_config):
"""Test successful strategy loading."""
manager = temp_strategy_manager
# First save the strategy
manager.save_strategy(sample_strategy_config)
# Then load it
loaded_strategy = manager.load_strategy(sample_strategy_config.id)
assert loaded_strategy is not None
assert loaded_strategy.name == sample_strategy_config.name
assert loaded_strategy.strategy_type == sample_strategy_config.strategy_type
assert loaded_strategy.parameters == sample_strategy_config.parameters
def test_load_strategy_not_found(self, temp_strategy_manager):
"""Test loading non-existent strategy."""
manager = temp_strategy_manager
loaded_strategy = manager.load_strategy("non-existent-id")
assert loaded_strategy is None
def test_load_strategy_invalid_json(self, temp_strategy_manager):
"""Test loading strategy with invalid JSON."""
manager = temp_strategy_manager
# Create file with invalid JSON
file_path = manager._get_strategy_file_path("test-id")
file_path.parent.mkdir(parents=True, exist_ok=True)
file_path.write_text("invalid json")
loaded_strategy = manager.load_strategy("test-id")
assert loaded_strategy is None
def test_list_strategies(self, temp_strategy_manager):
"""Test listing all strategies."""
manager = temp_strategy_manager
# Create and save multiple strategies
strategy1 = StrategyConfig(
id="id1", name="Strategy A", description="", strategy_type="ema_crossover",
category="trend_following", parameters={}, timeframes=[]
)
strategy2 = StrategyConfig(
id="id2", name="Strategy B", description="", strategy_type="rsi",
category="momentum", parameters={}, timeframes=[], enabled=False
)
manager.save_strategy(strategy1)
manager.save_strategy(strategy2)
# List all strategies
all_strategies = manager.list_strategies()
assert len(all_strategies) == 2
# List enabled only
enabled_strategies = manager.list_strategies(enabled_only=True)
assert len(enabled_strategies) == 1
assert enabled_strategies[0].name == "Strategy A"
def test_delete_strategy_success(self, temp_strategy_manager, sample_strategy_config):
"""Test successful strategy deletion."""
manager = temp_strategy_manager
# Save strategy first
manager.save_strategy(sample_strategy_config)
# Verify it exists
file_path = manager._get_strategy_file_path(sample_strategy_config.id)
assert file_path.exists()
# Delete it
result = manager.delete_strategy(sample_strategy_config.id)
assert result is True
assert not file_path.exists()
def test_delete_strategy_not_found(self, temp_strategy_manager):
"""Test deleting non-existent strategy."""
manager = temp_strategy_manager
result = manager.delete_strategy("non-existent-id")
assert result is False
def test_create_strategy_success(self, temp_strategy_manager):
"""Test successful strategy creation."""
manager = temp_strategy_manager
with patch.object(manager, '_validate_parameters', return_value=True):
strategy = manager.create_strategy(
name="New Strategy",
strategy_type=StrategyType.EMA_CROSSOVER.value,
parameters={"fast_period": 12, "slow_period": 26},
description="A new strategy"
)
assert strategy is not None
assert strategy.name == "New Strategy"
assert strategy.strategy_type == StrategyType.EMA_CROSSOVER.value
assert strategy.category == StrategyCategory.TREND_FOLLOWING.value # Default for EMA
assert strategy.timeframes == ["1h", "4h", "1d"] # Default for EMA
def test_create_strategy_invalid_type(self, temp_strategy_manager):
"""Test strategy creation with invalid type."""
manager = temp_strategy_manager
strategy = manager.create_strategy(
name="Invalid Strategy",
strategy_type="invalid_type",
parameters={}
)
assert strategy is None
def test_create_strategy_invalid_parameters(self, temp_strategy_manager):
"""Test strategy creation with invalid parameters."""
manager = temp_strategy_manager
with patch.object(manager, '_validate_parameters', return_value=False):
strategy = manager.create_strategy(
name="Invalid Strategy",
strategy_type=StrategyType.EMA_CROSSOVER.value,
parameters={"invalid": "params"}
)
assert strategy is None
def test_update_strategy_success(self, temp_strategy_manager, sample_strategy_config):
"""Test successful strategy update."""
manager = temp_strategy_manager
# Save original strategy
manager.save_strategy(sample_strategy_config)
# Update it
with patch.object(manager, '_validate_parameters', return_value=True):
result = manager.update_strategy(
sample_strategy_config.id,
name="Updated Strategy Name",
parameters={"fast_period": 15, "slow_period": 30}
)
assert result is True
# Load and verify update
updated_strategy = manager.load_strategy(sample_strategy_config.id)
assert updated_strategy.name == "Updated Strategy Name"
assert updated_strategy.parameters["fast_period"] == 15
def test_update_strategy_not_found(self, temp_strategy_manager):
"""Test updating non-existent strategy."""
manager = temp_strategy_manager
result = manager.update_strategy("non-existent-id", name="New Name")
assert result is False
def test_update_strategy_invalid_parameters(self, temp_strategy_manager, sample_strategy_config):
"""Test updating strategy with invalid parameters."""
manager = temp_strategy_manager
# Save original strategy
manager.save_strategy(sample_strategy_config)
# Try to update with invalid parameters
with patch.object(manager, '_validate_parameters', return_value=False):
result = manager.update_strategy(
sample_strategy_config.id,
parameters={"invalid": "params"}
)
assert result is False
def test_get_strategies_by_category(self, temp_strategy_manager):
"""Test filtering strategies by category."""
manager = temp_strategy_manager
# Create strategies with different categories
strategy1 = StrategyConfig(
id="id1", name="Trend Strategy", description="", strategy_type="ema_crossover",
category="trend_following", parameters={}, timeframes=[]
)
strategy2 = StrategyConfig(
id="id2", name="Momentum Strategy", description="", strategy_type="rsi",
category="momentum", parameters={}, timeframes=[]
)
manager.save_strategy(strategy1)
manager.save_strategy(strategy2)
trend_strategies = manager.get_strategies_by_category("trend_following")
momentum_strategies = manager.get_strategies_by_category("momentum")
assert len(trend_strategies) == 1
assert len(momentum_strategies) == 1
assert trend_strategies[0].name == "Trend Strategy"
assert momentum_strategies[0].name == "Momentum Strategy"
def test_get_available_strategy_types(self, temp_strategy_manager):
"""Test getting available strategy types."""
manager = temp_strategy_manager
types = manager.get_available_strategy_types()
assert StrategyType.EMA_CROSSOVER.value in types
assert StrategyType.RSI.value in types
assert StrategyType.MACD.value in types
def test_get_default_category(self, temp_strategy_manager):
"""Test getting default category for strategy types."""
manager = temp_strategy_manager
assert manager._get_default_category(StrategyType.EMA_CROSSOVER.value) == StrategyCategory.TREND_FOLLOWING.value
assert manager._get_default_category(StrategyType.RSI.value) == StrategyCategory.MOMENTUM.value
assert manager._get_default_category(StrategyType.MACD.value) == StrategyCategory.TREND_FOLLOWING.value
def test_get_default_timeframes(self, temp_strategy_manager):
"""Test getting default timeframes for strategy types."""
manager = temp_strategy_manager
ema_timeframes = manager._get_default_timeframes(StrategyType.EMA_CROSSOVER.value)
rsi_timeframes = manager._get_default_timeframes(StrategyType.RSI.value)
assert "1h" in ema_timeframes
assert "4h" in ema_timeframes
assert "1d" in ema_timeframes
assert "15m" in rsi_timeframes
assert "1h" in rsi_timeframes
def test_validate_parameters_success(self, temp_strategy_manager):
"""Test parameter validation success case."""
manager = temp_strategy_manager
with patch('config.strategies.config_utils.validate_strategy_parameters') as mock_validate:
mock_validate.return_value = (True, [])
result = manager._validate_parameters("ema_crossover", {"fast_period": 12})
assert result is True
def test_validate_parameters_failure(self, temp_strategy_manager):
"""Test parameter validation failure case."""
manager = temp_strategy_manager
with patch('config.strategies.config_utils.validate_strategy_parameters') as mock_validate:
mock_validate.return_value = (False, ["Invalid parameter"])
result = manager._validate_parameters("ema_crossover", {"invalid": "param"})
assert result is False
def test_validate_parameters_import_error(self, temp_strategy_manager):
"""Test parameter validation with import error."""
manager = temp_strategy_manager
with patch('builtins.__import__') as mock_import, \
patch.object(manager, 'logger', new_callable=MagicMock) as mock_manager_logger:
original_import = builtins.__import__
def custom_import(name, globals=None, locals=None, fromlist=(), level=0):
if name == 'config.strategies.config_utils' or 'config.strategies.config_utils' in fromlist:
raise ImportError("Simulated import error for config.strategies.config_utils")
return original_import(name, globals, locals, fromlist, level)
mock_import.side_effect = custom_import
result = manager._validate_parameters("ema_crossover", {"fast_period": 12})
assert result is True
mock_manager_logger.warning.assert_called_with(
"Strategy manager: Could not import validation function, skipping parameter validation"
)
def test_get_template_success(self, temp_strategy_manager):
"""Test successful template loading."""
manager = temp_strategy_manager
# Create a template file
template_data = {
"type": "ema_crossover",
"name": "EMA Crossover",
"parameter_schema": {"fast_period": {"type": "int"}}
}
template_file = manager._get_template_file_path("ema_crossover")
template_file.parent.mkdir(parents=True, exist_ok=True)
with open(template_file, 'w') as f:
json.dump(template_data, f)
template = manager.get_template("ema_crossover")
assert template is not None
assert template["name"] == "EMA Crossover"
def test_get_template_not_found(self, temp_strategy_manager):
"""Test template loading when template doesn't exist."""
manager = temp_strategy_manager
template = manager.get_template("non_existent_template")
assert template is None
class TestGetStrategyManager:
"""Tests for the global strategy manager function."""
def test_singleton_behavior(self):
"""Test that get_strategy_manager returns the same instance."""
manager1 = get_strategy_manager()
manager2 = get_strategy_manager()
assert manager1 is manager2
@patch('strategies.manager._strategy_manager', None)
def test_creates_new_instance_when_none(self):
"""Test that get_strategy_manager creates new instance when none exists."""
manager = get_strategy_manager()
assert isinstance(manager, StrategyManager)