diff --git a/backend/app/services/trade_executor.py b/backend/app/services/trade_executor.py index 0d34493..c5b5c4d 100644 --- a/backend/app/services/trade_executor.py +++ b/backend/app/services/trade_executor.py @@ -739,7 +739,14 @@ async def _execute_buy( f"invest={invest_amount:,.0f} qty={quantity} price={current_price:,.0f}" ) if quantity <= 0: - return {"action": "SKIP", "reason": "quantity_zero", "stock": stock_name} + # [관측성] 수량 0 원인을 구분: 배율(플레이북 FOMC·국면·뉴스)로 투자금이 + # 0이 됐는지, 아니면 현금 부족인지. "매수 실행 후 조용히 스킵"의 정체를 로그로. + if _position_multiplier <= 0 or _news_position_scale <= 0: + _qz_reason = "entry_blocked_multiplier_0(플레이북/국면/뉴스로 진입차단)" + else: + _qz_reason = "quantity_zero(현금/가격 대비 1주 미만)" + logger.info(f"[BUY_SKIP] {stock_code}/{stock_name}: {_qz_reason} (invest={invest_amount:,.0f})") + return {"action": "SKIP", "reason": _qz_reason, "stock": stock_name} # 최소 매수 금액 가드: 50만원 미만이면 수수료 대비 비효율 _MIN_BUY_AMOUNT = 500_000 @@ -1250,6 +1257,14 @@ async def execute_auto_trade_background(recommendations_response: dict, db: Asyn if result["action"] in ["BUY", "SELL"]: trade_results.append(result) + else: + # [관측성] 매수 후보가 왜 진입 안 됐는지 사유를 남긴다. 기존엔 HOLD/SKIP + # 사유가 로그에 안 남아 "매수 실행" 후 조용히 스킵되는 이유를 추적하기 + # 어려웠다(예: 플레이북 FOMC 차단, 상관/뉴스/쿨다운, 수량 0 등). + logger.info( + f"[AUTO_TRADE_BG] {stock.get('stock_code')} 진입 보류: " + f"action={result.get('action')} reason={result.get('reason', '미상')}" + ) await asyncio.sleep(0.5) # KIS API 레이트 리밋: 초당 2회 이하 권장 @@ -1595,7 +1610,7 @@ async def execute_auto_trade(stock: dict, db: AsyncSession): logger.info(f"[TRADE] ⏸️ HOLD: {stock_code}/{stock_name} (buy={should_buy}, sell={should_sell}, has_rule={has_rule})") - return {"action": "HOLD", "stock": stock_name} + return {"action": "HOLD", "stock": stock_name, "reason": "no_buy_signal(진입조건 미충족)"} except ValueError as e: logger.error(f"[TRADE_VALIDATION] {stock.get('stock_code')}: {e}")