Files
auto-stock/backend/tests/test_ai_analysis_buy_amount.py
shuim f480eac25c feat: AI 투자 분석에 목표가·손절가 표시
CLAUDE.md §5.2 기준으로 계산해 매수 추천 금액 아래에 함께 표시.
  · 손절 = ATR(2.5×)과 -7% 중 타이트한 쪽(가격 높은 쪽), 적응형 모드면 그 값 사용
  · 목표 = 진입가 + 2R (R = 진입가 − 손절가) — +2R 50% 분할 매도 지점
ATR 은 stock_analysis.atr_14 우선, 없으면 DB 일봉으로 계산(KIS API 미사용). 테스트 4건 추가.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-23 13:34:59 +09:00

105 lines
4.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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():
# 스윙 상한 50% = 500만, 이미 490만 보유 → 잔여 10만
portfolio_cache.summary = _summary(stock_value=4_900_000)
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