diff --git a/core/llm_advisor.py b/core/llm_advisor.py
index ebf11a9..6c65e38 100644
--- a/core/llm_advisor.py
+++ b/core/llm_advisor.py
@@ -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
diff --git a/daemons/tick_trader.py b/daemons/tick_trader.py
index 9e3f5c4..6c0a719 100644
--- a/daemons/tick_trader.py
+++ b/daemons/tick_trader.py
@@ -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"🔔 시그널 {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"⏭️ 매수 스킵 {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"⏭️ 매수 스킵 {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"📥 지정가 매수 제출 {ticker}\n"
- f"가격: {buy_price:,.0f}원 수량: {qty:.6f}\n"
- f"볼륨: {vol_ratio:.1f}x\n"
+ f"📥 지정가 매수 {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} 청산 {ticker} [{reason_tag}]\n"
+ f"{icon} 청산 {ticker} [{reason_tag}] ({llm_flag})\n"
f"진입: {pos['entry_price']:,.0f}원\n"
f"청산: {exit_price:,.0f}원\n"
f"PNL: {pnl:+.2f}% ({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"🤖 LLM 매도 설정 {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)