feat: AI 투자 분석에 매수 추천 금액 표시

BUY 신호일 때 실제 자동매매와 동일한 기준으로 매수 금액·수량을 계산해 패널 상단에 표시.
  · 종목당 상한 = 총자산 × MAX_POSITION_SIZE(적응형 모드면 그 값)
  · 신뢰도 보정 = 예산 × (confidence/0.7), 0.5~1.2배
  · 현금 여력(최소 현금보유 제외)·스윙 합산 상한(SWING_MAX_CAPITAL_RATIO) 잔여로 클램프
BUY 가 아니거나 예산이 1주 미만이면 금액 대신 사유를 표시. 테스트 6건.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-23 13:33:06 +09:00
parent 5706a8971c
commit acbd9f35ea
3 changed files with 181 additions and 0 deletions

View File

@@ -580,6 +580,88 @@ async def get_orderbook(stock_code: str):
return {"stock_code": stock_code, "asks": [], "bids": [], "error": "temporarily_unavailable"}
def _suggest_buy_amount(
current_price: int,
recommendation: str,
confidence: float,
stock_code: str,
) -> dict:
"""AI 분석 화면에 표시할 매수 추천 금액·수량 산출.
실제 자동매매 사이징과 같은 기준을 쓴다(CLAUDE.md §5.1):
· 종목당 상한 = 총자산 × MAX_POSITION_SIZE (적응형 모드면 그 값)
· 현금 여력 = 예수금 − 최소 현금보유(총자산 × (1 − MAX_TOTAL_CAPITAL_USAGE))
· 스윙 합산 상한(SWING_MAX_CAPITAL_RATIO) 잔여 한도
· 신뢰도 보정 = 예산 × (confidence / 0.7), 최대 1.2배
BUY 가 아니거나 잔고 정보를 모르면 금액 0 과 사유를 반환한다.
"""
from app.core.config import settings as _cfg
out = {
"suggested_amount": 0,
"suggested_quantity": 0,
"position_ratio": 0.0,
"sizing_note": "",
}
if current_price <= 0:
out["sizing_note"] = "현재가 없음"
return out
if recommendation != "BUY":
out["sizing_note"] = f"{recommendation} 신호 — 신규 매수 제안 없음"
return out
try:
from app.api.endpoints.portfolio import portfolio_cache
summary = portfolio_cache.get_summary_copy() or {}
total_assets = float(summary.get("total_assets", 0) or 0)
available_cash = float(summary.get("available_cash", 0) or 0)
stock_value = float(summary.get("total_value", 0) or 0)
except Exception:
total_assets = available_cash = stock_value = 0.0
if total_assets <= 0 or available_cash <= 0:
out["sizing_note"] = "잔고 조회 전 — 금액 계산 불가"
return out
position_cap = float(getattr(_cfg, "MAX_POSITION_SIZE", 0.06))
try:
from app.services.adaptive_strategy import get_current_adaptive_params
_adp = get_current_adaptive_params()
if not _adp.fallback_used:
position_cap = float(_adp.position_size)
except Exception:
pass
budget = total_assets * position_cap
# 신뢰도 보정 (기준 0.7)
if confidence > 0:
budget *= min(1.2, max(0.5, confidence / 0.7))
# 현금 여력
reserve = total_assets * (1 - float(getattr(_cfg, "MAX_TOTAL_CAPITAL_USAGE", 0.95)))
budget = min(budget, max(0.0, available_cash - reserve))
# 스윙 합산 상한 잔여
swing_cap = float(getattr(_cfg, "SWING_MAX_CAPITAL_RATIO", 1.0) or 1.0)
if 0 < swing_cap < 1:
budget = min(budget, max(0.0, total_assets * swing_cap - stock_value))
qty = int(budget // current_price)
if qty <= 0:
out["sizing_note"] = "가용 예산이 1주 미만 — 매수 제안 없음"
return out
amount = qty * current_price
out.update({
"suggested_amount": amount,
"suggested_quantity": qty,
"position_ratio": round(amount / total_assets * 100, 2),
"sizing_note": (
f"총자산 {total_assets:,.0f}원 × 종목당 {position_cap * 100:.0f}%"
f"(신뢰도 {confidence * 100:.0f}% 반영) 기준"
),
})
return out
@router.get("/ai-analysis/{stock_code}")
async def get_ai_analysis(stock_code: str, db: AsyncSession = Depends(get_db)):
"""AI 기반 종목 분석 엔드포인트 (기술적 지표 + 규칙 기반 분석)"""
@@ -791,6 +873,8 @@ async def get_ai_analysis(stock_code: str, db: AsyncSession = Depends(get_db)):
"recommendation": recommendation,
"confidence": confidence,
"trend": trend,
# 매수 추천 금액 (실제 자동매매 사이징과 동일 기준)
**_suggest_buy_amount(current_price, recommendation, confidence, stock_code),
"analyzed_at": datetime.now(_KST).isoformat(), # KST +09:00 포함
}