galaxis-po/backend/tests/unit/test_benchmark.py
머니페니 12d235a1f1 feat: add 9 new modules - notification alerts, trading journal, position sizing, pension allocation, drawdown monitoring, benchmark dashboard, tax simulation, correlation analysis, parameter optimizer
Phase 1:
- Real-time signal alerts (Discord/Telegram webhook)
- Trading journal with entry/exit tracking
- Position sizing calculator (Fixed/Kelly/ATR)

Phase 2:
- Pension asset allocation (DC/IRP 70% risk limit)
- Drawdown monitoring with SVG gauge
- Benchmark dashboard (portfolio vs KOSPI vs deposit)

Phase 3:
- Tax benefit simulation (Korean pension tax rules)
- Correlation matrix heatmap
- Parameter optimizer with grid search + overfit detection
2026-03-29 10:03:08 +09:00

167 lines
5.5 KiB
Python

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