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:
joungmin
2026-03-06 20:46:47 +09:00
parent 976c53ed66
commit 6e0c4508fa
69 changed files with 5018 additions and 495 deletions

View 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