refactor: MVC 구조 분리 + 미사용 파일 archive 정리
- 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>
This commit is contained in:
110
archive/core/market_regime.py
Normal file
110
archive/core/market_regime.py
Normal file
@@ -0,0 +1,110 @@
|
||||
"""시장 레짐(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
|
||||
Reference in New Issue
Block a user