feat: 자동매매 진입 보류 사유 로깅 (관측성)

"매수 실행" 결정 후 조용히 스킵되던 경우의 사유를 로그로 남겨, 왜 매수가
안 나가는지 즉시 파악 가능하게 함. (이번 세션에서 이 원인 추적에 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBmzf1HDnvFHLraJ8eaaKJ
This commit is contained in:
2026-09-16 14:07:07 +09:00
parent 2e26791d86
commit d41e048821

View File

@@ -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}")