galaxis-po/backend/tests/unit/test_benchmark.py

167 lines
5.5 KiB
Python
Raw Normal View History

"""
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