feat: switch vol-lead strategy from 1h to 40min candles
Simulation sweep showed 40min candles outperform 1h: - 40min: 91 trades, 48.4% WR, +119% PnL, -11% DD - 60min: 65 trades, 50.8% WR, +88% PnL, -12% DD Changes: - strategy.py: fetch minute10, resample to 40min for vol spike detection - LOCAL_VOL_CANDLES=7 (was LOCAL_VOL_HOURS=5, 5h/40min = 7 candles) - monitor.py: ATR calculated from 40min candles - ATR_CANDLES=7 (was 5, now 5h in 40min units) - ATR_CACHE_TTL=2400s (was 600s, aligned to 40min candle) - interval_sweep.py: new interval comparison tool (10/20/30/40/50/60min) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
"""Volume Lead 전략: 거래량 축적(급증+횡보) 감지 후 +TREND_AFTER_VOL% 상승 시 선진입.
|
||||
|
||||
흐름:
|
||||
1. 직전 1h 거래량 > 로컬 5h 평균 × VOL_MULT AND
|
||||
1. 직전 40분봉 거래량 > 로컬 5h(7봉) 평균 × VOL_MULT AND
|
||||
2h 가격 변동 < PRICE_QUIET_PCT% (횡보 중 축적)
|
||||
→ 신호가(signal_price) 기록
|
||||
2. signal_price 대비 +TREND_AFTER_VOL% 이상 상승 시 진입
|
||||
3. SIGNAL_TIMEOUT_H 내 임계값 미달 또는 신호가 이하 하락 시 신호 초기화
|
||||
|
||||
캔들: minute10 데이터를 40분봉으로 리샘플링하여 사용
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -29,25 +31,41 @@ TREND_AFTER_VOL = float(os.getenv("TREND_AFTER_VOL", "5.0")) # 진입 임계
|
||||
SIGNAL_TIMEOUT_H = float(os.getenv("SIGNAL_TIMEOUT_H", "8.0")) # 신호 유효 시간 (h)
|
||||
|
||||
# 거래량 파라미터
|
||||
LOCAL_VOL_HOURS = 5 # 로컬 기준 시간 (h)
|
||||
LOCAL_VOL_CANDLES = 7 # 5h를 40분봉으로 환산 (int(5 * 60/40) = 7)
|
||||
VOLUME_MULTIPLIER = float(os.getenv("VOLUME_MULTIPLIER", "2.0"))
|
||||
|
||||
# 40분봉 리샘플링 파라미터
|
||||
_CANDLE_MIN = 40
|
||||
_FETCH_10M = (LOCAL_VOL_CANDLES + 3) * (_CANDLE_MIN // 10) # 40 개의 10분봉
|
||||
|
||||
|
||||
def _resample_40m(df):
|
||||
"""minute10 DataFrame → 40분봉으로 리샘플링."""
|
||||
return (
|
||||
df.resample("40min")
|
||||
.agg({"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"})
|
||||
.dropna(subset=["close"])
|
||||
)
|
||||
|
||||
# 축적 신호 상태: ticker → {"price": float, "time": float(unix)}
|
||||
_accum_signals: dict[str, dict] = {}
|
||||
|
||||
|
||||
def _check_vol_spike(ticker: str, vol_mult: float) -> bool:
|
||||
"""직전 완성 1h 거래량이 로컬 5h 평균의 vol_mult 배 이상인지 확인."""
|
||||
fetch_count = LOCAL_VOL_HOURS + 3
|
||||
"""직전 완성 40분봉 거래량이 로컬 5h(7봉) 평균의 vol_mult 배 이상인지 확인."""
|
||||
try:
|
||||
df = pyupbit.get_ohlcv(ticker, interval="minute60", count=fetch_count)
|
||||
df10 = pyupbit.get_ohlcv(ticker, interval="minute10", count=_FETCH_10M)
|
||||
except Exception:
|
||||
return False
|
||||
if df is None or len(df) < LOCAL_VOL_HOURS + 1:
|
||||
if df10 is None or len(df10) < _CANDLE_MIN // 10 * 2:
|
||||
return False
|
||||
|
||||
recent_vol = df["volume"].iloc[-2] # 직전 완성된 1h 봉
|
||||
local_avg = df["volume"].iloc[-(LOCAL_VOL_HOURS + 1):-2].mean() # 이전 5h 평균
|
||||
df = _resample_40m(df10)
|
||||
if len(df) < LOCAL_VOL_CANDLES + 1:
|
||||
return False
|
||||
|
||||
recent_vol = df["volume"].iloc[-2] # 직전 완성된 40분봉
|
||||
local_avg = df["volume"].iloc[-(LOCAL_VOL_CANDLES + 1):-2].mean() # 이전 7봉(≈5h) 평균
|
||||
if local_avg <= 0:
|
||||
return False
|
||||
|
||||
@@ -55,7 +73,7 @@ def _check_vol_spike(ticker: str, vol_mult: float) -> bool:
|
||||
result = ratio >= vol_mult
|
||||
if result:
|
||||
logger.debug(
|
||||
f"[거래량↑] {ticker} 1h={recent_vol:.0f} / 5h평균={local_avg:.0f} ({ratio:.2f}x ≥ {vol_mult}x)"
|
||||
f"[거래량↑] {ticker} 40m={recent_vol:.0f} / 5h평균={local_avg:.0f} ({ratio:.2f}x ≥ {vol_mult}x)"
|
||||
)
|
||||
else:
|
||||
logger.debug(
|
||||
@@ -115,11 +133,11 @@ def should_buy(ticker: str) -> bool:
|
||||
)
|
||||
# 거래량 비율 계산 후 알림 전송
|
||||
try:
|
||||
fetch_count = LOCAL_VOL_HOURS + 3
|
||||
df_h = pyupbit.get_ohlcv(ticker, interval="minute60", count=fetch_count)
|
||||
if df_h is not None and len(df_h) >= LOCAL_VOL_HOURS + 1:
|
||||
df10 = pyupbit.get_ohlcv(ticker, interval="minute10", count=_FETCH_10M)
|
||||
df_h = _resample_40m(df10) if df10 is not None else None
|
||||
if df_h is not None and len(df_h) >= LOCAL_VOL_CANDLES + 1:
|
||||
recent_vol = df_h["volume"].iloc[-2]
|
||||
local_avg = df_h["volume"].iloc[-(LOCAL_VOL_HOURS + 1):-2].mean()
|
||||
local_avg = df_h["volume"].iloc[-(LOCAL_VOL_CANDLES + 1):-2].mean()
|
||||
ratio = recent_vol / local_avg if local_avg > 0 else 0
|
||||
notify_signal(ticker, current, ratio)
|
||||
except Exception:
|
||||
|
||||
Reference in New Issue
Block a user