From d41e0488213f072078173d07ed6a42669dd68fcf Mon Sep 17 00:00:00 2001 From: shuim Date: Wed, 16 Sep 2026 14:07:07 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=9E=90=EB=8F=99=EB=A7=A4=EB=A7=A4=20?= =?UTF-8?q?=EC=A7=84=EC=9E=85=20=EB=B3=B4=EB=A5=98=20=EC=82=AC=EC=9C=A0=20?= =?UTF-8?q?=EB=A1=9C=EA=B9=85=20(=EA=B4=80=EC=B8=A1=EC=84=B1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "매수 실행" 결정 후 조용히 스킵되던 경우의 사유를 로그로 남겨, 왜 매수가 안 나가는지 즉시 파악 가능하게 함. (이번 세션에서 이 원인 추적에 15턴+ 소요) - execute_auto_trade_background: execute_auto_trade가 BUY/SELL이 아닌 결과를 반환하면 종목코드+action+reason을 INFO 로그로 출력. - _execute_buy: 수량 0 스킵 시 배율(플레이북 FOMC·국면·뉴스)로 인한 진입차단 vs 현금부족을 구분해 사유 반환 + [BUY_SKIP] 로그. - 최종 HOLD(진입조건 미충족)에 reason="no_buy_signal" 부여(기존 '미상' 제거). 매매 판단 로직은 변경 없음(로깅·사유 문자열만 추가). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01HBmzf1HDnvFHLraJ8eaaKJ --- backend/app/services/trade_executor.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) 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}")