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:
139
core/signal.py
Normal file
139
core/signal.py
Normal 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,
|
||||
}
|
||||
Reference in New Issue
Block a user