## CRITICAL 수정 (20건) - day_trading: global 선언 누락, loss_tracker 연동, ban 체크 추가 - trade_executor: BUY 체결가 order_price로 수정, remaining_pct 누적 계산 - risk_validator: get_current_price dict 파싱, MAX_POSITION_SIZE 동기화, DB 세션 누수 - risk_management/recommendations: date.today() → KST 변환 - mdd_manager: Lock 밖 executed_key 참조 수정 - correlation_filter: output → output2 키 수정 - portfolio: 해외 종목 USD/KRW 환율 적용 분리 - Sharpe Ratio 거래빈도 기반 연간화 - telegram_notifier: send_message() 공개 래퍼 추가 - position_manager: 앱 재시작 시 DB 포지션 복원 - swing exit_service: remaining_pct 조건 범위 수정 - run_backtest: original_qty 기반 remaining_pct 계산 - us_market_schedule: 2027년 미국 공휴일 추가 - Login.tsx: res.ok 검증 추가 - mean_reversion_strategy: RSI 로그 하드코딩 수정 ## HIGH 수정 (10건) - db_worker: DLQ 파일 백업 메커니즘 추가 - korea_investment_api: 만료 토큰 임의 연장 제거 - market_data: chart_cache 최대 100개 + TTL 만료 - main.py: rate_limit_store 1시간 미접근 IP 정리 - recommendations: fire-and-forget try/except 추가 - swing/scheduler: DB 세션 3개 분리 - Dashboard: wsMessages 전체 순회 - Layout: /health 엔드포인트 + 30초 폴링 - ml_ensemble: POST /train require_api_key 추가 - day_trading: record_trade_result_safe() 전환 ## 코드 정리 - 프론트엔드: isUsStock, formatTime 공통 유틸 추출 - 백엔드: dead code 제거, 중복 import 통합 - MCP: JSON 파싱 헬퍼 추출 (~60줄 감소) - 전체 ~100개 파일 한국어 주석 추가 - 테스트 13건 코드 기준 수정 (468 passed) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
187 lines
11 KiB
Python
187 lines
11 KiB
Python
#!/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()
|