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:
@@ -19,20 +19,29 @@ TIME_STOP_HOURS = float(os.getenv("TIME_STOP_HOURS", "24"))
|
||||
TIME_STOP_MIN_GAIN_PCT = float(os.getenv("TIME_STOP_MIN_GAIN_PCT", "3"))
|
||||
|
||||
# ATR 기반 적응형 트레일링 스탑 파라미터
|
||||
ATR_CANDLES = 5 # 최근 N개 1h봉으로 자연 진폭 계산
|
||||
ATR_CANDLES = 7 # 최근 N개 40분봉으로 자연 진폭 계산 (≈5h, int(5*60/40)=7)
|
||||
ATR_MULT = 1.5 # 평균 진폭 × 배수 = 스탑 임계값
|
||||
ATR_MIN_STOP = 0.010 # 최소 스탑 1.0% (너무 좁아지는 거 방지)
|
||||
ATR_MAX_STOP = 0.020 # 최대 스탑 2.0% (너무 넓어지는 거 방지)
|
||||
|
||||
# ATR 캐시: 종목별 (스탑비율, 계산시각) — 10분마다 갱신
|
||||
# ATR 캐시: 종목별 (스탑비율, 계산시각) — 40분마다 갱신
|
||||
_atr_cache: dict[str, tuple[float, float]] = {}
|
||||
_ATR_CACHE_TTL = 600 # 10분
|
||||
_ATR_CACHE_TTL = 2400 # 40분
|
||||
|
||||
|
||||
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"])
|
||||
)
|
||||
|
||||
|
||||
def _get_adaptive_stop(ticker: str) -> float:
|
||||
"""최근 ATR_CANDLES개 1h봉 평균 진폭 × ATR_MULT 로 적응형 스탑 비율 반환.
|
||||
"""최근 ATR_CANDLES개 40분봉 평균 진폭 × ATR_MULT 로 적응형 스탑 비율 반환.
|
||||
|
||||
캐시(10분)를 활용해 API 호출 최소화.
|
||||
캐시(40분)를 활용해 API 호출 최소화.
|
||||
계산 실패 시 ATR_MIN_STOP 반환.
|
||||
"""
|
||||
now = time.time()
|
||||
@@ -41,8 +50,12 @@ def _get_adaptive_stop(ticker: str) -> float:
|
||||
return cached[0]
|
||||
|
||||
try:
|
||||
df = pyupbit.get_ohlcv(ticker, interval="minute60", count=ATR_CANDLES + 2)
|
||||
if df is None or len(df) < ATR_CANDLES:
|
||||
fetch_n = (ATR_CANDLES + 2) * 4 # 40분봉 N개 = 10분봉 N*4개
|
||||
df10 = pyupbit.get_ohlcv(ticker, interval="minute10", count=fetch_n)
|
||||
if df10 is None or df10.empty:
|
||||
return ATR_MIN_STOP
|
||||
df = _resample_40m(df10)
|
||||
if len(df) < ATR_CANDLES:
|
||||
return ATR_MIN_STOP
|
||||
ranges = (df["high"] - df["low"]) / df["low"]
|
||||
avg_range = ranges.iloc[-ATR_CANDLES:].mean()
|
||||
|
||||
Reference in New Issue
Block a user