"""병렬 페이퍼 변이(variant) 관리 — 각 변이는 {이름, 파라미터} + 자체 가상계좌. 정의: state/sim/variants.json = [{"id":"v1","name":"...","params":{KEY:VAL,...}}] 계좌: state/sim/variants//{portfolio.json, trades.jsonl} 비교: state/sim/variants_compare.json (웹 비교 탭) """ from __future__ import annotations import json from . import config VARIANTS_PATH = config.STATE_DIR / 'variants.json' VARIANTS_DIR = config.STATE_DIR / 'variants' COMPARE_PATH = config.STATE_DIR / 'variants_compare.json' def load_variants() -> list[dict]: try: return json.loads(VARIANTS_PATH.read_text()) except Exception: return [] def save_variants(variants: list[dict]): config.STATE_DIR.mkdir(parents=True, exist_ok=True) VARIANTS_PATH.write_text(json.dumps(variants, ensure_ascii=False, indent=2)) def variant_paths(vid: str): d = VARIANTS_DIR / vid return d / 'portfolio.json', d / 'trades.jsonl' def _label(params: dict) -> str: short = {'RR_RATIO': 'RR', 'STOP_ATR_MULT': 'S', 'SMA_LONG': 'SMA', 'PULLBACK_ATR_MULT': 'P', 'VOLUME_BREAKOUT_MULT': 'V', 'TURNOVER_OVERHEAT_MULT': 'OH', 'RSI_OVERBOUGHT': 'RSI↑', 'RSI_OVERSOLD': 'RSI↓', 'MAX_HOLD_DAYS': 'HOLD', 'RISK_PER_TRADE_PCT': 'RISK'} return '·'.join(f'{short.get(k, k)}{v:g}' for k, v in params.items()) def seed_from_sweep(n: int = 3) -> list[dict]: """backtest_results.json 검증 상위 n개 → 변이로 등록 (기존 계좌는 유지).""" try: res = json.loads((config.STATE_DIR / 'backtest_results.json').read_text()) except Exception: return [] existing = {v['id']: v for v in load_variants()} variants = list(existing.values()) seen_params = {json.dumps(v['params'], sort_keys=True) for v in variants} rank = 0 for r in res.get('results', []): if rank >= n: break params = r['params'] key = json.dumps(params, sort_keys=True) if key in seen_params: continue rank += 1 vid = f'v{len(variants)+1}' variants.append({'id': vid, 'name': f'#{rank} {_label(params)}', 'params': params}) seen_params.add(key) save_variants(variants) return variants def add_variant(params: dict) -> dict | None: """단일 params 조합을 변이로 추가 (백테스트 행 → 비교군). 이미 있으면 None.""" variants = load_variants() seen = {json.dumps(v['params'], sort_keys=True) for v in variants} if json.dumps(params, sort_keys=True) in seen: return None nums = [int(v['id'][1:]) for v in variants if v['id'][1:].isdigit()] vid = f'v{(max(nums) + 1) if nums else 1}' v = {'id': vid, 'name': _label(params), 'params': params} variants.append(v) save_variants(variants) return v def remove_variant(vid: str): """변이 1개 삭제 (정의 + 계좌).""" import shutil save_variants([v for v in load_variants() if v['id'] != vid]) d = VARIANTS_DIR / vid if d.exists(): shutil.rmtree(d) def reset_variant(vid: str): """변이 1개 성적만 초기화 (정의 유지 — 계좌 디렉터리 삭제 후 다음 스캔에 재생성).""" import shutil d = VARIANTS_DIR / vid if d.exists(): shutil.rmtree(d) def reset_all(): """모든 변이 계좌 초기화 (variants.json 정의는 유지).""" import shutil if VARIANTS_DIR.exists(): shutil.rmtree(VARIANTS_DIR) def clear(): """변이 정의·계좌 전부 삭제.""" reset_all() if VARIANTS_PATH.exists(): VARIANTS_PATH.unlink()