#!/usr/bin/env python3 """적응형 전략 파라미터 스윕 -- 최적 조합 탐색 run_backtest.py의 ADAPTIVE_MODES 블록을 동적으로 교체하여 6가지 파라미터 조합의 백테스트를 자동으로 실행하고 비교합니다. 실행: python backend/run_backtest_sweep.py 동작 방식: 1. run_backtest.py의 ADAPTIVE_MODES 블록을 텍스트 치환으로 교체 2. 임시 파일(_tmp_backtest.py)에 저장 후 subprocess로 실행 3. stdout에서 성과 지표(수익률, MDD, 승률 등)를 정규식 파싱 4. 6가지 조합 실행 후 최종 비교표 출력 조합 설명: A: 공격적 — 넓은 RSI 진입 + 큰 포지션 + 넓은 손절 B: 균형 — 기존 v5 설정 (기본) C: Crisis도 매매 — Crisis 모드에서 소량 매매 허용 D: 넓은 손절 + 이익 극대화 — 손절 폭 확대 + RSI 익절 지연 E: 빠른 익절 + 타이트 손절 — 짧은 보유 + 빠른 청산 F: 하이브리드 — 공격적 진입 + 보수적 청산 주의사항: - run_backtest.py의 ADAPTIVE_MODES 블록 형식이 변경되면 스윕이 실패할 수 있음 - 각 조합당 약 30~120초 소요 (총 3~12분) """ import subprocess, json, sys, os, copy, time # run_backtest.py를 import하지 않고 파라미터만 바꿔서 반복 실행 # 방법: run_backtest.py의 ADAPTIVE_MODES를 동적으로 오버라이드 BASE_DIR = os.path.dirname(os.path.abspath(__file__)) BACKTEST_SCRIPT = os.path.join(BASE_DIR, "run_backtest.py") # 6가지 조합 정의 CONFIGS = { "A: 공격적 (v2 수준)": { "aggressive": {"rsi_entry": 50, "bear_rsi_entry": 40, "bb_pct_b_scale": 0.50, "position_size": 0.08, "max_holdings": 18, "stop_loss": -9, "stop_loss_vol": -13, "atr_stop_mult": 3.0, "atr_trail_mult": 2.0, "rsi_partial": 60, "rsi_full": 75}, "normal": {"rsi_entry": 46, "bear_rsi_entry": 35, "bb_pct_b_scale": 0.40, "position_size": 0.06, "max_holdings": 14, "stop_loss": -7, "stop_loss_vol": -11, "atr_stop_mult": 2.5, "atr_trail_mult": 1.6, "rsi_partial": 55, "rsi_full": 70}, "defensive": {"rsi_entry": 38, "bear_rsi_entry": 28, "bb_pct_b_scale": 0.25, "position_size": 0.04, "max_holdings": 10, "stop_loss": -6, "stop_loss_vol": -9, "atr_stop_mult": 2.2, "atr_trail_mult": 1.3, "rsi_partial": 50, "rsi_full": 63}, "crisis": {"rsi_entry": 30, "bear_rsi_entry": 22, "bb_pct_b_scale": 0.15, "position_size": 0.02, "max_holdings": 5, "stop_loss": -4, "stop_loss_vol": -6, "atr_stop_mult": 1.5, "atr_trail_mult": 1.0, "rsi_partial": 45, "rsi_full": 55}, }, "B: 균형 (기존 v5)": { "aggressive": {"rsi_entry": 48, "bear_rsi_entry": 38, "bb_pct_b_scale": 0.45, "position_size": 0.07, "max_holdings": 15, "stop_loss": -8, "stop_loss_vol": -12, "atr_stop_mult": 3.0, "atr_trail_mult": 1.8, "rsi_partial": 58, "rsi_full": 72}, "normal": {"rsi_entry": 43, "bear_rsi_entry": 32, "bb_pct_b_scale": 0.35, "position_size": 0.05, "max_holdings": 12, "stop_loss": -6, "stop_loss_vol": -10, "atr_stop_mult": 2.5, "atr_trail_mult": 1.5, "rsi_partial": 52, "rsi_full": 67}, "defensive": {"rsi_entry": 35, "bear_rsi_entry": 25, "bb_pct_b_scale": 0.20, "position_size": 0.03, "max_holdings": 8, "stop_loss": -5, "stop_loss_vol": -8, "atr_stop_mult": 2.0, "atr_trail_mult": 1.2, "rsi_partial": 48, "rsi_full": 60}, "crisis": {"rsi_entry": 28, "bear_rsi_entry": 20, "bb_pct_b_scale": 0.10, "position_size": 0.0, "max_holdings": 3, "stop_loss": -4, "stop_loss_vol": -6, "atr_stop_mult": 1.5, "atr_trail_mult": 1.0, "rsi_partial": 45, "rsi_full": 55}, }, "C: Crisis도 매매 (소량)": { "aggressive": {"rsi_entry": 48, "bear_rsi_entry": 38, "bb_pct_b_scale": 0.45, "position_size": 0.07, "max_holdings": 15, "stop_loss": -8, "stop_loss_vol": -12, "atr_stop_mult": 3.0, "atr_trail_mult": 1.8, "rsi_partial": 58, "rsi_full": 72}, "normal": {"rsi_entry": 43, "bear_rsi_entry": 32, "bb_pct_b_scale": 0.35, "position_size": 0.05, "max_holdings": 12, "stop_loss": -6, "stop_loss_vol": -10, "atr_stop_mult": 2.5, "atr_trail_mult": 1.5, "rsi_partial": 52, "rsi_full": 67}, "defensive": {"rsi_entry": 35, "bear_rsi_entry": 25, "bb_pct_b_scale": 0.20, "position_size": 0.03, "max_holdings": 8, "stop_loss": -5, "stop_loss_vol": -8, "atr_stop_mult": 2.0, "atr_trail_mult": 1.2, "rsi_partial": 48, "rsi_full": 60}, "crisis": {"rsi_entry": 25, "bear_rsi_entry": 18, "bb_pct_b_scale": 0.10, "position_size": 0.015, "max_holdings": 3, "stop_loss": -3, "stop_loss_vol": -5, "atr_stop_mult": 1.5, "atr_trail_mult": 1.0, "rsi_partial": 42, "rsi_full": 52}, }, "D: 넓은 손절 + 이익 극대화": { "aggressive": {"rsi_entry": 50, "bear_rsi_entry": 40, "bb_pct_b_scale": 0.50, "position_size": 0.06, "max_holdings": 15, "stop_loss": -10, "stop_loss_vol": -15, "atr_stop_mult": 3.5, "atr_trail_mult": 2.0, "rsi_partial": 62, "rsi_full": 78}, "normal": {"rsi_entry": 45, "bear_rsi_entry": 34, "bb_pct_b_scale": 0.38, "position_size": 0.05, "max_holdings": 12, "stop_loss": -8, "stop_loss_vol": -12, "atr_stop_mult": 3.0, "atr_trail_mult": 1.8, "rsi_partial": 58, "rsi_full": 72}, "defensive": {"rsi_entry": 38, "bear_rsi_entry": 28, "bb_pct_b_scale": 0.25, "position_size": 0.035, "max_holdings": 8, "stop_loss": -6, "stop_loss_vol": -9, "atr_stop_mult": 2.5, "atr_trail_mult": 1.4, "rsi_partial": 52, "rsi_full": 65}, "crisis": {"rsi_entry": 30, "bear_rsi_entry": 20, "bb_pct_b_scale": 0.12, "position_size": 0.0, "max_holdings": 3, "stop_loss": -4, "stop_loss_vol": -6, "atr_stop_mult": 1.5, "atr_trail_mult": 1.0, "rsi_partial": 45, "rsi_full": 55}, }, "E: 빠른 익절 + 타이트 손절": { "aggressive": {"rsi_entry": 48, "bear_rsi_entry": 38, "bb_pct_b_scale": 0.45, "position_size": 0.06, "max_holdings": 15, "stop_loss": -5, "stop_loss_vol": -8, "atr_stop_mult": 2.0, "atr_trail_mult": 1.2, "rsi_partial": 50, "rsi_full": 60}, "normal": {"rsi_entry": 43, "bear_rsi_entry": 32, "bb_pct_b_scale": 0.35, "position_size": 0.05, "max_holdings": 12, "stop_loss": -4, "stop_loss_vol": -7, "atr_stop_mult": 1.8, "atr_trail_mult": 1.1, "rsi_partial": 48, "rsi_full": 58}, "defensive": {"rsi_entry": 35, "bear_rsi_entry": 25, "bb_pct_b_scale": 0.20, "position_size": 0.03, "max_holdings": 8, "stop_loss": -3.5, "stop_loss_vol": -6, "atr_stop_mult": 1.5, "atr_trail_mult": 1.0, "rsi_partial": 45, "rsi_full": 55}, "crisis": {"rsi_entry": 28, "bear_rsi_entry": 20, "bb_pct_b_scale": 0.10, "position_size": 0.0, "max_holdings": 3, "stop_loss": -3, "stop_loss_vol": -5, "atr_stop_mult": 1.2, "atr_trail_mult": 0.8, "rsi_partial": 42, "rsi_full": 50}, }, "F: 하이브리드 (공격적 진입 + 보수적 청산)": { "aggressive": {"rsi_entry": 50, "bear_rsi_entry": 40, "bb_pct_b_scale": 0.50, "position_size": 0.07, "max_holdings": 16, "stop_loss": -6, "stop_loss_vol": -9, "atr_stop_mult": 2.5, "atr_trail_mult": 1.3, "rsi_partial": 52, "rsi_full": 63}, "normal": {"rsi_entry": 46, "bear_rsi_entry": 35, "bb_pct_b_scale": 0.40, "position_size": 0.055, "max_holdings": 13, "stop_loss": -5.5, "stop_loss_vol": -8, "atr_stop_mult": 2.2, "atr_trail_mult": 1.2, "rsi_partial": 50, "rsi_full": 60}, "defensive": {"rsi_entry": 38, "bear_rsi_entry": 28, "bb_pct_b_scale": 0.25, "position_size": 0.035, "max_holdings": 9, "stop_loss": -5, "stop_loss_vol": -7, "atr_stop_mult": 2.0, "atr_trail_mult": 1.1, "rsi_partial": 48, "rsi_full": 58}, "crisis": {"rsi_entry": 28, "bear_rsi_entry": 20, "bb_pct_b_scale": 0.10, "position_size": 0.01, "max_holdings": 3, "stop_loss": -4, "stop_loss_vol": -6, "atr_stop_mult": 1.5, "atr_trail_mult": 1.0, "rsi_partial": 45, "rsi_full": 55}, }, } def patch_and_run(config_name, modes): """run_backtest.py의 ADAPTIVE_MODES를 동적으로 교체하고 실행. 텍스트 치환 방식으로 ADAPTIVE_MODES 블록만 교체. 임시 파일 생성 → subprocess 실행 → 결과 파싱 → 임시 파일 삭제. Args: config_name: 조합 이름 (로그용) modes: 적응형 모드 파라미터 딕셔너리 (aggressive/normal/defensive/crisis) Returns: dict | None: 파싱된 성과 지표, 실패 시 None """ # run_backtest.py를 읽고 ADAPTIVE_MODES 블록을 교체 with open(BACKTEST_SCRIPT, "r") as f: content = f.read() # ADAPTIVE_MODES 블록 찾기 start_marker = "ADAPTIVE_MODES = {" end_marker = "\n\ndef determine_adaptive_mode(" start_idx = content.find(start_marker) end_idx = content.find(end_marker) if start_idx == -1 or end_idx == -1: print(f" [ERROR] ADAPTIVE_MODES 블록을 찾을 수 없습니다") return None # 새 ADAPTIVE_MODES 생성 new_block = "ADAPTIVE_MODES = " + json.dumps(modes, indent=4, ensure_ascii=False) patched = content[:start_idx] + new_block + content[end_idx:] # 임시 파일에 저장 tmp_script = os.path.join(BASE_DIR, "_tmp_backtest.py") with open(tmp_script, "w") as f: f.write(patched) # 실행 result = subprocess.run( [sys.executable, tmp_script], capture_output=True, text=True, timeout=120, ) # 결과 파싱 output = result.stdout + result.stderr metrics = {} for line in output.split("\n"): line = line.strip() if "총 수익률:" in line: metrics["return"] = line.split(":")[-1].strip() elif "CAGR:" in line: metrics["cagr"] = line.split(":")[-1].strip() elif "Max Drawdown:" in line: metrics["mdd"] = line.split(":")[-1].strip() elif "승률:" in line: metrics["winrate"] = line.split(":")[-1].strip() elif "총 매매 횟수:" in line: metrics["trades"] = line.split(":")[-1].strip() elif "Sharpe Ratio:" in line: metrics["sharpe"] = line.split(":")[-1].strip() elif "Profit Factor:" in line: metrics["pf"] = line.split(":")[-1].strip() elif "적응형 모드 전환" in line: metrics["mode_changes"] = line.strip() # 임시 파일 삭제 try: os.remove(tmp_script) except: pass return metrics def main(): print("=" * 70) print(" Auto-Stock 적응형 전략 파라미터 스윕 (6가지 조합)") print("=" * 70) results = {} for i, (name, modes) in enumerate(CONFIGS.items(), 1): print(f"\n{'─' * 70}") print(f" [{i}/6] {name}") print(f"{'─' * 70}") t0 = time.time() metrics = patch_and_run(name, modes) elapsed = time.time() - t0 if metrics: results[name] = metrics print(f" ✅ 완료 ({elapsed:.1f}초)") print(f" 수익률: {metrics.get('return', 'N/A')}") print(f" MDD: {metrics.get('mdd', 'N/A')}") print(f" 승률: {metrics.get('winrate', 'N/A')}") else: print(f" ❌ 실패") # 최종 비교표 print(f"\n{'=' * 70}") print(f" 최종 비교 결과") print(f"{'=' * 70}") print(f"{'조합':<35s} {'수익률':>10s} {'CAGR':>10s} {'MDD':>10s} {'승률':>16s} {'Sharpe':>10s} {'매매':>8s}") print(f"{'─' * 99}") for name, m in results.items(): print(f"{name:<35s} {m.get('return',''):>10s} {m.get('cagr',''):>10s} {m.get('mdd',''):>10s} {m.get('winrate',''):>16s} {m.get('sharpe',''):>10s} {m.get('trades',''):>8s}") print(f"{'=' * 70}") if __name__ == "__main__": main()