Files
openclaw/agents/stock/workspace/sim/sweep.py
T
hyowons 9cc7043490 auto: 일일 백업 2026-06-10 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 02:00:02 +09:00

104 lines
4.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""파라미터 스윕 — 여러 튜닝을 백테스트로 동시 비교 → 순위표.
과최적화 방어: 기간을 앞(학습)/뒤(검증)로 쪼개 둘 다 측정.
- 학습기간 성적만 높고 검증기간 무너지면 = 과최적화 의심.
- 검증기간(out-of-sample) 성적으로 순위.
결과: state/sim/backtest_results.json (웹 백테스트 탭이 읽음).
"""
from __future__ import annotations
import itertools
import json
from datetime import datetime
from . import backtest, config, universe
# 백테스트에서 실제 영향 있는 파라미터만 스윕 (애널 게이트·시장필터는 backtest 중립이라 제외).
# 기존 5 + 신규 4(RSI·시간손절·리스크) 탐색. 조합폭발 억제 위해 기존 그리드는 핵심값으로 축소.
# base 3×3×2×2×2 = 72 × 신규 2×2×2×2 = 16 → 1152 조합.
GRID = {
'SMA_LONG': [10, 20, 40], # 추세 속도 — 빠른/느린 (다양성 핵심)
'RR_RATIO': [1.5, 2.0, 3.0], # 손익비
'STOP_ATR_MULT': [1.5, 2.0], # 손절 폭
'PULLBACK_ATR_MULT': [1.0, 1.5], # 눌림 깊이
'VOLUME_BREAKOUT_MULT': [1.3, 2.0], # 돌파 거래량 강도
'RSI_OVERBOUGHT': [75, 88], # 돌파 과열 컷 (엄격/느슨)
'RSI_OVERSOLD': [20, 35], # 눌림목 과매도 컷 (느슨/엄격)
'MAX_HOLD_DAYS': [0, 20], # 시간손절 끔/20일
'RISK_PER_TRADE_PCT': [0.01, 0.02], # 종목당 리스크 1%/2%
}
MIN_TRADES = 10 # 검증기간 거래 이 미만이면 표본 부족 → 하위로
def _all_dates(codes: list[str]) -> list[str]:
s = set()
for code in codes:
for c in backtest.load_history(code):
s.add(c['date'])
return sorted(s)
def run_sweep(codes: list[str] | None = None, train_frac: float = 0.7, grid: dict | None = None) -> dict:
codes = codes or [e['code'] for e in universe.build_universe()]
grid = grid or GRID
dates = _all_dates(codes)
if len(dates) < 60:
return {'error': 'insufficient_history', 'days': len(dates)}
split = int(len(dates) * train_frac)
split_date = dates[split]
train_to, test_from, test_to = dates[split - 1], dates[split], dates[-1]
train_from = dates[0]
keys = list(grid.keys())
combos = list(itertools.product(*[grid[k] for k in keys]))
results = []
for combo in combos:
params = dict(zip(keys, combo))
tr = backtest.run(params, codes, date_from=train_from, date_to=train_to)
te = backtest.run(params, codes, date_from=test_from, date_to=test_to)
results.append({'params': params, 'train': tr, 'test': te})
def sort_key(r):
te = r['test']
enough = (te.get('trades', 0) or 0) >= MIN_TRADES
return (1 if enough else 0, te.get('total_return_pct', -999) or -999)
results.sort(key=sort_key, reverse=True)
out = {
'generated_at': datetime.now(config.KST).isoformat(),
'universe_size': len(codes),
'swept_params': keys,
'grid': grid,
'split': {'train': f'{train_from}~{train_to}', 'test': f'{test_from}~{test_to}',
'train_frac': train_frac},
'min_trades': MIN_TRADES,
'flow_used': results[0]['test'].get('flow_used', False) if results else False,
'count': len(results),
'results': results,
}
config.STATE_DIR.mkdir(parents=True, exist_ok=True)
(config.STATE_DIR / 'backtest_results.json').write_text(json.dumps(out, ensure_ascii=False, indent=2))
return out
if __name__ == '__main__':
import sys
frac = float(sys.argv[1]) if len(sys.argv) > 1 else 0.7
res = run_sweep(train_frac=frac)
if res.get('error'):
print('스윕 실패:', res)
else:
print(f"스윕 완료 — {res['count']}개 조합 · 검증기간 {res['split']['test']}")
print(f"{'RR':>4} {'STOP':>5} {'SMA':>4} {'PULL':>5} {'RSI±':>8} {'HOLD':>5} {'RISK':>5} | "
f"{'검증수익%':>8} {'검증MDD%':>8} {'거래':>5} {'승률%':>6}")
for r in res['results'][:12]:
p, te = r['params'], r['test']
rsi = f"{p.get('RSI_OVERBOUGHT','-')}/{p.get('RSI_OVERSOLD','-')}"
print(f"{p['RR_RATIO']:>4} {p['STOP_ATR_MULT']:>5} {p['SMA_LONG']:>4} {p['PULLBACK_ATR_MULT']:>5} "
f"{rsi:>8} {p.get('MAX_HOLD_DAYS','-'):>5} {p.get('RISK_PER_TRADE_PCT','-'):>5} | "
f"{te['total_return_pct']:>8} {te['mdd_pct']:>8} "
f"{te['trades']:>5} {str(te['win_rate_pct']):>6}")