469 lines
18 KiB
Python
469 lines
18 KiB
Python
|
|
"""
|
||
|
|
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)
|