auto: 일일 백업 2026-06-11 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -10,10 +10,29 @@ from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
from multiprocessing import get_context
|
||||
|
||||
from . import backtest, config, universe
|
||||
|
||||
|
||||
def _eval_combo(args):
|
||||
"""워커 1개가 조합 1개를 평가 — 학습/검증 + 전체기간 3분할 구간(꾸준함 측정).
|
||||
파라미터는 학습(fit)이 없으므로 3분할은 '한 구간 운빨'을 거르는 일관성 검증."""
|
||||
keys, combo, codes, trf, trt, tef, tet, windows = args
|
||||
params = dict(zip(keys, combo))
|
||||
tr = backtest.run(params, codes, date_from=trf, date_to=trt)
|
||||
te = backtest.run(params, codes, date_from=tef, date_to=tet)
|
||||
wins = []
|
||||
for wf, wt in windows:
|
||||
w = backtest.run(params, codes, date_from=wf, date_to=wt)
|
||||
wins.append({'from': wf, 'to': wt,
|
||||
'ret': w.get('total_return_pct'), 'trades': w.get('trades', 0)})
|
||||
rets = [w['ret'] for w in wins if w['ret'] is not None]
|
||||
return {'params': params, 'train': tr, 'test': te, 'windows': wins,
|
||||
'worst_ret': min(rets) if rets else None}
|
||||
|
||||
# 백테스트에서 실제 영향 있는 파라미터만 스윕 (애널 게이트·시장필터는 backtest 중립이라 제외).
|
||||
# 기존 5 + 신규 4(RSI·시간손절·리스크) 탐색. 조합폭발 억제 위해 기존 그리드는 핵심값으로 축소.
|
||||
# base 3×3×2×2×2 = 72 × 신규 2×2×2×2 = 16 → 1152 조합.
|
||||
@@ -51,22 +70,39 @@ def run_sweep(codes: list[str] | None = None, train_frac: float = 0.7, grid: dic
|
||||
train_to, test_from, test_to = dates[split - 1], dates[split], dates[-1]
|
||||
train_from = dates[0]
|
||||
|
||||
# 전체기간 3분할 — 구간별 수익으로 '꾸준함'(최악 구간) 측정
|
||||
third = len(dates) // 3
|
||||
windows = [(dates[0], dates[third - 1]),
|
||||
(dates[third], dates[2 * third - 1]),
|
||||
(dates[2 * third], dates[-1])]
|
||||
|
||||
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})
|
||||
args = [(keys, combo, codes, train_from, train_to, test_from, test_to, windows) for combo in combos]
|
||||
|
||||
# 멀티프로세싱 — 조합을 코어에 분배. macOS는 fork가 데드락 위험이라 spawn 사용
|
||||
# (워커가 메인 재import → sim/__main__.py의 `if __name__=='__main__'` 가드로 재귀 방지).
|
||||
# chunksize 크게 → 워커당 연속 조합 처리로 종목 일봉 메모이즈 재활용 극대화. pool.map은 순서 유지(결정론).
|
||||
nproc = max(1, (os.cpu_count() or 4) - 2)
|
||||
if len(args) <= 1 or nproc == 1:
|
||||
results = [_eval_combo(a) for a in args]
|
||||
else:
|
||||
chunk = max(1, len(args) // nproc)
|
||||
with get_context('spawn').Pool(nproc) as pool:
|
||||
results = pool.map(_eval_combo, args, chunksize=chunk)
|
||||
|
||||
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)
|
||||
worst = r.get('worst_ret')
|
||||
# 1순위 최악 구간 수익(꾸준함 — 한 구간 운빨 배제), 2순위 검증수익
|
||||
return (1 if enough else 0,
|
||||
worst if worst is not None else -999,
|
||||
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),
|
||||
@@ -81,6 +117,8 @@ def run_sweep(codes: list[str] | None = None, train_frac: float = 0.7, grid: dic
|
||||
}
|
||||
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))
|
||||
# ⚠️ 비교군 자동 동기화는 2026-06-10 관리자님 지시로 끔 — sweep은 결과 파일만 갱신.
|
||||
# 비교군 변경은 명시 요청 시에만: `python3 -m sim variants sync` 또는 variants.sync_label_tops().
|
||||
return out
|
||||
|
||||
|
||||
@@ -91,13 +129,13 @@ if __name__ == '__main__':
|
||||
if res.get('error'):
|
||||
print('스윕 실패:', res)
|
||||
else:
|
||||
print(f"스윕 완료 — {res['count']}개 조합 · 검증기간 {res['split']['test']}")
|
||||
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}")
|
||||
f"{'최악구간%':>8} {'검증수익%':>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"{str(r.get('worst_ret')):>8} {te['total_return_pct']:>8} "
|
||||
f"{te['trades']:>5} {str(te['win_rate_pct']):>6}")
|
||||
|
||||
Reference in New Issue
Block a user