- 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.
383 lines
15 KiB
Python
383 lines
15 KiB
Python
"""
|
|
Tests for strategy configuration utilities.
|
|
"""
|
|
|
|
import pytest
|
|
import json
|
|
import tempfile
|
|
import os
|
|
from pathlib import Path
|
|
from unittest.mock import patch, mock_open
|
|
|
|
from config.strategies.config_utils import (
|
|
load_strategy_templates,
|
|
get_strategy_dropdown_options,
|
|
get_strategy_parameter_schema,
|
|
get_strategy_default_parameters,
|
|
get_strategy_metadata,
|
|
get_strategy_required_indicators,
|
|
generate_parameter_fields_config,
|
|
validate_strategy_parameters,
|
|
save_user_strategy,
|
|
load_user_strategies,
|
|
delete_user_strategy,
|
|
export_strategy_config,
|
|
import_strategy_config
|
|
)
|
|
|
|
|
|
class TestLoadStrategyTemplates:
|
|
"""Tests for template loading functionality."""
|
|
|
|
@patch('os.path.exists')
|
|
@patch('os.listdir')
|
|
@patch('builtins.open', new_callable=mock_open)
|
|
def test_load_templates_success(self, mock_file, mock_listdir, mock_exists):
|
|
"""Test successful template loading."""
|
|
mock_exists.return_value = True
|
|
mock_listdir.return_value = ['ema_crossover_template.json', 'rsi_template.json']
|
|
|
|
# Mock template content
|
|
template_data = {
|
|
'type': 'ema_crossover',
|
|
'name': 'EMA Crossover',
|
|
'parameter_schema': {'fast_period': {'type': 'int', 'default': 12}}
|
|
}
|
|
mock_file.return_value.read.return_value = json.dumps(template_data)
|
|
|
|
templates = load_strategy_templates()
|
|
|
|
assert 'ema_crossover' in templates
|
|
assert templates['ema_crossover']['name'] == 'EMA Crossover'
|
|
|
|
@patch('os.path.exists')
|
|
def test_load_templates_no_directory(self, mock_exists):
|
|
"""Test loading when template directory doesn't exist."""
|
|
mock_exists.return_value = False
|
|
|
|
templates = load_strategy_templates()
|
|
|
|
assert templates == {}
|
|
|
|
@patch('os.path.exists')
|
|
@patch('os.listdir')
|
|
@patch('builtins.open', new_callable=mock_open)
|
|
def test_load_templates_invalid_json(self, mock_file, mock_listdir, mock_exists):
|
|
"""Test loading with invalid JSON."""
|
|
mock_exists.return_value = True
|
|
mock_listdir.return_value = ['invalid_template.json']
|
|
mock_file.return_value.read.return_value = 'invalid json'
|
|
|
|
templates = load_strategy_templates()
|
|
|
|
assert templates == {}
|
|
|
|
|
|
class TestGetStrategyDropdownOptions:
|
|
"""Tests for dropdown options generation."""
|
|
|
|
@patch('config.strategies.config_utils.load_strategy_templates')
|
|
def test_dropdown_options_success(self, mock_load_templates):
|
|
"""Test successful dropdown options generation."""
|
|
mock_load_templates.return_value = {
|
|
'ema_crossover': {'name': 'EMA Crossover'},
|
|
'rsi': {'name': 'RSI Strategy'}
|
|
}
|
|
|
|
options = get_strategy_dropdown_options()
|
|
|
|
assert len(options) == 2
|
|
assert {'label': 'EMA Crossover', 'value': 'ema_crossover'} in options
|
|
assert {'label': 'RSI Strategy', 'value': 'rsi'} in options
|
|
|
|
@patch('config.strategies.config_utils.load_strategy_templates')
|
|
def test_dropdown_options_empty(self, mock_load_templates):
|
|
"""Test dropdown options with no templates."""
|
|
mock_load_templates.return_value = {}
|
|
|
|
options = get_strategy_dropdown_options()
|
|
|
|
assert options == []
|
|
|
|
|
|
class TestParameterValidation:
|
|
"""Tests for parameter validation functionality."""
|
|
|
|
def test_validate_ema_crossover_parameters_valid(self):
|
|
"""Test validation of valid EMA crossover parameters."""
|
|
# Create a mock template for testing
|
|
with patch('config.strategies.config_utils.get_strategy_parameter_schema') as mock_schema:
|
|
mock_schema.return_value = {
|
|
'fast_period': {'type': 'int', 'min': 5, 'max': 50, 'required': True},
|
|
'slow_period': {'type': 'int', 'min': 10, 'max': 200, 'required': True}
|
|
}
|
|
|
|
parameters = {'fast_period': 12, 'slow_period': 26}
|
|
is_valid, errors = validate_strategy_parameters('ema_crossover', parameters)
|
|
|
|
assert is_valid
|
|
assert errors == []
|
|
|
|
def test_validate_ema_crossover_parameters_invalid(self):
|
|
"""Test validation of invalid EMA crossover parameters."""
|
|
with patch('config.strategies.config_utils.get_strategy_parameter_schema') as mock_schema:
|
|
mock_schema.return_value = {
|
|
'fast_period': {'type': 'int', 'min': 5, 'max': 50, 'required': True},
|
|
'slow_period': {'type': 'int', 'min': 10, 'max': 200, 'required': True}
|
|
}
|
|
|
|
parameters = {'fast_period': 100} # Missing slow_period, fast_period out of range
|
|
is_valid, errors = validate_strategy_parameters('ema_crossover', parameters)
|
|
|
|
assert not is_valid
|
|
assert len(errors) >= 2 # Should have errors for both issues
|
|
|
|
def test_validate_rsi_parameters_valid(self):
|
|
"""Test validation of valid RSI parameters."""
|
|
with patch('config.strategies.config_utils.get_strategy_parameter_schema') as mock_schema:
|
|
mock_schema.return_value = {
|
|
'period': {'type': 'int', 'min': 2, 'max': 50, 'required': True},
|
|
'overbought': {'type': 'float', 'min': 50.0, 'max': 95.0, 'required': True},
|
|
'oversold': {'type': 'float', 'min': 5.0, 'max': 50.0, 'required': True}
|
|
}
|
|
|
|
parameters = {'period': 14, 'overbought': 70.0, 'oversold': 30.0}
|
|
is_valid, errors = validate_strategy_parameters('rsi', parameters)
|
|
|
|
assert is_valid
|
|
assert errors == []
|
|
|
|
def test_validate_parameters_no_schema(self):
|
|
"""Test validation when no schema is found."""
|
|
with patch('config.strategies.config_utils.get_strategy_parameter_schema') as mock_schema:
|
|
mock_schema.return_value = None
|
|
|
|
parameters = {'any_param': 'any_value'}
|
|
is_valid, errors = validate_strategy_parameters('unknown_strategy', parameters)
|
|
|
|
assert not is_valid
|
|
assert 'No schema found' in str(errors)
|
|
|
|
|
|
class TestUserStrategyManagement:
|
|
"""Tests for user strategy file management."""
|
|
|
|
def test_save_user_strategy_success(self):
|
|
"""Test successful saving of user strategy."""
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
with patch('config.strategies.config_utils.os.path.dirname') as mock_dirname:
|
|
mock_dirname.return_value = temp_dir
|
|
|
|
config = {
|
|
'name': 'My EMA Strategy',
|
|
'strategy': 'ema_crossover',
|
|
'fast_period': 12,
|
|
'slow_period': 26
|
|
}
|
|
|
|
result = save_user_strategy('My EMA Strategy', config)
|
|
|
|
assert result
|
|
# Check file was created
|
|
expected_file = Path(temp_dir) / 'user_strategies' / 'my_ema_strategy.json'
|
|
assert expected_file.exists()
|
|
|
|
def test_save_user_strategy_error(self):
|
|
"""Test error handling during strategy saving."""
|
|
with patch('builtins.open', mock_open()) as mock_file:
|
|
mock_file.side_effect = IOError("Permission denied")
|
|
|
|
config = {'name': 'Test Strategy'}
|
|
result = save_user_strategy('Test Strategy', config)
|
|
|
|
assert not result
|
|
|
|
def test_load_user_strategies_success(self):
|
|
"""Test successful loading of user strategies."""
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
# Create test strategy file
|
|
user_strategies_dir = Path(temp_dir) / 'user_strategies'
|
|
user_strategies_dir.mkdir()
|
|
|
|
strategy_file = user_strategies_dir / 'test_strategy.json'
|
|
strategy_data = {
|
|
'name': 'Test Strategy',
|
|
'strategy': 'ema_crossover',
|
|
'parameters': {'fast_period': 12}
|
|
}
|
|
|
|
with open(strategy_file, 'w') as f:
|
|
json.dump(strategy_data, f)
|
|
|
|
with patch('config.strategies.config_utils.os.path.dirname') as mock_dirname:
|
|
mock_dirname.return_value = temp_dir
|
|
|
|
strategies = load_user_strategies()
|
|
|
|
assert 'Test Strategy' in strategies
|
|
assert strategies['Test Strategy']['strategy'] == 'ema_crossover'
|
|
|
|
def test_load_user_strategies_no_directory(self):
|
|
"""Test loading when user strategies directory doesn't exist."""
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
with patch('config.strategies.config_utils.os.path.dirname') as mock_dirname:
|
|
mock_dirname.return_value = temp_dir
|
|
|
|
strategies = load_user_strategies()
|
|
|
|
assert strategies == {}
|
|
|
|
def test_delete_user_strategy_success(self):
|
|
"""Test successful deletion of user strategy."""
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
# Create test strategy file
|
|
user_strategies_dir = Path(temp_dir) / 'user_strategies'
|
|
user_strategies_dir.mkdir()
|
|
|
|
strategy_file = user_strategies_dir / 'test_strategy.json'
|
|
strategy_file.write_text('{}')
|
|
|
|
with patch('config.strategies.config_utils.os.path.dirname') as mock_dirname:
|
|
mock_dirname.return_value = temp_dir
|
|
|
|
result = delete_user_strategy('Test Strategy')
|
|
|
|
assert result
|
|
assert not strategy_file.exists()
|
|
|
|
def test_delete_user_strategy_not_found(self):
|
|
"""Test deletion of non-existent strategy."""
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
with patch('config.strategies.config_utils.os.path.dirname') as mock_dirname:
|
|
mock_dirname.return_value = temp_dir
|
|
|
|
result = delete_user_strategy('Non Existent Strategy')
|
|
|
|
assert not result
|
|
|
|
|
|
class TestStrategyConfigImportExport:
|
|
"""Tests for strategy configuration import/export functionality."""
|
|
|
|
def test_export_strategy_config(self):
|
|
"""Test exporting strategy configuration."""
|
|
config = {
|
|
'strategy': 'ema_crossover',
|
|
'fast_period': 12,
|
|
'slow_period': 26
|
|
}
|
|
|
|
result = export_strategy_config('My Strategy', config)
|
|
|
|
# Parse the exported JSON
|
|
exported_data = json.loads(result)
|
|
|
|
assert exported_data['name'] == 'My Strategy'
|
|
assert exported_data['config'] == config
|
|
assert 'exported_at' in exported_data
|
|
assert 'version' in exported_data
|
|
|
|
def test_import_strategy_config_success(self):
|
|
"""Test successful import of strategy configuration."""
|
|
import_data = {
|
|
'name': 'Imported Strategy',
|
|
'config': {
|
|
'strategy': 'ema_crossover',
|
|
'fast_period': 12,
|
|
'slow_period': 26
|
|
},
|
|
'version': '1.0'
|
|
}
|
|
|
|
json_string = json.dumps(import_data)
|
|
|
|
with patch('config.strategies.config_utils.validate_strategy_parameters') as mock_validate:
|
|
mock_validate.return_value = (True, [])
|
|
|
|
success, data, errors = import_strategy_config(json_string)
|
|
|
|
assert success
|
|
assert data['name'] == 'Imported Strategy'
|
|
assert errors == []
|
|
|
|
def test_import_strategy_config_invalid_json(self):
|
|
"""Test import with invalid JSON."""
|
|
json_string = 'invalid json'
|
|
|
|
success, data, errors = import_strategy_config(json_string)
|
|
|
|
assert not success
|
|
assert data is None
|
|
assert len(errors) > 0
|
|
assert 'Invalid JSON format' in str(errors)
|
|
|
|
def test_import_strategy_config_missing_fields(self):
|
|
"""Test import with missing required fields."""
|
|
import_data = {'name': 'Test Strategy'} # Missing 'config'
|
|
json_string = json.dumps(import_data)
|
|
|
|
success, data, errors = import_strategy_config(json_string)
|
|
|
|
assert not success
|
|
assert data is None
|
|
assert 'missing name or config fields' in str(errors)
|
|
|
|
def test_import_strategy_config_invalid_parameters(self):
|
|
"""Test import with invalid strategy parameters."""
|
|
import_data = {
|
|
'name': 'Invalid Strategy',
|
|
'config': {
|
|
'strategy': 'ema_crossover',
|
|
'fast_period': 'invalid' # Should be int
|
|
}
|
|
}
|
|
|
|
json_string = json.dumps(import_data)
|
|
|
|
with patch('config.strategies.config_utils.validate_strategy_parameters') as mock_validate:
|
|
mock_validate.return_value = (False, ['Invalid parameter type'])
|
|
|
|
success, data, errors = import_strategy_config(json_string)
|
|
|
|
assert not success
|
|
assert data is None
|
|
assert 'Invalid parameter type' in str(errors)
|
|
|
|
|
|
class TestParameterFieldsConfig:
|
|
"""Tests for parameter fields configuration generation."""
|
|
|
|
def test_generate_parameter_fields_config_success(self):
|
|
"""Test successful generation of parameter fields configuration."""
|
|
with patch('config.strategies.config_utils.get_strategy_parameter_schema') as mock_schema, \
|
|
patch('config.strategies.config_utils.get_strategy_default_parameters') as mock_defaults:
|
|
|
|
mock_schema.return_value = {
|
|
'fast_period': {
|
|
'type': 'int',
|
|
'description': 'Fast EMA period',
|
|
'min': 5,
|
|
'max': 50,
|
|
'default': 12
|
|
}
|
|
}
|
|
mock_defaults.return_value = {'fast_period': 12}
|
|
|
|
config = generate_parameter_fields_config('ema_crossover')
|
|
|
|
assert 'fast_period' in config
|
|
field_config = config['fast_period']
|
|
assert field_config['type'] == 'int'
|
|
assert field_config['label'] == 'Fast Period'
|
|
assert field_config['default'] == 12
|
|
assert field_config['min'] == 5
|
|
assert field_config['max'] == 50
|
|
|
|
def test_generate_parameter_fields_config_no_schema(self):
|
|
"""Test parameter fields config when no schema exists."""
|
|
with patch('config.strategies.config_utils.get_strategy_parameter_schema') as mock_schema:
|
|
mock_schema.return_value = None
|
|
|
|
config = generate_parameter_fields_config('unknown_strategy')
|
|
|
|
assert config is None |