fix: 포지션 한도 초과 방지 + LLM 호출 최적화
- VOL_MIN 4→8 복원 (시그널 빈도 과다) - process_signal: LLM 호출 전/후 포지션 한도 재확인 - check_pending_buys: 체결 시점 한도 초과면 즉시 취소 - LLM tool 중복 호출 방지 (같은 tool+args → 캐시 응답) - 모든 tool 호출 완료 시 tool 제거해 강제 텍스트 응답 - max_rounds 8→5 축소 - 재시작 시 Upbit 잔고 기반 포지션 자동 복구 - LLM 모델: google/gemini-2.5-flash로 전환 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -43,7 +43,7 @@ TICKERS = [
|
||||
BAR_SEC = 20 # 봉 주기 (초)
|
||||
VOL_LOOKBACK = 61 # 거래량 평균 기준 봉 수
|
||||
ATR_LOOKBACK = 28 # ATR 계산 봉 수
|
||||
VOL_MIN = 4.0 # 거래량 배수 임계값 (완화 — LLM이 최종 판단)
|
||||
VOL_MIN = 8.0 # 거래량 배수 임계값
|
||||
BUY_TIMEOUT = 60 # 지정가 매수 미체결 타임아웃 (초)
|
||||
|
||||
MAX_POS = int(os.environ.get('MAX_POSITIONS', 3))
|
||||
@@ -296,6 +296,11 @@ def process_signal(sig: dict) -> None:
|
||||
cur_price = sig['price']
|
||||
vol_ratio = sig['vol_ratio']
|
||||
|
||||
# LLM 호출 전 포지션 수 재확인 (동시 진행 방지)
|
||||
if len(positions) + len(pending_buys) >= MAX_POS:
|
||||
log.info(f"[시그널] {ticker} 포지션 한도 도달 → 스킵")
|
||||
return
|
||||
|
||||
log.info(f"[시그널] {ticker} {cur_price:,.0f}원 vol {vol_ratio:.1f}x → LLM 판단 요청")
|
||||
|
||||
llm_result = get_entry_price(
|
||||
@@ -319,6 +324,11 @@ def process_signal(sig: dict) -> None:
|
||||
)
|
||||
return
|
||||
|
||||
# LLM 호출 후 포지션 수 재확인
|
||||
if len(positions) + len(pending_buys) >= MAX_POS:
|
||||
log.info(f"[매수/LLM] {ticker} → 승인됐으나 포지션 한도 도달 → 스킵")
|
||||
return
|
||||
|
||||
buy_price = _round_price(llm_result['price'])
|
||||
confidence = llm_result.get('confidence', '?')
|
||||
reason = llm_result.get('reason', '')
|
||||
@@ -359,13 +369,19 @@ def process_signal(sig: dict) -> None:
|
||||
|
||||
|
||||
def check_pending_buys() -> None:
|
||||
"""지정가 매수 주문 체결 확인. 체결 시 포지션 등록, 타임아웃 시 취소."""
|
||||
"""지정가 매수 주문 체결 확인. 체결 시 포지션 등록, 타임아웃/한도초과 시 취소."""
|
||||
for ticker in list(pending_buys.keys()):
|
||||
pb = pending_buys[ticker]
|
||||
elapsed = (datetime.now() - pb['ts']).total_seconds()
|
||||
|
||||
# 포지션 한도 초과 시 미체결 주문 즉시 취소
|
||||
if len(positions) >= MAX_POS:
|
||||
cancel_order_safe(pb['uuid'])
|
||||
log.info(f"[매수취소] {ticker} 포지션 한도({MAX_POS}) 도달 → 취소")
|
||||
del pending_buys[ticker]
|
||||
continue
|
||||
|
||||
if SIM_MODE:
|
||||
# SIM: 최근 봉 저가가 매수 지정가 이하이면 체결
|
||||
bar_list = list(bars.get(ticker, []))
|
||||
if bar_list and bar_list[-1]['low'] <= pb['price']:
|
||||
log.info(f"[SIM 매수체결] {ticker} {pb['price']:,.0f}원")
|
||||
@@ -616,6 +632,46 @@ def preload_bars() -> None:
|
||||
log.info(f"[사전적재] 완료 {loaded}/{len(TICKERS)} 티커")
|
||||
|
||||
|
||||
def restore_positions() -> None:
|
||||
"""Upbit 잔고에서 보유 종목을 positions에 복구 (재시작 대응)."""
|
||||
if SIM_MODE:
|
||||
return
|
||||
try:
|
||||
balances = upbit_client.get_balances()
|
||||
for b in balances:
|
||||
currency = b.get('currency', '')
|
||||
bal = float(b.get('balance', 0))
|
||||
avg = float(b.get('avg_buy_price', 0))
|
||||
if currency == 'KRW' or bal <= 0 or avg <= 0:
|
||||
continue
|
||||
ticker = f'KRW-{currency}'
|
||||
if ticker not in TICKERS:
|
||||
continue
|
||||
if ticker in positions:
|
||||
continue
|
||||
# cascade ① 지정가 매도 즉시 제출
|
||||
_, _, lr, stag = CASCADE_STAGES[0]
|
||||
target = avg * (1 + lr)
|
||||
sell_uuid = submit_limit_sell(ticker, bal, target)
|
||||
positions[ticker] = {
|
||||
'entry_price': avg,
|
||||
'entry_ts': datetime.now() - timedelta(seconds=LLM_MIN_ELAPSED), # LLM 즉시 활성
|
||||
'running_peak': avg,
|
||||
'qty': bal,
|
||||
'stage': 0,
|
||||
'sell_uuid': sell_uuid,
|
||||
'sell_price': target,
|
||||
'llm_last_ts': None,
|
||||
'llm_active': False,
|
||||
}
|
||||
log.info(f"[복구] {ticker} 수량:{bal:.6f} 매수평균:{avg:,.0f}원")
|
||||
tg(f"♻️ <b>포지션 복구</b> {ticker}\n매수평균: {avg:,.0f}원 수량: {bal:.6f}")
|
||||
if positions:
|
||||
log.info(f"[복구] 총 {len(positions)}개 포지션 복구됨")
|
||||
except Exception as e:
|
||||
log.warning(f"[복구] 잔고 조회 실패: {e}")
|
||||
|
||||
|
||||
def main():
|
||||
mode = "🔴 실거래" if not SIM_MODE else "🟡 시뮬레이션"
|
||||
log.info(f"=== tick_trader 시작 ({mode}) ===")
|
||||
@@ -636,6 +692,7 @@ def main():
|
||||
)
|
||||
|
||||
preload_bars()
|
||||
restore_positions()
|
||||
|
||||
t = threading.Thread(target=finalize_bars, daemon=True)
|
||||
t.start()
|
||||
|
||||
Reference in New Issue
Block a user