"""파라미터 스윕 — 여러 튜닝을 백테스트로 동시 비교 → 순위표.

과최적화 방어: 기간을 앞(학습)/뒤(검증)로 쪼개 둘 다 측정.
  - 학습기간 성적만 높고 검증기간 무너지면 = 과최적화 의심.
  - 검증기간(out-of-sample) 성적으로 순위.

결과: state/sim/backtest_results.json (웹 백테스트 탭이 읽음).
"""
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 4×3×2×2×2 = 96 × 신규 2×2×2×2 = 16 → 1536 조합. (2026-06-15 SMA_LONG 60 추가)
GRID = {
    'SMA_LONG': [10, 20, 60],                  # 추세선 길이 — 10/20/60 (2026-06-15 40 제거: 어중간·관리자님 지시. 60은 중기선과 겹침)
    '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]

    # 전체기간 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]))
    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
        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),
        '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))
    # ⚠️ 비교군 자동 동기화는 2026-06-10 관리자님 지시로 끔 — sweep은 결과 파일만 갱신.
    # 비교군 변경은 명시 요청 시에만: `python3 -m sim variants sync` 또는 variants.sync_label_tops().
    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} {'검증수익%':>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"{str(r.get('worst_ret')):>8} {te['total_return_pct']:>8} "
                  f"{te['trades']:>5} {str(te['win_rate_pct']):>6}")
