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

171
archive/core/strategy.py Normal file
View File

@@ -0,0 +1,171 @@
"""Volume Lead 전략: 10분봉 거래량 급증 + 횡보 감지 후 +THRESH% 상승 시 진입.
흐름:
1. 직전 완성 10분봉 거래량 > 로컬 LV봉(280분) 평균 × VOL_THRESH AND
QN봉(120분) 이전 종가 대비 가격 변동 < PRICE_QUIET_PCT% (횡보 중 축적)
→ 신호가(signal_price) + 거래량비율(vol_ratio) 기록
* 더 강한 vol(> 기존 sig vol_ratio)이 오면 sig_p 갱신
2. signal_price 대비 +TREND_AFTER_VOL%(4.8%) 이상 상승 시 진입
3. 신호불사: 가격이 신호가 아래로 내려가도 신호 유지 (sig_p 고정, 만료까지 대기)
4. SIGNAL_TIMEOUT_MIN(480분=8h) 초과 시 신호 초기화
거래량 임계값 + 진입 차단 (F&G 기반 3구간):
- F&G ≤ FNG_FEAR_THRESHOLD(40): VOL_THRESH_FEAR(6.0x) ← 공포/극공포
- F&G 41 ~ FNG_MAX_ENTRY(50): VOL_THRESH_NORMAL(5.0x) ← 중립
- F&G > FNG_MAX_ENTRY(50): 진입 차단 ← 탐욕/극탐욕
캔들: minute10 데이터 직접 사용 (40분봉 리샘플링 없음)
"""
from __future__ import annotations
import logging
import os
import time
import pyupbit
from .fng import get_fng
from .market import get_current_price
from .notify import notify_signal, notify_watch
logger = logging.getLogger(__name__)
# 10분봉 직접 사용 파라미터
LOCAL_VOL_CANDLES = int(os.getenv("LOCAL_VOL_CANDLES", "28")) # 로컬 vol 평균 구간 (280분)
QUIET_CANDLES = int(os.getenv("QUIET_CANDLES", "12")) # 횡보 체크 구간 (120분)
PRICE_QUIET_PCT = float(os.getenv("PRICE_QUIET_PCT", "2.0")) # 횡보 기준 (%)
TREND_AFTER_VOL = float(os.getenv("TREND_AFTER_VOL", "4.8")) # 진입 임계값 (신호가 대비 %)
SIGNAL_TIMEOUT_MIN = int(os.getenv("SIGNAL_TIMEOUT_MIN", "480")) # 신호 유효 시간 (분=8h)
# F&G 기반 거래량 임계값 + 진입 차단
VOL_THRESH_NORMAL = float(os.getenv("VOL_THRESH_NORMAL", "5.0")) # 중립 구간 (F&G 41~FNG_MAX_ENTRY)
VOL_THRESH_FEAR = float(os.getenv("VOL_THRESH_FEAR", "6.0")) # 공포/극공포 (F&G ≤ FNG_FEAR_THRESHOLD)
FNG_FEAR_THRESHOLD = int(os.getenv("FNG_FEAR_THRESHOLD", "40")) # 공포 기준 (이하 → FEAR 임계값)
FNG_MAX_ENTRY = int(os.getenv("FNG_MAX_ENTRY", "50")) # 진입 허용 최대 F&G (초과 → 차단)
# 관찰 알림 (신호 임계값에 근접했지만 미달인 종목)
WATCH_VOL_THRESH = float(os.getenv("WATCH_VOL_THRESH", "4.0")) # 관찰 시작 임계값
WATCH_COOLDOWN_MIN = int(os.getenv("WATCH_COOLDOWN_MIN", "30")) # 같은 종목 재알림 최소 간격 (분)
WATCH_VOL_JUMP = float(os.getenv("WATCH_VOL_JUMP", "0.5")) # 쿨다운 무시 vol 상승폭
# 10분봉 조회 수
_FETCH_10M = LOCAL_VOL_CANDLES + QUIET_CANDLES + 5 # 45봉
# 축적 신호 상태: ticker → {"price": float, "time": float(unix), "vol_ratio": float}
_accum_signals: dict[str, dict] = {}
# 관찰 알림 상태: ticker → {"time": float, "vol_ratio": float}
_watch_notified: dict[str, dict] = {}
def get_active_signals() -> dict[str, dict]:
"""현재 활성화된 신호 딕셔너리 반환 (fast-poll 루프용).
Returns:
{ticker: {"price": float, "time": float, "vol_ratio": float}}
"""
return dict(_accum_signals)
def should_buy(ticker: str) -> bool:
"""Volume Lead 전략 (10분봉 직접 감지).
1단계: F&G 값으로 vol 임계값 동적 설정 (≤40→6x, >40→5x)
2단계: 10분봉 거래량 급증 + QN봉 횡보 → 신호가 기록 (더 강한 vol이면 갱신)
3단계: 신호가 대비 +TREND_AFTER_VOL% 상승 확인 시 진입
신호불사: 가격이 신호가 아래로 내려가도 신호 유지 (sig_p 고정)
"""
fng = get_fng()
# F&G 탐욕/극탐욕 구간 진입 차단
if fng > FNG_MAX_ENTRY:
logger.debug(f"[F&G차단] {ticker} F&G={fng} > {FNG_MAX_ENTRY} (탐욕) → 진입 금지")
return False
# F&G 구간별 vol 임계값
vth = VOL_THRESH_FEAR if fng <= FNG_FEAR_THRESHOLD else VOL_THRESH_NORMAL
current = get_current_price(ticker)
if not current:
return False
now = time.time()
# ── 신호 만료 체크 ────────────────────────────────────
sig = _accum_signals.get(ticker)
if sig is not None:
age_min = (now - sig["time"]) / 60
if age_min > SIGNAL_TIMEOUT_MIN:
del _accum_signals[ticker]
sig = None
logger.debug(f"[축적타임아웃] {ticker} {age_min:.0f}분 경과 → 신호 초기화")
# ── 10분봉 데이터 조회 ────────────────────────────────
try:
df10 = pyupbit.get_ohlcv(ticker, interval="minute10", count=_FETCH_10M)
except Exception:
return False
if df10 is None or len(df10) < LOCAL_VOL_CANDLES + QUIET_CANDLES:
return False
# ── 거래량 비율 계산 (직전 완성봉 기준) ───────────────
vol_prev = float(df10["volume"].iloc[-2])
vol_avg = float(df10["volume"].iloc[-(LOCAL_VOL_CANDLES + 2):-2].mean())
vol_r = vol_prev / vol_avg if vol_avg > 0 else 0.0
# ── 횡보 체크 (QN봉 이전 종가 기준) ──────────────────
close_qn = float(df10["close"].iloc[-(QUIET_CANDLES + 1)])
chg = abs(current - close_qn) / close_qn * 100 if close_qn > 0 else 999.0
# ── 관찰 알림: WATCH_VOL_THRESH ≤ vol_r < vth + 횡보 ──
if WATCH_VOL_THRESH <= vol_r < vth and chg < PRICE_QUIET_PCT:
prev = _watch_notified.get(ticker)
age_min = (now - prev["time"]) / 60 if prev else 999.0
vol_jump = vol_r - prev["vol_ratio"] if prev else vol_r
if prev is None or age_min >= WATCH_COOLDOWN_MIN or vol_jump >= WATCH_VOL_JUMP:
_watch_notified[ticker] = {"time": now, "vol_ratio": vol_r}
logger.info(
f"[관찰] {ticker} vol={vol_r:.2f}x (신호기준={vth:.1f}x) + 횡보({chg:.1f}%) | F&G={fng}"
)
notify_watch(ticker, current, vol_r, vth, chg, fng=fng)
elif vol_r < WATCH_VOL_THRESH:
_watch_notified.pop(ticker, None)
# ── vol 스파이크 + 횡보 → 신호 설정/갱신 ────────────
if vol_r >= vth and chg < PRICE_QUIET_PCT:
if sig is None or vol_r > sig.get("vol_ratio", 0.0):
_accum_signals[ticker] = {"price": current, "time": now, "vol_ratio": vol_r}
sig = _accum_signals[ticker]
logger.info(
f"[축적감지] {ticker} 10m vol={vol_r:.2f}x ≥ {vth:.1f}x + 횡보({chg:.1f}%) "
f"→ 신호가={current:,.2f}원 (F&G={fng})"
)
notify_signal(ticker, current, vol_r, fng=fng)
if sig is None:
logger.debug(
f"[축적✗] {ticker} vol={vol_r:.2f}x (기준={vth:.1f}x) / 횡보={chg:.1f}%"
)
return False
# ── 진입 확인: 신호가 대비 +TREND_AFTER_VOL% 이상 ──
signal_price = sig["price"]
vol_ratio = sig["vol_ratio"]
move_pct = (current - signal_price) / signal_price * 100
age_min = (now - sig["time"]) / 60
if move_pct >= TREND_AFTER_VOL:
del _accum_signals[ticker]
logger.info(
f"[축적진입] {ticker} 신호가={signal_price:,.2f}원 → 현재={current:,.2f}"
f"(+{move_pct:.1f}% ≥ {TREND_AFTER_VOL}% | 거래량={vol_ratio:.2f}x | F&G={fng})"
)
return True
# 신호불사: 가격 하락해도 신호 유지 (sig_p 고정, 만료까지 대기)
logger.debug(
f"[축적대기] {ticker} 신호가={signal_price:,.2f} 현재={current:,.2f} "
f"({move_pct:+.1f}% / 목표={TREND_AFTER_VOL}% | "
f"거래량={vol_ratio:.2f}x | 경과={age_min:.0f}분)"
)
return False