From 4f9e2c44c7b29c729f1eb47614a597e2f0110320 Mon Sep 17 00:00:00 2001 From: joungmin Date: Thu, 5 Mar 2026 21:57:19 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20LLM=20=ED=8C=90=EB=8B=A8=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=EB=A1=9C=EA=B9=85=20+=20=ED=85=94=EB=A0=88?= =?UTF-8?q?=EA=B7=B8=EB=9E=A8=20=EB=A9=94=EC=8B=9C=EC=A7=80=EC=97=90=20LLM?= =?UTF-8?q?=20=EC=9D=91=EB=8B=B5=20=ED=8F=AC=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- core/llm_advisor.py | 39 ++++++++++++++++------------ daemons/tick_trader.py | 58 +++++++++++++++++++++++++++++++++--------- 2 files changed, 69 insertions(+), 28 deletions(-) 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)