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>
This commit is contained in:
2026-09-23 13:34:59 +09:00
parent acbd9f35ea
commit f480eac25c
3 changed files with 138 additions and 0 deletions

View File

@@ -66,3 +66,39 @@ 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