"""sim10m.py — 10분봉 + 극단적 거래량 즉시 진입 전략 시뮬. 기존 전략 (1h봉 vol-lead) vs 신규 전략 (10분봉 + 100x 이상 거래량 즉시 진입) 비교. 데이터: 최근 45일 Upbit API """ import pickle import time import sys from pathlib import Path from datetime import datetime, timedelta import pandas as pd import pyupbit from dotenv import load_dotenv load_dotenv(dotenv_path=Path(__file__).parent / ".env") sys.path.insert(0, str(Path(__file__).parent)) # ── 파라미터 ────────────────────────────────────────────── SIM_DAYS = 45 TOP30_FILE = Path("top30_tickers.pkl") CACHE_FILE = Path("sim10m_cache.pkl") TOP_N = 20 FEE = 0.0005 TIME_STOP_MIN_PCT = 3.0 ATR_MULT = 1.5 ATR_MIN = 0.010 ATR_MAX = 0.020 # A: 기존 1h봉 A_LOCAL_VOL = 5 # 봉수 (= 5h) A_VOL_MULT = 2.0 A_QUIET_PCT = 2.0 A_THRESH = 4.8 A_SIGNAL_TO = 8 # 신호 유효 봉수 (= 8h) A_ATR_CANDLES = 5 A_TIME_STOP = 8 # 타임스탑 봉수 (= 8h) # B: 신규 10분봉 B_LOCAL_VOL = 30 # 봉수 (5h = 30 × 10min) B_VOL_MULT = 2.0 B_EXTREME_VOL = 100 # 이 이상 → 횡보 조건 면제, 다음 봉 즉시 진입 B_QUIET_CANDLES = 12 # 2h = 12봉 B_QUIET_PCT = 2.0 B_THRESH = 4.8 B_SIGNAL_TO = 48 # 신호 유효 봉수 (8h = 48봉) B_ATR_CANDLES = 30 # ATR 봉수 (5h = 30봉) B_TIME_STOP = 48 # 타임스탑 봉수 (8h = 48봉) # ── 데이터 로드 ────────────────────────────────────────── def fetch_ohlcv(ticker, interval, days): """OHLCV 과거 데이터 페이징 로드.""" target_start = datetime.now() - timedelta(days=days) all_dfs, to, prev_oldest = [], None, None while True: kwargs = dict(ticker=ticker, interval=interval, count=200) if to: kwargs["to"] = to df = pyupbit.get_ohlcv(**kwargs) if df is None or df.empty: break df.index = df.index.tz_localize(None) oldest = df.index[0] # 더 이상 과거로 못 가면 중단 (상장일에 도달) if prev_oldest is not None and oldest >= prev_oldest: all_dfs.append(df) break all_dfs.append(df) prev_oldest = oldest if oldest <= target_start: break to = oldest.strftime("%Y-%m-%d %H:%M:%S") time.sleep(0.15) if not all_dfs: return None result = pd.concat(all_dfs).sort_index().drop_duplicates() return result[result.index >= target_start] def load_data(tickers): if CACHE_FILE.exists(): print(f"캐시 로드: {CACHE_FILE}") return pickle.load(open(CACHE_FILE, "rb")) data = {"1h": {}, "10m": {}} for idx, ticker in enumerate(tickers, 1): print(f" [{idx}/{len(tickers)}] {ticker}...", end=" ", flush=True) df1h = fetch_ohlcv(ticker, "minute60", SIM_DAYS) df10m = fetch_ohlcv(ticker, "minute10", SIM_DAYS) n1h = len(df1h) if df1h is not None else 0 n10m = len(df10m) if df10m is not None else 0 print(f"1h={n1h}봉 10m={n10m}봉") if df1h is not None and n1h >= 50: data["1h"][ticker] = df1h if df10m is not None and n10m >= 200: data["10m"][ticker] = df10m pickle.dump(data, open(CACHE_FILE, "wb")) print(f"캐시 저장: {CACHE_FILE}\n") return data # ── ATR 계산 (시뮬용) ───────────────────────────────────── def calc_atr(df, buy_idx, n): sub = df.iloc[max(0, buy_idx - n - 1):buy_idx] if len(sub) < 3: return ATR_MIN try: avg = ((sub["high"] - sub["low"]) / sub["low"]).iloc[-n:].mean() return float(max(ATR_MIN, min(ATR_MAX, avg * ATR_MULT))) except Exception: return ATR_MIN # ── 포지션 시뮬 ────────────────────────────────────────── def simulate_pos(df, buy_idx, buy_price, stop_pct, time_stop_candles): peak = buy_price for i in range(buy_idx + 1, len(df)): row = df.iloc[i] ts = df.index[i] if row["high"] > peak: peak = row["high"] if row["low"] <= peak * (1 - stop_pct): sp = peak * (1 - stop_pct) pnl = (sp * (1-FEE) - buy_price * (1+FEE)) / (buy_price * (1+FEE)) * 100 return pnl > 0, sp, ts, f"트레일링({pnl:+.1f}%)", pnl pnl_now = (row["close"] - buy_price) / buy_price * 100 if (i - buy_idx) >= time_stop_candles and pnl_now < TIME_STOP_MIN_PCT: pnl = (row["close"] * (1-FEE) - buy_price * (1+FEE)) / (buy_price * (1+FEE)) * 100 return pnl > 0, row["close"], ts, "타임스탑", pnl last = df.iloc[-1]["close"] pnl = (last * (1-FEE) - buy_price * (1+FEE)) / (buy_price * (1+FEE)) * 100 return pnl > 0, last, df.index[-1], "데이터종료", pnl # ── 전략 A: 기존 1h봉 vol-lead ────────────────────────── def run_a(df): trades = [] sig_i = sig_p = None in_pos = False buy_idx = buy_price = stop_pct = None i = max(A_LOCAL_VOL + 2, 3) while i < len(df): if in_pos: is_win, sp, sdt, reason, pnl = simulate_pos(df, buy_idx, buy_price, stop_pct, A_TIME_STOP) next_i = next((j for j in range(i, len(df)) if df.index[j] > sdt), len(df)) trades.append((is_win, pnl, df.index[buy_idx], sdt, "일반")) in_pos = False sig_i = sig_p = None i = next_i continue close = df.iloc[i]["close"] chg_2h = abs(close - df.iloc[i-2]["close"]) / df.iloc[i-2]["close"] * 100 quiet = chg_2h < A_QUIET_PCT vol_p = df.iloc[i-1]["volume"] vol_avg = df.iloc[i-A_LOCAL_VOL-1:i-1]["volume"].mean() spike = vol_avg > 0 and vol_p >= vol_avg * A_VOL_MULT if quiet and spike: if sig_i is None: sig_i, sig_p = i, close else: if sig_i is not None and close < sig_p: sig_i = sig_p = None if sig_i is not None and (i - sig_i) > A_SIGNAL_TO: sig_i = sig_p = None if sig_i is not None and (close - sig_p) / sig_p * 100 >= A_THRESH: in_pos = True; buy_idx = i; buy_price = close stop_pct = calc_atr(df, i, A_ATR_CANDLES) sig_i = sig_p = None i += 1 return trades # ── 전략 B: 10분봉 + 극단적 거래량 즉시 진입 ──────────── def run_b(df): # trade tuple: (is_win, pnl, buy_dt, sell_dt, entry_type) # entry_type: '일반' | '극단' trades = [] sig_i = sig_p = None extreme_pending = False in_pos = False buy_idx = buy_price = stop_pct = None i = max(B_LOCAL_VOL + 2, B_QUIET_CANDLES + 1) while i < len(df): if in_pos: is_win, sp, sdt, reason, pnl = simulate_pos(df, buy_idx, buy_price, stop_pct, B_TIME_STOP) next_i = next((j for j in range(i, len(df)) if df.index[j] > sdt), len(df)) trades.append((is_win, pnl, df.index[buy_idx], sdt, entry_type)) in_pos = False sig_i = sig_p = None extreme_pending = False i = next_i continue close = df.iloc[i]["close"] vol_p = df.iloc[i-1]["volume"] vol_avg = df.iloc[i-B_LOCAL_VOL-1:i-1]["volume"].mean() vol_r = vol_p / vol_avg if vol_avg > 0 else 0 # 극단적 거래량 → 다음 봉 진입 대기 설정 if not extreme_pending and vol_r >= B_EXTREME_VOL: extreme_pending = True i += 1 continue # 극단적 거래량 다음 봉 → 즉시 진입 if extreme_pending: in_pos = True; buy_idx = i; buy_price = close stop_pct = calc_atr(df, i, B_ATR_CANDLES) entry_type = "극단" extreme_pending = False sig_i = sig_p = None i += 1 continue # 일반 vol-lead close_2h = df.iloc[i - B_QUIET_CANDLES]["close"] chg_2h = abs(close - close_2h) / close_2h * 100 quiet = chg_2h < B_QUIET_PCT spike = vol_r >= B_VOL_MULT if quiet and spike: if sig_i is None: sig_i, sig_p = i, close else: if sig_i is not None and close < sig_p: sig_i = sig_p = None if sig_i is not None and (i - sig_i) > B_SIGNAL_TO: sig_i = sig_p = None if sig_i is not None and (close - sig_p) / sig_p * 100 >= B_THRESH: in_pos = True; buy_idx = i; buy_price = close stop_pct = calc_atr(df, i, B_ATR_CANDLES) entry_type = "일반" sig_i = sig_p = None i += 1 return trades # ── 통계 계산 ───────────────────────────────────────────── def calc_stats(trades): if not trades: return {"n": 0, "wr": 0.0, "cum": 0.0, "dd": 0.0} wins = sum(1 for t in trades if t[0]) cum = peak = dd = 0.0 for t in sorted(trades, key=lambda x: x[2]): cum += t[1] peak = max(peak, cum) dd = max(dd, peak - cum) return {"n": len(trades), "wr": wins / len(trades) * 100, "cum": cum, "dd": dd} # ── 메인 ───────────────────────────────────────────────── def main(): top30 = pickle.load(open(TOP30_FILE, "rb")) tickers = top30[:TOP_N] print(f"{'='*60}") print(f"10분봉 전략 시뮬 | 최근 {SIM_DAYS}일 | {TOP_N}종목") print(f" A: 기존 1h봉 vol-lead") print(f" B: 10분봉 vol-lead + 극단거래량({B_EXTREME_VOL}x) 즉시 진입") print(f"{'='*60}\n") print(f"데이터 로드 중...") data = load_data(tickers) v1h = [t for t in tickers if t in data["1h"] and len(data["1h"][t]) >= 50] v10m = [t for t in tickers if t in data["10m"] and len(data["10m"][t]) >= 200] print(f"유효: 1h={len(v1h)}종목 / 10m={len(v10m)}종목\n") # 전략 A a_all = [] for t in v1h: a_all.extend(run_a(data["1h"][t])) # 전략 B b_all = [] for t in v10m: b_all.extend(run_b(data["10m"][t])) b_extreme = [t for t in b_all if t[4] == "극단"] b_normal = [t for t in b_all if t[4] == "일반"] sa = calc_stats(a_all) sb = calc_stats(b_all) sbe = calc_stats(b_extreme) sbn = calc_stats(b_normal) print(f"{'='*65}") print(f"{'전략':18} {'거래수':>6} {'승률':>6} {'누적PnL%':>10} {'최대낙폭%':>10}") print(f"{'─'*65}") print(f"{'A (1h vol-lead)':18} {sa['n']:>6}건 {sa['wr']:>5.1f}% {sa['cum']:>+9.2f}% {-sa['dd']:>+9.2f}%") print(f"{'B 전체 (10m)':18} {sb['n']:>6}건 {sb['wr']:>5.1f}% {sb['cum']:>+9.2f}% {-sb['dd']:>+9.2f}%") print(f"{' └ 극단거래량':18} {sbe['n']:>6}건 {sbe['wr']:>5.1f}% {sbe['cum']:>+9.2f}% {-sbe['dd']:>+9.2f}%") print(f"{' └ 일반vol-lead':18} {sbn['n']:>6}건 {sbn['wr']:>5.1f}% {sbn['cum']:>+9.2f}% {-sbn['dd']:>+9.2f}%") print(f"{'='*65}") # 극단 거래량 진입 상세 if b_extreme: print(f"\n── 극단거래량 진입 상세 ({len(b_extreme)}건) ─────────────────────") print(f" {'매수시각':<18} {'PnL%':>7} {'청산'}") for t in sorted(b_extreme, key=lambda x: x[2])[:20]: mark = "✅" if t[0] else "❌" print(f" {str(t[2])[:16]:<18} {t[1]:>+6.1f}% {mark} {t[4]}") if __name__ == "__main__": main()