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

139
core/signal.py Normal file
View File

@@ -0,0 +1,139 @@
"""시그널 감지 + 지표 계산 모듈.
20초봉 데이터에서 양봉 + 거래량 + 사전 필터를 적용하여
매수 시그널 후보를 반환한다.
"""
from __future__ import annotations
import logging
from typing import Optional
log = logging.getLogger(__name__)
def calc_vr(bar_list: list[dict], idx: int, lookback: int = 61) -> float:
"""거래량비(Volume Ratio) 계산. 상위 10% 트리밍.
Args:
bar_list: 봉 리스트.
idx: 현재 봉 인덱스.
lookback: 기준 봉 수.
Returns:
현재 봉 거래량 / trimmed mean 비율.
"""
start = max(0, idx - lookback)
end = max(0, idx - 2)
baseline = sorted(bar_list[i]['volume'] for i in range(start, end))
if not baseline:
return 0.0
trim = max(1, len(baseline) // 10)
trimmed = baseline[:len(baseline) - trim]
if not trimmed:
return 0.0
avg = sum(trimmed) / len(trimmed)
return bar_list[idx]['volume'] / avg if avg > 0 else 0.0
def calc_atr(bar_list: list[dict], lookback: int = 28) -> float:
"""ATR(Average True Range) 비율 계산.
Args:
bar_list: 봉 리스트.
lookback: ATR 계산 봉 수.
Returns:
ATR / 직전 종가 비율 (0~1 범위).
"""
if len(bar_list) < lookback + 2:
return 0.0
trs = []
for i in range(-lookback - 1, -1):
b = bar_list[i]
bp = bar_list[i - 1]
tr = max(
b['high'] - b['low'],
abs(b['high'] - bp['close']),
abs(b['low'] - bp['close']),
)
trs.append(tr)
prev_close = bar_list[-2]['close']
return (sum(trs) / len(trs)) / prev_close if prev_close > 0 else 0.0
def detect_signal(
ticker: str,
bar_list: list[dict],
*,
vol_min: float = 5.0,
vol_lookback: int = 61,
vol_krw_min: float = 5_000_000,
spread_min: float = 0.3,
) -> Optional[dict]:
"""양봉 + 거래량 + 사전 필터 3종을 적용하여 시그널 후보를 반환.
Args:
ticker: 종목 코드.
bar_list: 봉 리스트 (list로 변환된 deque).
vol_min: 최소 거래량 배수.
vol_lookback: 거래량 평균 기준 봉 수.
vol_krw_min: 최소 거래대금 (원).
spread_min: 횡보 필터 최소 변동폭 (%).
Returns:
시그널 dict 또는 None.
"""
n = len(bar_list)
if n < vol_lookback + 5:
return None
b = bar_list[-1]
if b['close'] <= b['open']:
return None
vr = calc_vr(bar_list, n - 1, lookback=vol_lookback)
if vr < vol_min:
return None
bar_krw = b['close'] * b['volume']
if bar_krw < vol_krw_min:
return None
# 1) 횡보 필터: 최근 15봉 변동폭 < 0.3%
recent = bar_list[-15:]
period_high = max(x['high'] for x in recent)
period_low = min(x['low'] for x in recent)
if period_low > 0:
spread_pct = (period_high - period_low) / period_low * 100
if spread_pct < spread_min:
log.debug(f"[필터/횡보] {ticker} 15봉 변동 {spread_pct:.2f}% -> 스킵")
return None
# 2) 고점 필터: 30분 구간 90%+ 위치 & 변동 1%+
long_bars = bar_list[-90:]
long_high = max(x['high'] for x in long_bars)
long_low = min(x['low'] for x in long_bars)
if long_high > long_low:
pos_in_range = (b['close'] - long_low) / (long_high - long_low)
move_pct = (long_high - long_low) / long_low * 100
if pos_in_range > 0.9 and move_pct > 1.0:
log.debug(f"[필터/고점] {ticker} 구간 {pos_in_range:.0%} 위치, 변동 {move_pct:.1f}% -> 스킵")
return None
# 3) 연속 양봉 필터: 직전 2봉 이상 연속 양봉
prev_greens = 0
for k in range(len(bar_list) - 2, max(len(bar_list) - 12, 0), -1):
if bar_list[k]['close'] > bar_list[k]['open']:
prev_greens += 1
else:
break
if prev_greens < 2:
log.debug(f"[필터/양봉] {ticker} 직전 연속양봉 {prev_greens}개 < 2 -> 스킵")
return None
return {
'ticker': ticker,
'price': b['close'],
'vol_ratio': vr,
'bar_list': bar_list,
}