""" Tests for the strategy optimizer service and schemas. """ import pytest from datetime import date from decimal import Decimal from unittest.mock import MagicMock, patch from app.schemas.optimizer import ( DEFAULT_GRIDS, KJB_DEFAULT_GRID, STRATEGY_TYPES, OptimizeRequest, OptimizeResponse, OptimizeResultItem, ) from app.services.optimizer import ( OptimizerService, _expand_grid, _build_strategy_params, ) # --- Unit tests for grid expansion --- class TestExpandGrid: def test_single_param(self): grid = {"a": [1, 2, 3]} result = _expand_grid(grid) assert len(result) == 3 assert result[0] == {"a": 1} assert result[2] == {"a": 3} def test_two_params(self): grid = {"a": [1, 2], "b": [10, 20]} result = _expand_grid(grid) assert len(result) == 4 assert {"a": 1, "b": 10} in result assert {"a": 2, "b": 20} in result def test_empty_grid(self): result = _expand_grid({}) assert len(result) == 1 # single empty combo assert result[0] == {} def test_kjb_default_grid_size(self): result = _expand_grid(KJB_DEFAULT_GRID) # 3 * 3 * 3 = 27 assert len(result) == 27 class TestBuildStrategyParams: def test_flat_params(self): result = _build_strategy_params("kjb", { "stop_loss_pct": 0.05, "target1_pct": 0.07, }) assert result == {"stop_loss_pct": 0.05, "target1_pct": 0.07} def test_nested_params(self): result = _build_strategy_params("multi_factor", { "weights.value": 0.3, "weights.quality": 0.2, }) assert result == {"weights": {"value": 0.3, "quality": 0.2}} def test_deeply_nested(self): result = _build_strategy_params("test", { "a.b.c": 1, }) assert result == {"a": {"b": {"c": 1}}} # --- Schema tests --- class TestOptimizeRequest: def test_defaults(self): req = OptimizeRequest( strategy_type="kjb", start_date=date(2024, 1, 1), end_date=date(2024, 12, 31), ) assert req.initial_capital == Decimal("100000000") assert req.commission_rate == Decimal("0.00015") assert req.slippage_rate == Decimal("0.001") assert req.benchmark == "KOSPI" assert req.top_n == 30 assert req.rank_by == "sharpe_ratio" assert req.param_grid is None def test_custom_grid(self): custom = {"stop_loss_pct": [0.02, 0.04]} req = OptimizeRequest( strategy_type="kjb", start_date=date(2024, 1, 1), end_date=date(2024, 12, 31), param_grid=custom, ) assert req.param_grid == custom def test_all_strategy_types_valid(self): for st in STRATEGY_TYPES: req = OptimizeRequest( strategy_type=st, start_date=date(2024, 1, 1), end_date=date(2024, 12, 31), ) assert req.strategy_type == st class TestOptimizeResponse: def test_response_serialization(self): item = OptimizeResultItem( rank=1, params={"stop_loss_pct": 0.05}, total_return=Decimal("15.5"), cagr=Decimal("12.3"), mdd=Decimal("-8.2"), sharpe_ratio=Decimal("1.45"), volatility=Decimal("18.7"), benchmark_return=Decimal("10.0"), excess_return=Decimal("5.5"), ) resp = OptimizeResponse( strategy_type="kjb", total_combinations=27, results=[item], best_params={"stop_loss_pct": 0.05}, ) data = resp.model_dump(mode="json") assert data["total_combinations"] == 27 assert data["results"][0]["sharpe_ratio"] == 1.45 assert isinstance(data["results"][0]["sharpe_ratio"], float) # --- Default grids --- class TestDefaultGrids: def test_all_strategy_types_have_grids(self): for st in STRATEGY_TYPES: assert st in DEFAULT_GRIDS def test_kjb_grid_keys(self): assert "stop_loss_pct" in KJB_DEFAULT_GRID assert "target1_pct" in KJB_DEFAULT_GRID assert "rs_lookback" in KJB_DEFAULT_GRID # --- OptimizerService tests with mocked DB --- class TestOptimizerService: def _make_service(self): db = MagicMock() return OptimizerService(db) def test_optimize_no_grid_raises_for_unknown_type(self): service = self._make_service() req = OptimizeRequest( strategy_type="unknown_type", start_date=date(2024, 1, 1), end_date=date(2024, 12, 31), ) with pytest.raises(ValueError, match="No parameter grid"): service.optimize(req) @patch.object(OptimizerService, "_run_single") def test_optimize_uses_default_grid(self, mock_run): mock_run.return_value = { "total_return": 10.0, "cagr": 8.0, "mdd": -5.0, "sharpe_ratio": 1.2, "volatility": 15.0, "benchmark_return": 7.0, "excess_return": 3.0, } service = self._make_service() req = OptimizeRequest( strategy_type="kjb", start_date=date(2024, 1, 1), end_date=date(2024, 12, 31), ) result = service.optimize(req) assert result.strategy_type == "kjb" assert result.total_combinations == 27 # 3*3*3 assert len(result.results) == 27 assert result.results[0].rank == 1 @patch.object(OptimizerService, "_run_single") def test_optimize_ranks_by_sharpe(self, mock_run): mock_run.side_effect = [ { "total_return": 10.0, "cagr": 8.0, "mdd": -5.0, "sharpe_ratio": 0.5, "volatility": 15.0, "benchmark_return": 7.0, "excess_return": 3.0, }, { "total_return": 20.0, "cagr": 15.0, "mdd": -10.0, "sharpe_ratio": 2.0, "volatility": 20.0, "benchmark_return": 7.0, "excess_return": 13.0, }, ] service = self._make_service() req = OptimizeRequest( strategy_type="kjb", start_date=date(2024, 1, 1), end_date=date(2024, 12, 31), param_grid={"stop_loss_pct": [0.03, 0.05]}, rank_by="sharpe_ratio", ) result = service.optimize(req) assert result.total_combinations == 2 assert result.results[0].sharpe_ratio == Decimal("2.0") assert result.results[1].sharpe_ratio == Decimal("0.5") assert result.best_params == {"stop_loss_pct": 0.05} @patch.object(OptimizerService, "_run_single") def test_optimize_handles_failures_gracefully(self, mock_run): mock_run.side_effect = [ Exception("data error"), { "total_return": 10.0, "cagr": 8.0, "mdd": -5.0, "sharpe_ratio": 1.0, "volatility": 15.0, "benchmark_return": 7.0, "excess_return": 3.0, }, ] service = self._make_service() req = OptimizeRequest( strategy_type="kjb", start_date=date(2024, 1, 1), end_date=date(2024, 12, 31), param_grid={"stop_loss_pct": [0.03, 0.05]}, ) result = service.optimize(req) assert result.total_combinations == 2 assert len(result.results) == 1 @patch.object(OptimizerService, "_run_single") def test_optimize_all_fail_returns_empty(self, mock_run): mock_run.side_effect = Exception("fail") service = self._make_service() req = OptimizeRequest( strategy_type="kjb", start_date=date(2024, 1, 1), end_date=date(2024, 12, 31), param_grid={"stop_loss_pct": [0.03]}, ) result = service.optimize(req) assert result.total_combinations == 1 assert len(result.results) == 0 assert result.best_params == {} @patch.object(OptimizerService, "_run_single") def test_optimize_rank_by_cagr(self, mock_run): mock_run.side_effect = [ { "total_return": 30.0, "cagr": 25.0, "mdd": -15.0, "sharpe_ratio": 0.8, "volatility": 25.0, "benchmark_return": 7.0, "excess_return": 23.0, }, { "total_return": 15.0, "cagr": 12.0, "mdd": -5.0, "sharpe_ratio": 1.5, "volatility": 10.0, "benchmark_return": 7.0, "excess_return": 8.0, }, ] service = self._make_service() req = OptimizeRequest( strategy_type="quality", start_date=date(2024, 1, 1), end_date=date(2024, 12, 31), param_grid={"min_fscore": [6, 7]}, rank_by="cagr", ) result = service.optimize(req) assert result.results[0].cagr == Decimal("25.0") assert result.results[1].cagr == Decimal("12.0")