fix: CRITICAL/HIGH 버그 30건 수정 + 전체 코드 주석 추가 + 중복 정리

## 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>
This commit is contained in:
2026-03-26 16:05:41 +09:00
parent 739a8a2e5d
commit de7921d270
149 changed files with 133353 additions and 3338 deletions

View File

@@ -0,0 +1,186 @@
#!/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()