- korea_investment_api: KIS_QUOTES_VIA_REAL — 모의투자 모드에서 시세/차트/호가 조회를 실계좌 시세 서버(20/s)로 위임, 주문·잔고는 모의서버 유지 (EGW00201 대응) - recommendations: execute_auto_trade_background asyncio.Lock 중복 실행 방지 + MAX_DAILY_TRADES를 당일 BUY 건수 차감 전역 기준으로 적용 (11종목 동시 매수 재발 방지) - manual_buy.py 신규: POST /api/v1/portfolio/buy (리스크 가드 + safe_place_order + db_worker) - mcp-server: claude_code_client(claude -p 헤드리스 모드) 추가, trade_monitor 부분매도 시 active 유지·remaining_pct 갱신, API 키 헤더·/api/v1 경로 수정, ODNO 주문ID 수정 - config: MCP_REQUEST_TIMEOUT, MCP_RISK_CACHE_TTL, BATCH_ANALYSIS_INTERVAL, SWING_INDEX_BREAKER_ENABLED(OFF), API_TRIGGERED_AUTO_TRADE(OFF) 추가 - docker-compose: 포트 충돌 해소 (postgres 15433, frontend 3001) - frontend: 추천 패널 매수 버튼, 차트/호가/저널 패널 개선 - CLAUDE.md 압축 최적화 (규칙 변경 없음), _sweep_single_axis.py 백테스트 단일축 스윕 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
114 lines
4.7 KiB
Python
114 lines
4.7 KiB
Python
#!/usr/bin/env python3
|
|
"""단일 파라미터 축 민감도 스윕 (run_backtest.py의 'normal' 모드 필드 1개만 오버라이드).
|
|
|
|
run_backtest_sweep.py와 동일한 텍스트 치환 방식을 사용하되, ADAPTIVE_MODES 전체가
|
|
아니라 normal 모드의 지정된 키 하나만 바꿔서 단일 축 민감도를 측정한다.
|
|
"""
|
|
import subprocess, json, sys, os, time, argparse
|
|
|
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
BACKTEST_SCRIPT = os.path.join(BASE_DIR, "run_backtest.py")
|
|
|
|
BASELINE_MODES = {
|
|
"aggressive": {"rsi_entry": 48.0, "bear_rsi_entry": 38.0, "bb_pct_b_scale": 0.45, "position_size": 0.07,
|
|
"max_holdings": 15, "stop_loss": -8.0, "stop_loss_vol": -12.0, "atr_stop_mult": 3.0,
|
|
"atr_trail_mult": 1.8, "rsi_partial": 58.0, "rsi_full": 72.0},
|
|
"normal": {"rsi_entry": 43.0, "bear_rsi_entry": 32.0, "bb_pct_b_scale": 0.35, "position_size": 0.05,
|
|
"max_holdings": 12, "stop_loss": -6.0, "stop_loss_vol": -10.0, "atr_stop_mult": 2.5,
|
|
"atr_trail_mult": 1.5, "rsi_partial": 52.0, "rsi_full": 67.0},
|
|
"defensive": {"rsi_entry": 35.0, "bear_rsi_entry": 25.0, "bb_pct_b_scale": 0.20, "position_size": 0.03,
|
|
"max_holdings": 8, "stop_loss": -5.0, "stop_loss_vol": -8.0, "atr_stop_mult": 2.0,
|
|
"atr_trail_mult": 1.2, "rsi_partial": 48.0, "rsi_full": 60.0},
|
|
"crisis": {"rsi_entry": 28.0, "bear_rsi_entry": 20.0, "bb_pct_b_scale": 0.10, "position_size": 0.0,
|
|
"max_holdings": 3, "stop_loss": -4.0, "stop_loss_vol": -6.0, "atr_stop_mult": 1.5,
|
|
"atr_trail_mult": 1.0, "rsi_partial": 45.0, "rsi_full": 55.0},
|
|
}
|
|
|
|
|
|
def patch_and_run(modes, start, end, timeout=600):
|
|
with open(BACKTEST_SCRIPT, "r") as f:
|
|
content = f.read()
|
|
start_marker = "ADAPTIVE_MODES = {"
|
|
end_marker = "\n\ndef determine_adaptive_mode("
|
|
s = content.find(start_marker)
|
|
e = content.find(end_marker)
|
|
if s == -1 or e == -1:
|
|
print("[ERROR] ADAPTIVE_MODES 블록을 찾을 수 없습니다")
|
|
return None
|
|
new_block = "ADAPTIVE_MODES = " + json.dumps(modes, indent=4, ensure_ascii=False)
|
|
patched = content[:s] + new_block + content[e:]
|
|
|
|
tmp_script = os.path.join(BASE_DIR, f"_tmp_sweep_{os.getpid()}.py")
|
|
with open(tmp_script, "w") as f:
|
|
f.write(patched)
|
|
try:
|
|
result = subprocess.run(
|
|
[sys.executable, tmp_script, "--start", start, "--end", end],
|
|
capture_output=True, text=True, timeout=timeout,
|
|
)
|
|
output = result.stdout + result.stderr
|
|
finally:
|
|
try:
|
|
os.remove(tmp_script)
|
|
except OSError:
|
|
pass
|
|
|
|
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["max_loss_streak"] = line.split(":")[-1].strip()
|
|
if not metrics:
|
|
print("---- RAW OUTPUT (no metrics parsed) ----")
|
|
print(output[-3000:])
|
|
return metrics
|
|
|
|
|
|
AXIS_KEY_MAP = {
|
|
"rsi_entry": "rsi_entry",
|
|
"stop_loss": "stop_loss",
|
|
"rsi_partial": "rsi_partial",
|
|
"atr_stop_mult": "atr_stop_mult",
|
|
}
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--axis", required=True, choices=list(AXIS_KEY_MAP.keys()))
|
|
ap.add_argument("--values", required=True, help="comma separated values, e.g. 42,46,50")
|
|
ap.add_argument("--start", default="2024-09-01")
|
|
ap.add_argument("--end", default="2026-09-05")
|
|
args = ap.parse_args()
|
|
|
|
key = AXIS_KEY_MAP[args.axis]
|
|
values = [float(v) for v in args.values.split(",")]
|
|
|
|
print(f"=== Axis: {args.axis} ({key}) | period {args.start}~{args.end} ===")
|
|
for v in values:
|
|
import copy
|
|
modes = copy.deepcopy(BASELINE_MODES)
|
|
modes["normal"][key] = v
|
|
print(f"\n--- {args.axis}={v} (normal mode only) ---")
|
|
t0 = time.time()
|
|
m = patch_and_run(modes, args.start, args.end)
|
|
elapsed = time.time() - t0
|
|
print(f" elapsed={elapsed:.1f}s {m}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|