feat: LLM 판단 상세 로깅 + 텔레그램 메시지에 LLM 응답 포함

- get_entry_price/get_exit_price가 전체 dict 반환 (action, price, confidence, reason, market_status, watch_needed)
- 매수: 시그널→LLM 승인/스킵 사유, 확신도, 시장 상태 텔레그램 전송
- 매도: LLM 지정가 설정 시 진입 대비 수익률, 확신도, 관망 여부 텔레그램 전송
- 청산: LLM/cascade 구분 태그 (텔레그램 + 로그)
- cascade fallback 전환 시 로그 명시

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
joungmin
2026-03-05 21:57:19 +09:00
parent 19a35e1009
commit 4f9e2c44c7
2 changed files with 69 additions and 28 deletions

View File

@@ -567,20 +567,21 @@ F&G지수: {fng} ({'공포' if fng <= 40 else '중립' if fng <= 50 else '탐욕
data = _call_llm(prompt, ticker)
if data is None:
log.info(f'[LLM매수] {ticker} → LLM 오류/무응답')
return None
reason = data.get('reason', '')
status = data.get('market_status', '')
reason = data.get('reason', '')
status = data.get('market_status', '')
confidence = data.get('confidence', '?')
if data.get('action') == 'skip':
log.info(f'[LLM매수] {ticker} → skip | {status} | {reason}')
return None
log.info(f'[LLM매수] {ticker} → skip | {confidence} | {status} | {reason}')
return data # action=skip
if data.get('action') == 'buy':
price = float(data['price'])
confidence = data.get('confidence', '?')
log.info(f'[LLM매수] {ticker} → buy {price:,.0f}원 | {confidence} | {status} | {reason}')
return price
data['price'] = float(data['price'])
log.info(f'[LLM매수] {ticker} → buy {data["price"]:,.0f}원 | {confidence} | {status} | {reason}')
return data # action=buy, price=float
log.warning(f'[LLM매수] {ticker} 알 수 없는 action: {data}')
return None
@@ -620,16 +621,22 @@ def get_exit_price(
data = _call_llm(prompt, ticker)
if data is None:
log.info(f'[LLM매도] {ticker} → LLM 오류/무응답 → cascade fallback')
return None
reason = data.get('reason', '')
status = data.get('market_status', '')
reason = data.get('reason', '')
status = data.get('market_status', '')
confidence = data.get('confidence', '?')
watch = data.get('watch_needed', False)
if data.get('action') == 'hold':
log.info(f'[LLM매도] {ticker} → hold | {status} | {reason}')
return None
log.info(f'[LLM매도] {ticker} → hold | {confidence} | {status} | watch={watch} | {reason}')
return data # action=hold
suggested = float(data['price'])
confidence = data.get('confidence', '?')
log.info(f'[LLM매도] {ticker} 지정가 교체: {current_target:,.0f}{suggested:,.0f}원 | {confidence} | {status} | {reason}')
return suggested
data['price'] = float(data['price'])
pnl_from_entry = (data['price'] - entry_price) / entry_price * 100
log.info(
f'[LLM매도] {ticker} 지정가 교체: {current_target:,.0f}{data["price"]:,.0f}'
f'(진입 대비 {pnl_from_entry:+.2f}%) | {confidence} | {status} | watch={watch} | {reason}'
)
return data # action=sell, price=float

View File

@@ -297,9 +297,8 @@ def process_signal(sig: dict) -> None:
vol_ratio = sig['vol_ratio']
log.info(f"[시그널] {ticker} {cur_price:,.0f}원 vol {vol_ratio:.1f}x → LLM 판단 요청")
tg(f"🔔 <b>시그널</b> {ticker}\n가격: {cur_price:,.0f}원 볼륨: {vol_ratio:.1f}x\nLLM 판단 요청 중...")
buy_price = get_entry_price(
llm_result = get_entry_price(
ticker=ticker,
signal=sig,
bar_list=bar_list,
@@ -308,12 +307,25 @@ def process_signal(sig: dict) -> None:
max_positions=MAX_POS,
)
if buy_price is None:
tg(f"⏭️ <b>매수 스킵</b> {ticker}\nLLM이 매수 거절")
if llm_result is None or llm_result.get('action') != 'buy':
reason = llm_result.get('reason', 'LLM 오류') if llm_result else 'LLM 무응답'
status = llm_result.get('market_status', '') if llm_result else ''
log.info(f"[매수/LLM] {ticker} → 스킵 | {reason}")
tg(
f"⏭️ <b>매수 스킵</b> {ticker}\n"
f"현재가: {cur_price:,.0f}원 볼륨: {vol_ratio:.1f}x\n"
f"시장: {status}\n"
f"사유: {reason}"
)
return
buy_price = _round_price(buy_price)
buy_price = _round_price(llm_result['price'])
confidence = llm_result.get('confidence', '?')
reason = llm_result.get('reason', '')
status = llm_result.get('market_status', '')
qty = PER_POS * (1 - FEE) / buy_price
diff_pct = (buy_price - cur_price) / cur_price * 100
log.info(f"[매수/LLM] {ticker} → 승인 {buy_price:,.0f}원 (현재가 {cur_price:,.0f}원, 차이 {diff_pct:+.2f}%)")
if SIM_MODE:
uuid = f"sim-buy-{ticker}"
@@ -337,9 +349,11 @@ def process_signal(sig: dict) -> None:
}
log.info(f"[지정가매수] {ticker} {buy_price:,.0f}원 수량: {qty:.6f}")
tg(
f"📥 <b>지정가 매수 제출</b> {ticker}\n"
f": {buy_price:,.0f} 수량: {qty:.6f}\n"
f"볼륨: {vol_ratio:.1f}x\n"
f"📥 <b>지정가 매수</b> {ticker}\n"
f"지정가: {buy_price:,.0f}(현재가 대비 {diff_pct:+.2f}%)\n"
f"수량: {qty:.6f} 볼륨: {vol_ratio:.1f}x\n"
f"확신: {confidence} 시장: {status}\n"
f"LLM: {reason}\n"
f"{'[시뮬]' if SIM_MODE else '[실거래]'}"
)
@@ -438,10 +452,11 @@ def _record_exit(ticker: str, exit_price: float, tag: str) -> None:
'trail': '⑤ 트레일스탑', 'timeout': '⑤ 타임아웃',
}.get(tag, tag)
llm_flag = 'LLM' if pos.get('llm_active') else 'cascade'
icon = "" if pnl > 0 else "🔴"
log.info(f"[청산/{tag}] {ticker} {exit_price:,.0f}원 PNL {pnl:+.2f}% {krw:+,.0f}{held}초 보유")
log.info(f"[청산/{tag}/{llm_flag}] {ticker} {exit_price:,.0f}원 PNL {pnl:+.2f}% {krw:+,.0f}{held}초 보유")
tg(
f"{icon} <b>청산</b> {ticker} [{reason_tag}]\n"
f"{icon} <b>청산</b> {ticker} [{reason_tag}] ({llm_flag})\n"
f"진입: {pos['entry_price']:,.0f}\n"
f"청산: {exit_price:,.0f}\n"
f"PNL: <b>{pnl:+.2f}%</b> ({krw:+,.0f}원) {held}초 보유\n"
@@ -502,19 +517,38 @@ def check_filled_positions() -> None:
if _should_call_llm(pos, elapsed):
pos['llm_last_ts'] = datetime.now()
current_price = bar_list[-1]['close'] if bar_list else pos['sell_price']
new_price = get_exit_price(ticker, pos, bar_list, current_price)
if new_price is not None:
llm_sell = get_exit_price(ticker, pos, bar_list, current_price)
if llm_sell is not None and llm_sell.get('action') == 'sell':
new_price = llm_sell['price']
confidence = llm_sell.get('confidence', '?')
reason = llm_sell.get('reason', '')
status = llm_sell.get('market_status', '')
watch = llm_sell.get('watch_needed', False)
pnl_pct = (new_price - pos['entry_price']) / pos['entry_price'] * 100
cancel_order_safe(uuid)
new_uuid = submit_limit_sell(ticker, pos['qty'], new_price)
pos['sell_uuid'] = new_uuid
pos['sell_price'] = new_price
pos['llm_active'] = True
log.info(f"[매도/LLM] {ticker} 지정가 {new_price:,.0f}원 설정")
tg(
f"🤖 <b>LLM 매도 설정</b> {ticker}\n"
f"지정가: {new_price:,.0f}원 (진입 대비 {pnl_pct:+.2f}%)\n"
f"확신: {confidence} 시장: {status} 관망: {'Y' if watch else 'N'}\n"
f"LLM: {reason}"
)
continue
else:
reason = llm_sell.get('reason', 'hold') if llm_sell else '오류/무응답'
watch = llm_sell.get('watch_needed', False) if llm_sell else False
pos['llm_active'] = False
log.info(f"[매도/LLM→fallback] {ticker} {reason} → cascade 대기")
# ── Cascade fallback: LLM 실패 시에만 단계 전환 ──────────────────
if not pos.get('llm_active') and elapsed >= end:
log.info(f"[매도/cascade] {ticker} {elapsed:.0f}초 경과 → 다음 단계")
_advance_stage(ticker)