737 lines
25 KiB
Python
737 lines
25 KiB
Python
"""Unit tests for strategy optimizer module."""
|
|
|
|
import time
|
|
from datetime import datetime, timedelta
|
|
from unittest.mock import Mock
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
from openclaw.backtest.engine import BacktestResult
|
|
from openclaw.optimizer import (
|
|
BayesianOptimizer,
|
|
GridSearchOptimizer,
|
|
OptimizationAnalyzer,
|
|
OptimizationResult,
|
|
OptimizerConfig,
|
|
ParameterSpace,
|
|
RandomSearchOptimizer,
|
|
StrategyOptimizer,
|
|
)
|
|
from openclaw.optimizer.base import OptimizationObjective
|
|
|
|
|
|
class TestParameterSpace:
|
|
"""Test ParameterSpace class."""
|
|
|
|
def test_add_continuous_parameter(self):
|
|
"""Test adding continuous parameter."""
|
|
space = ParameterSpace()
|
|
space.add_continuous("learning_rate", 0.001, 0.1, distribution="log_uniform")
|
|
|
|
assert "learning_rate" in space
|
|
param = space.get_parameter("learning_rate")
|
|
assert param.param_type.value == "continuous"
|
|
assert param.bounds == (0.001, 0.1)
|
|
assert param.distribution == "log_uniform"
|
|
|
|
def test_add_integer_parameter(self):
|
|
"""Test adding integer parameter."""
|
|
space = ParameterSpace()
|
|
space.add_integer("window_size", 5, 50)
|
|
|
|
assert "window_size" in space
|
|
param = space.get_parameter("window_size")
|
|
assert param.param_type.value == "integer"
|
|
assert param.bounds == (5, 50)
|
|
|
|
def test_add_discrete_parameter(self):
|
|
"""Test adding discrete parameter."""
|
|
space = ParameterSpace()
|
|
space.add_discrete("threshold", [0.1, 0.2, 0.3, 0.4, 0.5])
|
|
|
|
assert "threshold" in space
|
|
param = space.get_parameter("threshold")
|
|
assert param.param_type.value == "discrete"
|
|
assert param.bounds == [0.1, 0.2, 0.3, 0.4, 0.5]
|
|
|
|
def test_add_categorical_parameter(self):
|
|
"""Test adding categorical parameter."""
|
|
space = ParameterSpace()
|
|
space.add_categorical("strategy_type", ["momentum", "mean_reversion", "trend_following"])
|
|
|
|
assert "strategy_type" in space
|
|
param = space.get_parameter("strategy_type")
|
|
assert param.param_type.value == "categorical"
|
|
assert "momentum" in param.bounds
|
|
|
|
def test_method_chaining(self):
|
|
"""Test that methods can be chained."""
|
|
space = (
|
|
ParameterSpace()
|
|
.add_continuous("param1", 0.0, 1.0)
|
|
.add_integer("param2", 1, 10)
|
|
.add_categorical("param3", ["a", "b"])
|
|
)
|
|
|
|
assert len(space) == 3
|
|
assert "param1" in space
|
|
assert "param2" in space
|
|
assert "param3" in space
|
|
|
|
def test_invalid_bounds(self):
|
|
"""Test that invalid bounds raise errors."""
|
|
space = ParameterSpace()
|
|
|
|
with pytest.raises(ValueError, match="Invalid bounds"):
|
|
space.add_continuous("invalid", 1.0, 0.5)
|
|
|
|
def test_sample_random(self):
|
|
"""Test random sampling from parameter space."""
|
|
space = (
|
|
ParameterSpace()
|
|
.add_continuous("continuous", 0.0, 1.0)
|
|
.add_integer("integer", 1, 10)
|
|
.add_categorical("categorical", ["a", "b", "c"])
|
|
)
|
|
|
|
params = space.sample_random()
|
|
|
|
assert "continuous" in params
|
|
assert 0.0 <= params["continuous"] <= 1.0
|
|
assert "integer" in params
|
|
assert 1 <= params["integer"] <= 10
|
|
assert "categorical" in params
|
|
assert params["categorical"] in ["a", "b", "c"]
|
|
|
|
def test_get_grid_points(self):
|
|
"""Test grid point generation."""
|
|
space = (
|
|
ParameterSpace()
|
|
.add_continuous("param1", 0.0, 1.0)
|
|
.add_integer("param2", 1, 3)
|
|
)
|
|
|
|
grid = space.get_grid_points(n_points=3)
|
|
|
|
# Should have 3 (continuous) * 3 (integer: 1,2,3) = 9 points
|
|
assert len(grid) == 9
|
|
|
|
# Check first point
|
|
assert "param1" in grid[0]
|
|
assert "param2" in grid[0]
|
|
|
|
def test_get_nonexistent_parameter(self):
|
|
"""Test getting a parameter that doesn't exist."""
|
|
space = ParameterSpace()
|
|
|
|
with pytest.raises(KeyError):
|
|
space.get_parameter("nonexistent")
|
|
|
|
|
|
class TestOptimizerConfig:
|
|
"""Test OptimizerConfig class."""
|
|
|
|
def test_default_config(self):
|
|
"""Test default configuration values."""
|
|
config = OptimizerConfig()
|
|
|
|
assert config.objective == OptimizationObjective.MAXIMIZE_SHARPE
|
|
assert config.max_iterations == 100
|
|
assert config.n_jobs == -1
|
|
assert config.early_stopping is True
|
|
assert config.early_stopping_patience == 10
|
|
|
|
def test_custom_config(self):
|
|
"""Test custom configuration."""
|
|
config = OptimizerConfig(
|
|
objective=OptimizationObjective.MAXIMIZE_RETURN,
|
|
max_iterations=50,
|
|
n_jobs=4,
|
|
early_stopping=False,
|
|
random_state=42,
|
|
)
|
|
|
|
assert config.objective == OptimizationObjective.MAXIMIZE_RETURN
|
|
assert config.max_iterations == 50
|
|
assert config.n_jobs == 4
|
|
assert config.early_stopping is False
|
|
assert config.random_state == 42
|
|
|
|
def test_invalid_max_iterations(self):
|
|
"""Test that invalid max_iterations raises error."""
|
|
with pytest.raises(ValueError, match="max_iterations"):
|
|
OptimizerConfig(max_iterations=0)
|
|
|
|
def test_invalid_validation_split(self):
|
|
"""Test that invalid validation_split raises error."""
|
|
with pytest.raises(ValueError, match="validation_split"):
|
|
OptimizerConfig(validation_split=1.5)
|
|
|
|
|
|
class TestGridSearchOptimizer:
|
|
"""Test GridSearchOptimizer class."""
|
|
|
|
@pytest.fixture
|
|
def simple_space(self):
|
|
"""Create a simple parameter space."""
|
|
return (
|
|
ParameterSpace()
|
|
.add_discrete("threshold", [0.1, 0.2, 0.3])
|
|
.add_integer("window", 5, 6)
|
|
)
|
|
|
|
@pytest.fixture
|
|
def mock_backtest_fn(self):
|
|
"""Create a mock backtest function."""
|
|
def backtest_fn(params):
|
|
# Simple scoring: higher threshold + window = better
|
|
score = params.get("threshold", 0) * 100 + params.get("window", 0)
|
|
return BacktestResult(
|
|
start_date=datetime.now(),
|
|
end_date=datetime.now(),
|
|
initial_capital=10000.0,
|
|
final_equity=10000.0 + score * 100,
|
|
total_return=score,
|
|
total_trades=10,
|
|
winning_trades=5,
|
|
losing_trades=5,
|
|
win_rate=50.0,
|
|
avg_win=100.0,
|
|
avg_loss=-50.0,
|
|
profit_factor=1.5,
|
|
sharpe_ratio=score / 10,
|
|
max_drawdown=10.0,
|
|
max_drawdown_duration=5,
|
|
volatility=15.0,
|
|
calmar_ratio=score / 10,
|
|
)
|
|
return backtest_fn
|
|
|
|
def test_initialization(self, simple_space):
|
|
"""Test optimizer initialization."""
|
|
optimizer = GridSearchOptimizer(simple_space, n_points=3)
|
|
|
|
assert optimizer.parameter_space == simple_space
|
|
assert optimizer.n_points == 3
|
|
|
|
def test_get_grid_size(self, simple_space):
|
|
"""Test getting grid size."""
|
|
optimizer = GridSearchOptimizer(simple_space, n_points=3)
|
|
|
|
# 3 thresholds * 2 windows (5, 6) = 6
|
|
assert optimizer.get_grid_size() == 6
|
|
|
|
def test_optimize(self, simple_space, mock_backtest_fn):
|
|
"""Test optimization."""
|
|
config = OptimizerConfig(max_iterations=100, n_jobs=1)
|
|
optimizer = GridSearchOptimizer(simple_space, config=config, n_points=3)
|
|
|
|
result = optimizer.optimize(mock_backtest_fn)
|
|
|
|
assert isinstance(result, OptimizationResult)
|
|
assert result.best_params is not None
|
|
assert result.best_score > float("-inf")
|
|
assert len(result.all_results) == 6 # All combinations
|
|
assert result.converged is True
|
|
|
|
def test_optimize_with_max_iterations(self, simple_space, mock_backtest_fn):
|
|
"""Test optimization respects max_iterations."""
|
|
config = OptimizerConfig(max_iterations=4, n_jobs=1)
|
|
optimizer = GridSearchOptimizer(simple_space, config=config, n_points=5)
|
|
|
|
result = optimizer.optimize(mock_backtest_fn)
|
|
|
|
# Should be limited by max_iterations
|
|
assert result.n_iterations <= 4
|
|
|
|
def test_optimize_with_callback(self, simple_space, mock_backtest_fn):
|
|
"""Test optimization with callback."""
|
|
callback_calls = []
|
|
|
|
def callback(iteration, params, score):
|
|
callback_calls.append((iteration, params, score))
|
|
|
|
config = OptimizerConfig(max_iterations=100, n_jobs=1)
|
|
optimizer = GridSearchOptimizer(simple_space, config=config)
|
|
|
|
optimizer.optimize(mock_backtest_fn, callback=callback)
|
|
|
|
assert len(callback_calls) == 6
|
|
|
|
|
|
class TestRandomSearchOptimizer:
|
|
"""Test RandomSearchOptimizer class."""
|
|
|
|
@pytest.fixture
|
|
def continuous_space(self):
|
|
"""Create a parameter space with continuous parameters."""
|
|
return (
|
|
ParameterSpace()
|
|
.add_continuous("alpha", 0.0, 1.0)
|
|
.add_continuous("beta", 0.0, 1.0)
|
|
)
|
|
|
|
@pytest.fixture
|
|
def mock_backtest_fn(self):
|
|
"""Create a mock backtest function."""
|
|
def backtest_fn(params):
|
|
score = params.get("alpha", 0) + params.get("beta", 0)
|
|
return BacktestResult(
|
|
start_date=datetime.now(),
|
|
end_date=datetime.now(),
|
|
initial_capital=10000.0,
|
|
final_equity=10000.0,
|
|
total_return=score * 100,
|
|
total_trades=10,
|
|
winning_trades=5,
|
|
losing_trades=5,
|
|
win_rate=50.0,
|
|
avg_win=100.0,
|
|
avg_loss=-50.0,
|
|
profit_factor=1.5,
|
|
sharpe_ratio=score,
|
|
max_drawdown=10.0,
|
|
max_drawdown_duration=5,
|
|
volatility=15.0,
|
|
calmar_ratio=score,
|
|
)
|
|
return backtest_fn
|
|
|
|
def test_initialization(self, continuous_space):
|
|
"""Test optimizer initialization."""
|
|
optimizer = RandomSearchOptimizer(continuous_space, n_samples=50)
|
|
|
|
assert optimizer.parameter_space == continuous_space
|
|
assert optimizer.n_samples == 50
|
|
|
|
def test_optimize(self, continuous_space, mock_backtest_fn):
|
|
"""Test optimization."""
|
|
config = OptimizerConfig(max_iterations=100, n_jobs=1, early_stopping=False, random_state=42)
|
|
optimizer = RandomSearchOptimizer(continuous_space, config=config, n_samples=20)
|
|
|
|
result = optimizer.optimize(mock_backtest_fn)
|
|
|
|
assert isinstance(result, OptimizationResult)
|
|
assert result.best_params is not None
|
|
assert result.best_score > float("-inf")
|
|
assert result.n_iterations == 20
|
|
|
|
def test_optimize_with_early_stopping(self, continuous_space, mock_backtest_fn):
|
|
"""Test optimization with early stopping."""
|
|
config = OptimizerConfig(
|
|
max_iterations=100,
|
|
n_jobs=1,
|
|
early_stopping=True,
|
|
early_stopping_patience=2,
|
|
early_stopping_min_delta=0.1,
|
|
random_state=42,
|
|
)
|
|
optimizer = RandomSearchOptimizer(continuous_space, config=config, n_samples=50)
|
|
|
|
result = optimizer.optimize(mock_backtest_fn)
|
|
|
|
# Should stop early due to no improvement
|
|
assert result.n_iterations < 50
|
|
|
|
def test_optimize_with_warm_start(self, continuous_space, mock_backtest_fn):
|
|
"""Test optimization with warm start."""
|
|
initial_params = [
|
|
({"alpha": 0.5, "beta": 0.5}, 1.0),
|
|
({"alpha": 0.6, "beta": 0.4}, 1.0),
|
|
]
|
|
|
|
config = OptimizerConfig(max_iterations=100, n_jobs=1, random_state=42)
|
|
optimizer = RandomSearchOptimizer(continuous_space, config=config, n_samples=10)
|
|
|
|
result = optimizer.optimize_with_warm_start(mock_backtest_fn, initial_params)
|
|
|
|
assert len(result.all_results) >= 2 # At least initial params
|
|
assert result.best_score >= 1.0
|
|
|
|
|
|
class TestBayesianOptimizer:
|
|
"""Test BayesianOptimizer class."""
|
|
|
|
@pytest.fixture
|
|
def simple_space(self):
|
|
"""Create a simple parameter space."""
|
|
return (
|
|
ParameterSpace()
|
|
.add_continuous("x", 0.0, 10.0)
|
|
.add_continuous("y", 0.0, 10.0)
|
|
)
|
|
|
|
@pytest.fixture
|
|
def mock_backtest_fn(self):
|
|
"""Create a mock backtest function with known optimum."""
|
|
def backtest_fn(params):
|
|
# Optimum at x=7, y=3
|
|
x = params.get("x", 0)
|
|
y = params.get("y", 0)
|
|
score = -((x - 7) ** 2 + (y - 3) ** 2) / 100 + 5
|
|
return BacktestResult(
|
|
start_date=datetime.now(),
|
|
end_date=datetime.now(),
|
|
initial_capital=10000.0,
|
|
final_equity=10000.0,
|
|
total_return=score * 10,
|
|
total_trades=10,
|
|
winning_trades=5,
|
|
losing_trades=5,
|
|
win_rate=50.0,
|
|
avg_win=100.0,
|
|
avg_loss=-50.0,
|
|
profit_factor=1.5,
|
|
sharpe_ratio=score,
|
|
max_drawdown=10.0,
|
|
max_drawdown_duration=5,
|
|
volatility=15.0,
|
|
calmar_ratio=score,
|
|
)
|
|
return backtest_fn
|
|
|
|
def test_initialization(self, simple_space):
|
|
"""Test optimizer initialization."""
|
|
optimizer = BayesianOptimizer(
|
|
simple_space,
|
|
n_initial_points=5,
|
|
acquisition="ei",
|
|
)
|
|
|
|
assert optimizer.parameter_space == simple_space
|
|
assert optimizer.n_initial_points == 5
|
|
assert optimizer.acquisition == "ei"
|
|
|
|
def test_invalid_acquisition(self, simple_space):
|
|
"""Test that invalid acquisition function raises error."""
|
|
with pytest.raises(ValueError, match="Unknown acquisition"):
|
|
BayesianOptimizer(simple_space, acquisition="invalid")
|
|
|
|
def test_optimize(self, simple_space, mock_backtest_fn):
|
|
"""Test optimization."""
|
|
config = OptimizerConfig(max_iterations=20, n_jobs=1, random_state=42)
|
|
optimizer = BayesianOptimizer(
|
|
simple_space,
|
|
config=config,
|
|
n_initial_points=5,
|
|
acquisition="ei",
|
|
)
|
|
|
|
result = optimizer.optimize(mock_backtest_fn)
|
|
|
|
assert isinstance(result, OptimizationResult)
|
|
assert result.best_params is not None
|
|
assert result.best_score > float("-inf")
|
|
assert result.n_iterations >= 5 # At least initial points
|
|
|
|
# Should find something close to optimum
|
|
best = result.best_params
|
|
assert 5 <= best["x"] <= 9 # Around 7
|
|
assert 1 <= best["y"] <= 5 # Around 3
|
|
|
|
def test_parameter_importance(self, simple_space, mock_backtest_fn):
|
|
"""Test parameter importance calculation."""
|
|
config = OptimizerConfig(max_iterations=15, n_jobs=1, random_state=42)
|
|
optimizer = BayesianOptimizer(
|
|
simple_space,
|
|
config=config,
|
|
n_initial_points=5,
|
|
)
|
|
|
|
result = optimizer.optimize(mock_backtest_fn)
|
|
|
|
assert "x" in result.parameter_importance
|
|
assert "y" in result.parameter_importance
|
|
# Importance scores should sum to 1
|
|
assert abs(sum(result.parameter_importance.values()) - 1.0) < 0.01
|
|
|
|
|
|
class TestOptimizationAnalyzer:
|
|
"""Test OptimizationAnalyzer class."""
|
|
|
|
@pytest.fixture
|
|
def analyzer(self):
|
|
"""Create an analyzer instance."""
|
|
return OptimizationAnalyzer()
|
|
|
|
@pytest.fixture
|
|
def sample_result(self):
|
|
"""Create a sample optimization result."""
|
|
all_results = [
|
|
({"param1": 0.1, "param2": 10}, 1.0, None),
|
|
({"param1": 0.2, "param2": 20}, 2.0, None),
|
|
({"param1": 0.3, "param2": 30}, 3.0, None),
|
|
({"param1": 0.4, "param2": 40}, 4.0, None),
|
|
({"param1": 0.5, "param2": 50}, 5.0, None),
|
|
]
|
|
|
|
return OptimizationResult(
|
|
best_params={"param1": 0.5, "param2": 50},
|
|
best_score=5.0,
|
|
best_result=None,
|
|
all_results=all_results,
|
|
optimization_time=10.0,
|
|
n_iterations=5,
|
|
converged=True,
|
|
)
|
|
|
|
def test_analyze_parameter_sensitivity(self, analyzer, sample_result):
|
|
"""Test parameter sensitivity analysis."""
|
|
sensitivity = analyzer.analyze_parameter_sensitivity(
|
|
sample_result, "param1"
|
|
)
|
|
|
|
assert sensitivity.parameter_name == "param1"
|
|
assert len(sensitivity.values) > 0
|
|
assert len(sensitivity.scores) > 0
|
|
assert sensitivity.sensitivity_score >= 0.0
|
|
|
|
def test_detect_overfitting(self, analyzer):
|
|
"""Test overfitting detection."""
|
|
train_result = OptimizationResult(
|
|
best_params={},
|
|
best_score=10.0,
|
|
best_result=None,
|
|
all_results=[],
|
|
converged=True,
|
|
)
|
|
validation_result = OptimizationResult(
|
|
best_params={},
|
|
best_score=5.0,
|
|
best_result=None,
|
|
all_results=[],
|
|
converged=True,
|
|
)
|
|
|
|
overfitting = analyzer.detect_overfitting(
|
|
train_result, validation_result, threshold=0.3
|
|
)
|
|
|
|
assert overfitting.is_overfitted is True
|
|
assert overfitting.train_score == 10.0
|
|
assert overfitting.validation_score == 5.0
|
|
assert overfitting.severity in ["low", "medium", "high"]
|
|
|
|
def test_no_overfitting(self, analyzer):
|
|
"""Test when there's no overfitting."""
|
|
train_result = OptimizationResult(
|
|
best_params={},
|
|
best_score=10.0,
|
|
best_result=None,
|
|
all_results=[],
|
|
converged=True,
|
|
)
|
|
validation_result = OptimizationResult(
|
|
best_params={},
|
|
best_score=10.0, # Same as train = no overfitting
|
|
best_result=None,
|
|
all_results=[],
|
|
converged=True,
|
|
)
|
|
|
|
overfitting = analyzer.detect_overfitting(
|
|
train_result, validation_result, threshold=0.3
|
|
)
|
|
|
|
assert overfitting.is_overfitted is False
|
|
assert overfitting.severity == "none"
|
|
|
|
def test_get_optimization_curve(self, analyzer, sample_result):
|
|
"""Test getting optimization curve."""
|
|
iterations, best_scores = analyzer.get_optimization_curve(sample_result)
|
|
|
|
assert len(iterations) == 5
|
|
assert len(best_scores) == 5
|
|
assert best_scores == [1.0, 2.0, 3.0, 4.0, 5.0] # Cumulative best
|
|
|
|
def test_get_convergence_rate(self, analyzer, sample_result):
|
|
"""Test convergence rate calculation."""
|
|
rate = analyzer.get_convergence_rate(sample_result, window_size=2)
|
|
|
|
assert isinstance(rate, float)
|
|
|
|
def test_get_top_configurations(self, analyzer, sample_result):
|
|
"""Test getting top configurations."""
|
|
top = analyzer.get_top_configurations(sample_result, n_top=3)
|
|
|
|
assert len(top) == 3
|
|
# Should be sorted by score (descending)
|
|
assert top[0][1] >= top[1][1] >= top[2][1]
|
|
|
|
def test_calculate_robustness_score(self, analyzer, sample_result):
|
|
"""Test robustness score calculation."""
|
|
score = analyzer.calculate_robustness_score(sample_result, n_bootstrap=50)
|
|
|
|
assert 0.0 <= score <= 1.0
|
|
|
|
def test_generate_report(self, analyzer, sample_result):
|
|
"""Test report generation."""
|
|
report = analyzer.generate_report(sample_result)
|
|
|
|
assert "best_params" in report
|
|
assert "best_score" in report
|
|
assert "n_iterations" in report
|
|
assert "score_statistics" in report
|
|
assert "parameter_sensitivity" in report
|
|
assert "top_configurations" in report
|
|
|
|
assert report["best_score"] == 5.0
|
|
assert report["converged"] is True
|
|
|
|
def test_empty_results(self, analyzer):
|
|
"""Test handling of empty results."""
|
|
empty_result = OptimizationResult(
|
|
best_params={},
|
|
best_score=0.0,
|
|
best_result=None,
|
|
all_results=[],
|
|
converged=False,
|
|
)
|
|
|
|
report = analyzer.generate_report(empty_result)
|
|
assert "error" in report
|
|
|
|
|
|
class TestIntegration:
|
|
"""Integration tests for the optimizer module."""
|
|
|
|
def test_full_optimization_workflow(self):
|
|
"""Test a complete optimization workflow."""
|
|
# Create parameter space
|
|
space = (
|
|
ParameterSpace()
|
|
.add_continuous("multiplier", 1.0, 3.0)
|
|
.add_integer("period", 5, 15)
|
|
)
|
|
|
|
# Create mock backtest function
|
|
call_count = 0
|
|
def backtest_fn(params):
|
|
nonlocal call_count
|
|
call_count += 1
|
|
multiplier = params.get("multiplier", 1.0)
|
|
period = params.get("period", 10)
|
|
score = multiplier * period / 10
|
|
return BacktestResult(
|
|
start_date=datetime.now(),
|
|
end_date=datetime.now(),
|
|
initial_capital=10000.0,
|
|
final_equity=10000.0 + score * 100,
|
|
total_return=score * 10,
|
|
total_trades=10,
|
|
winning_trades=5,
|
|
losing_trades=5,
|
|
win_rate=50.0,
|
|
avg_win=100.0,
|
|
avg_loss=-50.0,
|
|
profit_factor=1.5,
|
|
sharpe_ratio=score,
|
|
max_drawdown=10.0,
|
|
max_drawdown_duration=5,
|
|
volatility=15.0,
|
|
calmar_ratio=score,
|
|
)
|
|
|
|
# Test with each optimizer
|
|
config = OptimizerConfig(max_iterations=10, n_jobs=1, random_state=42)
|
|
|
|
# Grid Search
|
|
call_count = 0
|
|
grid_optimizer = GridSearchOptimizer(space, config=config, n_points=2)
|
|
grid_result = grid_optimizer.optimize(backtest_fn)
|
|
assert grid_result.best_score > 0
|
|
assert len(grid_result.all_results) > 0
|
|
|
|
# Random Search
|
|
call_count = 0
|
|
random_optimizer = RandomSearchOptimizer(space, config=config, n_samples=10)
|
|
random_result = random_optimizer.optimize(backtest_fn)
|
|
assert random_result.best_score > 0
|
|
|
|
# Bayesian Optimization
|
|
call_count = 0
|
|
bayesian_optimizer = BayesianOptimizer(
|
|
space,
|
|
config=config,
|
|
n_initial_points=5,
|
|
acquisition="ei",
|
|
)
|
|
bayesian_result = bayesian_optimizer.optimize(backtest_fn)
|
|
assert bayesian_result.best_score > 0
|
|
|
|
# Analyze results
|
|
analyzer = OptimizationAnalyzer()
|
|
grid_report = analyzer.generate_report(grid_result)
|
|
assert grid_report["best_score"] == grid_result.best_score
|
|
|
|
def test_different_objectives(self):
|
|
"""Test optimization with different objectives."""
|
|
space = ParameterSpace().add_continuous("param", 0.0, 1.0)
|
|
|
|
def backtest_fn(params):
|
|
value = params.get("param", 0.5)
|
|
return BacktestResult(
|
|
start_date=datetime.now(),
|
|
end_date=datetime.now(),
|
|
initial_capital=10000.0,
|
|
final_equity=10000.0,
|
|
total_return=value * 100,
|
|
total_trades=10,
|
|
winning_trades=int(value * 10),
|
|
losing_trades=int((1 - value) * 10),
|
|
win_rate=value * 100,
|
|
avg_win=100.0,
|
|
avg_loss=-50.0,
|
|
profit_factor=1.5,
|
|
sharpe_ratio=value * 2,
|
|
max_drawdown=(1 - value) * 20,
|
|
max_drawdown_duration=5,
|
|
volatility=15.0,
|
|
calmar_ratio=value * 5,
|
|
)
|
|
|
|
# Test different objectives
|
|
objectives = [
|
|
OptimizationObjective.MAXIMIZE_RETURN,
|
|
OptimizationObjective.MAXIMIZE_SHARPE,
|
|
OptimizationObjective.MAXIMIZE_CALMAR,
|
|
OptimizationObjective.MINIMIZE_DRAWDOWN,
|
|
OptimizationObjective.MAXIMIZE_WIN_RATE,
|
|
]
|
|
|
|
for objective in objectives:
|
|
config = OptimizerConfig(
|
|
objective=objective,
|
|
max_iterations=5,
|
|
n_jobs=1,
|
|
random_state=42,
|
|
)
|
|
optimizer = RandomSearchOptimizer(space, config=config, n_samples=5)
|
|
result = optimizer.optimize(backtest_fn)
|
|
assert result.best_score != float("-inf")
|
|
|
|
def test_parameter_correlations(self):
|
|
"""Test parameter correlation analysis."""
|
|
all_results = [
|
|
({"x": 1.0, "y": 1.0}, 2.0, None),
|
|
({"x": 2.0, "y": 2.0}, 4.0, None),
|
|
({"x": 3.0, "y": 3.0}, 6.0, None),
|
|
({"x": 4.0, "y": 4.0}, 8.0, None),
|
|
({"x": 5.0, "y": 5.0}, 10.0, None),
|
|
]
|
|
|
|
result = OptimizationResult(
|
|
best_params={"x": 5.0, "y": 5.0},
|
|
best_score=10.0,
|
|
best_result=None,
|
|
all_results=all_results,
|
|
converged=True,
|
|
)
|
|
|
|
analyzer = OptimizationAnalyzer()
|
|
correlations = analyzer.analyze_parameter_correlations(result)
|
|
|
|
# x and y are perfectly correlated in the data
|
|
assert ("x", "y") in correlations or ("y", "x") in correlations
|