- tick_trader.py를 Controller로 축소, 로직을 3개 모듈로 분리: - core/signal.py: 시그널 감지, 지표 계산 (calc_vr, calc_atr, detect_signal) - core/order.py: Upbit 주문 실행 (매수/매도/취소/조회) - core/position_manager.py: 포지션 관리, DB sync, 복구, 청산 조건 - type hints, Google docstring, 구체적 예외 타입 적용 - 50줄 초과 함수 분리 (process_signal, restore_positions) - 미사용 파일 58개 archive/ 폴더로 이동 - README.md 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
111 lines
3.2 KiB
Python
111 lines
3.2 KiB
Python
"""시장 레짐(Bull/Neutral/Bear) 판단.
|
|
|
|
BTC·ETH·SOL·XRP 가중 평균 2h 추세로 레짐을 결정하고
|
|
매수 조건 파라미터(trend_pct, vol_mult)를 동적으로 반환한다.
|
|
계산된 현재가는 price_history DB에 저장해 재활용한다.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import time
|
|
|
|
import pyupbit
|
|
|
|
from .price_db import get_price_n_hours_ago, insert_prices
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# 대장 코인 가중치
|
|
LEADERS: dict[str, float] = {
|
|
"KRW-BTC": 0.40,
|
|
"KRW-ETH": 0.30,
|
|
"KRW-SOL": 0.15,
|
|
"KRW-XRP": 0.15,
|
|
}
|
|
TREND_HOURS = 2 # 2h 추세 기준
|
|
|
|
BULL_THRESHOLD = 1.5 # score ≥ 1.5% → Bull
|
|
BEAR_THRESHOLD = -0.5 # score < -0.5% → Bear
|
|
|
|
# 레짐별 매수 조건 파라미터
|
|
REGIME_PARAMS: dict[str, dict] = {
|
|
"bull": {"trend_pct": 3.0, "vol_mult": 1.5, "emoji": "🟢"},
|
|
"neutral": {"trend_pct": 5.0, "vol_mult": 2.0, "emoji": "🟡"},
|
|
"bear": {"trend_pct": 8.0, "vol_mult": 3.5, "emoji": "🔴"},
|
|
}
|
|
|
|
# 10분 캐시 (스캔 루프마다 API 호출 방지)
|
|
_cache: dict = {}
|
|
_cache_ts: float = 0.0
|
|
_CACHE_TTL = 600
|
|
|
|
|
|
def get_regime() -> dict:
|
|
"""현재 시장 레짐 반환.
|
|
|
|
Returns:
|
|
{
|
|
'name': 'bull' | 'neutral' | 'bear',
|
|
'score': float, # 가중 평균 2h 추세(%)
|
|
'trend_pct': float, # 매수 추세 임계값
|
|
'vol_mult': float, # 거래량 배수 임계값
|
|
'emoji': str,
|
|
}
|
|
"""
|
|
global _cache, _cache_ts
|
|
if _cache and (time.time() - _cache_ts) < _CACHE_TTL:
|
|
return _cache
|
|
|
|
score = 0.0
|
|
current_prices: dict[str, float] = {}
|
|
|
|
for ticker, weight in LEADERS.items():
|
|
try:
|
|
current = pyupbit.get_current_price(ticker)
|
|
if not current:
|
|
continue
|
|
current_prices[ticker] = current
|
|
|
|
# DB에서 2h 전 가격 조회 → 없으면 API 캔들로 대체
|
|
past = get_price_n_hours_ago(ticker, TREND_HOURS)
|
|
if past is None:
|
|
df = pyupbit.get_ohlcv(ticker, interval="minute60", count=4)
|
|
if df is not None and len(df) >= 3:
|
|
past = float(df["close"].iloc[-3])
|
|
|
|
if past:
|
|
trend = (current - past) / past * 100
|
|
score += trend * weight
|
|
logger.debug(f"[레짐] {ticker} {trend:+.2f}% (기여 {trend*weight:+.3f})")
|
|
|
|
except Exception as e:
|
|
logger.warning(f"[레짐] {ticker} 오류: {e}")
|
|
|
|
# 현재가 DB 저장 (다음 레짐 계산 및 추세 판단에 재활용)
|
|
if current_prices:
|
|
try:
|
|
insert_prices(current_prices)
|
|
except Exception as e:
|
|
logger.warning(f"[레짐] 가격 저장 오류: {e}")
|
|
|
|
# 레짐 결정
|
|
if score >= BULL_THRESHOLD:
|
|
name = "bull"
|
|
elif score < BEAR_THRESHOLD:
|
|
name = "bear"
|
|
else:
|
|
name = "neutral"
|
|
|
|
params = REGIME_PARAMS[name]
|
|
result = {"name": name, "score": round(score, 3), **params}
|
|
|
|
logger.info(
|
|
f"[레짐] score={score:+.3f}% → {params['emoji']} {name.upper()} "
|
|
f"(TREND≥{params['trend_pct']}% / VOL≥{params['vol_mult']}x)"
|
|
)
|
|
|
|
_cache = result
|
|
_cache_ts = time.time()
|
|
return result
|