""" Unit tests for benchmark service. """ from datetime import date, timedelta from decimal import Decimal from unittest.mock import MagicMock, patch import pandas as pd import pytest from app.services.benchmark import BenchmarkService @pytest.fixture def db(): return MagicMock() @pytest.fixture def service(db): return BenchmarkService(db) class TestGetDepositRate: def test_returns_fixed_rate(self, service): rate = service.get_deposit_rate() assert rate == 3.5 class TestGetBenchmarkData: @patch("app.services.benchmark.pykrx_stock") def test_returns_kospi_time_series(self, mock_pykrx, service): dates = pd.date_range("2025-01-02", periods=3, freq="B") mock_pykrx.get_index_ohlcv.return_value = pd.DataFrame( {"시가": [2800, 2810, 2820], "종가": [2810, 2820, 2830]}, index=dates, ) result = service.get_benchmark_data( "kospi", date(2025, 1, 2), date(2025, 1, 6) ) assert len(result) == 3 assert result[0]["date"] == dates[0].date() assert result[0]["close"] == 2810 @patch("app.services.benchmark.pykrx_stock") def test_empty_data_returns_empty_list(self, mock_pykrx, service): mock_pykrx.get_index_ohlcv.return_value = pd.DataFrame() result = service.get_benchmark_data( "kospi", date(2025, 1, 2), date(2025, 1, 6) ) assert result == [] class TestCalculateMetrics: def test_cumulative_return(self, service): returns = [0.01, 0.02, -0.005, 0.015] metrics = service._calculate_metrics(returns, num_days=120) expected_cum = ((1.01) * (1.02) * (0.995) * (1.015) - 1) * 100 assert abs(metrics.cumulative_return - expected_cum) < 0.01 def test_max_drawdown(self, service): returns = [0.10, -0.20, 0.05] metrics = service._calculate_metrics(returns, num_days=90) assert metrics.max_drawdown < 0 def test_sharpe_ratio_with_zero_std(self, service): returns = [0.01, 0.01, 0.01] metrics = service._calculate_metrics(returns, num_days=90) assert metrics.sharpe_ratio is None def test_empty_returns(self, service): metrics = service._calculate_metrics([], num_days=0) assert metrics.cumulative_return == 0.0 assert metrics.annualized_return == 0.0 assert metrics.max_drawdown == 0.0 class TestCompareWithBenchmark: def _make_snapshot(self, snapshot_date, total_value): snap = MagicMock() snap.snapshot_date = snapshot_date snap.total_value = Decimal(str(total_value)) return snap @patch("app.services.benchmark.pykrx_stock") def test_compare_basic(self, mock_pykrx, service, db): base = date(2025, 1, 2) snapshots = [ self._make_snapshot(base, 10000), self._make_snapshot(base + timedelta(days=30), 10500), self._make_snapshot(base + timedelta(days=60), 10800), ] portfolio = MagicMock() portfolio.id = 1 portfolio.name = "Test Portfolio" portfolio.user_id = 1 db.query.return_value.filter.return_value.first.return_value = portfolio db.query.return_value.filter.return_value.order_by.return_value.all.return_value = snapshots dates = pd.date_range(base.strftime("%Y%m%d"), periods=3, freq="30D") mock_pykrx.get_index_ohlcv.return_value = pd.DataFrame( {"시가": [2800, 2850, 2900], "종가": [2810, 2860, 2910]}, index=dates, ) result = service.compare_with_benchmark( portfolio_id=1, benchmark_type="kospi", period="all", user_id=1 ) assert result.portfolio_name == "Test Portfolio" assert result.benchmark_type == "kospi" assert result.alpha is not None assert len(result.time_series) > 0 @patch("app.services.benchmark.pykrx_stock") def test_compare_not_found(self, mock_pykrx, service, db): db.query.return_value.filter.return_value.first.return_value = None with pytest.raises(ValueError, match="Portfolio not found"): service.compare_with_benchmark( portfolio_id=999, benchmark_type="kospi", period="1y", user_id=1 ) @patch("app.services.benchmark.pykrx_stock") def test_compare_no_snapshots(self, mock_pykrx, service, db): portfolio = MagicMock() portfolio.id = 1 portfolio.name = "Empty" portfolio.user_id = 1 db.query.return_value.filter.return_value.first.return_value = portfolio db.query.return_value.filter.return_value.order_by.return_value.all.return_value = [] with pytest.raises(ValueError, match="스냅샷 데이터가 없습니다"): service.compare_with_benchmark( portfolio_id=1, benchmark_type="kospi", period="1y", user_id=1 ) class TestCalculateInformationRatio: def test_positive_tracking_error(self, service): portfolio_returns = [0.02, 0.03, -0.01, 0.04] benchmark_returns = [0.01, 0.02, -0.005, 0.02] ir = service._calculate_information_ratio(portfolio_returns, benchmark_returns) assert ir is not None assert isinstance(ir, float) def test_zero_tracking_error(self, service): same_returns = [0.01, 0.02, 0.03] ir = service._calculate_information_ratio(same_returns, same_returns) assert ir is None def test_empty_returns(self, service): ir = service._calculate_information_ratio([], []) assert ir is None