TCPDashboard/tests/config/strategies/test_config_utils.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

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