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:
@@ -739,7 +739,14 @@ async def _execute_buy(
|
|||||||
f"invest={invest_amount:,.0f} qty={quantity} price={current_price:,.0f}"
|
f"invest={invest_amount:,.0f} qty={quantity} price={current_price:,.0f}"
|
||||||
)
|
)
|
||||||
if quantity <= 0:
|
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만원 미만이면 수수료 대비 비효율
|
# 최소 매수 금액 가드: 50만원 미만이면 수수료 대비 비효율
|
||||||
_MIN_BUY_AMOUNT = 500_000
|
_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"]:
|
if result["action"] in ["BUY", "SELL"]:
|
||||||
trade_results.append(result)
|
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회 이하 권장
|
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})")
|
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:
|
except ValueError as e:
|
||||||
logger.error(f"[TRADE_VALIDATION] {stock.get('stock_code')}: {e}")
|
logger.error(f"[TRADE_VALIDATION] {stock.get('stock_code')}: {e}")
|
||||||
|
|||||||
Reference in New Issue
Block a user