""" 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)