"""AI 투자 분석 — 매수 추천 금액 산출 검증 (자동매매 사이징과 동일 기준).""" from unittest.mock import patch import pytest from app.api.endpoints.market_data import _suggest_buy_amount from app.api.endpoints.portfolio import portfolio_cache def _summary(total_assets=10_000_000, cash=5_000_000, stock_value=3_000_000): return {"total_assets": total_assets, "available_cash": cash, "total_value": stock_value} @pytest.fixture(autouse=True) def _cache(): portfolio_cache.summary = _summary() yield portfolio_cache.summary = None class _Adp: fallback_used = False position_size = 0.06 def test_buy_amount_uses_position_cap_and_confidence(): with patch("app.services.adaptive_strategy.get_current_adaptive_params", return_value=_Adp()): out = _suggest_buy_amount(current_price=70_000, recommendation="BUY", confidence=0.7, stock_code="005930") # 1,000만 × 6% = 60만, 신뢰도 0.7 → 보정 1.0 → 70,000원 × 8주 = 560,000 assert out["suggested_quantity"] == 8 assert out["suggested_amount"] == 560_000 assert out["position_ratio"] == pytest.approx(5.6, abs=0.01) assert "6%" in out["sizing_note"] def test_high_confidence_scales_up_capped_at_120pct(): with patch("app.services.adaptive_strategy.get_current_adaptive_params", return_value=_Adp()): out = _suggest_buy_amount(70_000, "BUY", 1.0, "005930") # 60만 × min(1.2, 1.0/0.7=1.43) = 72만 → 10주 assert out["suggested_quantity"] == 10 def test_non_buy_recommendation_returns_zero(): for rec in ("HOLD", "SELL"): out = _suggest_buy_amount(70_000, rec, 0.8, "005930") assert out["suggested_amount"] == 0 and rec in out["sizing_note"] def test_limited_by_available_cash_and_reserve(): portfolio_cache.summary = _summary(cash=600_000) # 예수금 60만, 최소보유 5% = 50만 with patch("app.services.adaptive_strategy.get_current_adaptive_params", return_value=_Adp()): out = _suggest_buy_amount(70_000, "BUY", 0.7, "005930") assert out["suggested_quantity"] == 1 # 사용 가능 10만 → 1주 def test_limited_by_swing_capital_cap(): # 스윙 자본 상한이 구속 조건이 되는 경우를 검증한다. # [2026-09-27] 기존에는 stock_value=4_900_000 을 하드코딩해 "상한 50% = 500만" 을 # 전제했으나, SWING_MAX_CAPITAL_RATIO 가 0.50→0.70 으로 바뀌면서(2026-09-23) # 상한이 700만이 되어 종목당 6% 캡(60만)이 먼저 걸렸다 → 8주가 나와 실패. # config 값이 또 바뀌어도 의도가 유지되도록 상한에서 역산한다(잔여 10만 → 1주). from app.core.config import settings _total = 10_000_000 _held = int(_total * settings.SWING_MAX_CAPITAL_RATIO) - 100_000 portfolio_cache.summary = _summary(total_assets=_total, stock_value=_held) with patch("app.services.adaptive_strategy.get_current_adaptive_params", return_value=_Adp()): out = _suggest_buy_amount(70_000, "BUY", 0.7, "005930") assert out["suggested_quantity"] == 1 def test_no_balance_info_reports_reason(): portfolio_cache.summary = None out = _suggest_buy_amount(70_000, "BUY", 0.8, "005930") assert out["suggested_amount"] == 0 and "잔고" in out["sizing_note"] # ── 목표가 · 손절가 ───────────────────────────────────────────────────────── from app.api.endpoints.market_data import _suggest_targets # noqa: E402 def test_targets_use_tighter_stop_and_2r(): """ATR 손절(2.5×)과 -7% 중 타이트한 쪽(가격 높은 쪽), 목표는 +2R.""" with patch("app.services.adaptive_strategy.get_current_adaptive_params", side_effect=Exception): # ATR 1,000 → ATR손절 100,000-2,500 = 97,500 / -7% = 93,000 → 97,500 채택 out = _suggest_targets(100_000, atr=1_000.0) assert out["stop_loss_price"] == 97_500 assert out["target_price"] == 105_000 # 100,000 + 2×2,500 assert out["stop_loss_pct"] == pytest.approx(-2.5, abs=0.01) assert out["target_pct"] == pytest.approx(5.0, abs=0.01) assert out["risk_reward"] == 2.0 assert "ATR" in out["target_note"] def test_percent_stop_wins_when_atr_is_wide(): with patch("app.services.adaptive_strategy.get_current_adaptive_params", side_effect=Exception): # ATR 5,000 → ATR손절 87,500 < -7% 93,000 → 93,000(타이트) 채택 out = _suggest_targets(100_000, atr=5_000.0) assert out["stop_loss_price"] == 93_000 assert out["target_price"] == 114_000 assert "-7%" in out["target_note"] def test_targets_without_atr_fall_back_to_percent(): with patch("app.services.adaptive_strategy.get_current_adaptive_params", side_effect=Exception): out = _suggest_targets(100_000, atr=None) assert out["stop_loss_price"] == 93_000 and "ATR 없음" in out["target_note"] def test_targets_zero_price(): assert _suggest_targets(0, atr=1000.0)["target_price"] == 0