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:
@@ -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)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user