diff --git a/backend/app/services/naver_finance.py b/backend/app/services/naver_finance.py index 4c2ade3..2eef2ef 100644 --- a/backend/app/services/naver_finance.py +++ b/backend/app/services/naver_finance.py @@ -369,3 +369,51 @@ async def get_vix_index() -> Optional[Dict]: except Exception as e: logger.warning("VIX 조회 실패: %s", e) return None + + +# ── 종목 펀더멘털(실적·컨센서스·목표주가) — 매수 스토리 기록용 (Phase 1) ──────────── +_FUNDAMENTALS_CACHE: Dict[str, Tuple[float, Optional[Dict]]] = {} +_FUNDAMENTALS_TTL = 600 # 10분 캐시 + + +async def get_stock_fundamentals(stock_code: str) -> Optional[Dict]: + """네이버 m.stock integration API로 실적·컨센서스·목표주가 조회. + + 매수 시점 "스토리" 기록용(매매 판단엔 미사용 — Phase 1). 실패/타임아웃 시 None + (호출측에서 기술 스토리로 폴백). 10분 TTL 캐시. + + Returns: + {"per","eps","target_price","recomm_mean"} 중 확보된 값. 없으면 None. + """ + import time as _t + _now = _t.monotonic() + _c = _FUNDAMENTALS_CACHE.get(stock_code) + if _c and _now - _c[0] < _FUNDAMENTALS_TTL: + return _c[1] + try: + url = f"https://m.stock.naver.com/api/stock/{stock_code}/integration" + async with httpx.AsyncClient(verify=True, timeout=5.0, headers=_HEADERS) as client: + resp = await client.get(url) + if resp.status_code != 200: + _FUNDAMENTALS_CACHE[stock_code] = (_now, None) + return None + data = resp.json() + out: Dict = {} + for it in (data.get("totalInfos") or []): + k = it.get("key") + if k == "PER": + out["per"] = it.get("value") + elif k == "EPS": + out["eps"] = it.get("value") + cons = data.get("consensusInfo") or {} + if cons.get("priceTargetMean"): + out["target_price"] = cons.get("priceTargetMean") + if cons.get("recommMean"): + out["recomm_mean"] = cons.get("recommMean") + result = out or None + _FUNDAMENTALS_CACHE[stock_code] = (_now, result) + return result + except Exception as e: + logger.debug("[FUNDAMENTALS] %s 조회 실패: %s", stock_code, e) + _FUNDAMENTALS_CACHE[stock_code] = (_now, None) + return None diff --git a/backend/app/services/trade_executor.py b/backend/app/services/trade_executor.py index 91f23d3..0d34493 100644 --- a/backend/app/services/trade_executor.py +++ b/backend/app/services/trade_executor.py @@ -769,7 +769,43 @@ async def _execute_buy( ) total_amount = Decimal(str(order_price)) * quantity - entry_reason = f"매수 신호: {', '.join(signals[:3])}. 점수: {score_val}점" + + # ── 매수 스토리 구성: 기술 + 뉴스 + 실적 + 전망 (Phase 1: 기록 전용) ────── + # 주문은 이미 위에서 접수됐으므로 여기서의 펀더멘털 조회는 주문을 지연·차단하지 + # 않는다(실패/타임아웃 시 기술 스토리로 폴백). 매매 판단엔 미사용. + _story = [f"기술: {', '.join(signals[:3])} (점수 {score_val})"] + try: + _ns = stock.get("_news_position_scale") + if _ns is not None and float(_ns) < 1.0: + _story.append(f"뉴스: 악재 감지 → 비중 {float(_ns):.0%}") + except Exception: + pass + if is_domestic: + try: + from app.services.naver_finance import get_stock_fundamentals + _fd = await get_stock_fundamentals(stock_code) + if _fd: + _val = [] + if _fd.get("per"): + _val.append(f"PER {_fd['per']}") + if _fd.get("eps"): + _val.append(f"EPS {_fd['eps']}") + if _val: + _story.append("실적: " + ", ".join(_val)) + _tp = _fd.get("target_price") + if _tp: + try: + _tpv = float(str(_tp).replace(",", "")) + _up = (_tpv / current_price - 1) * 100 if current_price else 0.0 + _outlook = f"전망: 목표가 {_tp}({_up:+.0f}%)" + if _fd.get("recomm_mean"): + _outlook += f", 투자의견 {_fd['recomm_mean']}/5" + _story.append(_outlook) + except Exception: + pass + except Exception as _story_err: + logger.debug(f"[STORY] {stock_code} 펀더멘털 스킵: {_story_err}") + entry_reason = " | ".join(_story) success = order_result.get("rt_cd") == "0" # KIS 성공 코드 is_pending = order_result.get("rt_cd") == "1" # 주문접수(체결 미확인) status = "COMPLETED" if success else ("PENDING" if is_pending else "FAILED")