feat: 자동매매 신뢰성 개선 — 시세 실서버 라우팅·일일 한도 전역화·수동 매수 API
- 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>
This commit is contained in:
113
backend/_sweep_single_axis.py
Normal file
113
backend/_sweep_single_axis.py
Normal file
@@ -0,0 +1,113 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user