TCPDashboard/tests/strategies/test_strategy_manager.py
Vasily.onl d34da789ec 4.0 - 2.0 Implement strategy configuration utilities and templates
- Introduced `config_utils.py` for loading and managing strategy configurations, including functions for loading templates, generating dropdown options, and retrieving parameter schemas and default values.
- Added JSON templates for EMA Crossover, MACD, and RSI strategies, defining their parameters and validation rules to enhance modularity and maintainability.
- Implemented `StrategyManager` in `manager.py` for managing user-defined strategies with file-based storage, supporting easy sharing and portability.
- Updated `__init__.py` to include new components and ensure proper module exports.
- Enhanced error handling and logging practices across the new modules for improved reliability.

These changes establish a robust foundation for strategy management and configuration, aligning with project goals for modularity, performance, and maintainability.
2025-06-12 15:17:35 +08:00

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)