#!/usr/bin/env python3 """2단계: 인과 기법 GT 정합 분석 (과거 데이터만 · 0단계 타점 대비).""" from __future__ import annotations import argparse import logging import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[1] SRC = ROOT / "src" if str(SRC) not in sys.path: sys.path.insert(0, str(SRC)) from bithumb.config import load_settings from bithumb.data.intervals import interval_label from bithumb.evaluation.report import ( build_comparison_report, render_comparison_html, save_comparison_report, ) from bithumb.techniques.base import TechniqueParams from bithumb.techniques.registry import list_technique_ids from bithumb.techniques.runner import ( load_ground_truth, run_all_techniques, save_technique_result, ) def _configure_logging(verbose: bool) -> None: """로깅 레벨을 설정한다.""" level = logging.DEBUG if verbose else logging.INFO logging.basicConfig( level=level, format="%(asctime)s [%(levelname)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) def main() -> int: """CLI 진입점.""" parser = argparse.ArgumentParser(description="2단계: 인과 기법 GT 정합 분석") parser.add_argument( "--techniques", type=str, default=None, help="실행할 기법 ID (쉼표 구분). 기본: 전체", ) parser.add_argument("--tolerance", type=int, default=None, help="GT 정합 허용 봉 수") parser.add_argument("--no-report", action="store_true", help="비교 리포트 생략") parser.add_argument("-v", "--verbose", action="store_true") args = parser.parse_args() _configure_logging(args.verbose) settings = load_settings() if not settings.ground_truth_file.exists(): logging.error("Ground Truth 파일 없음: %s — 먼저 0_ground_truth.py 실행", settings.ground_truth_file) return 1 gt_result = load_ground_truth(settings.ground_truth_file) technique_ids = None if args.techniques: technique_ids = [t.strip() for t in args.techniques.split(",") if t.strip()] params = TechniqueParams( interval_min=settings.gt_interval_min, lookback_days=settings.gt_lookback_days, min_leg_pct=settings.gt_min_leg_pct, initial_cash_krw=settings.gt_initial_cash_krw, fee_rate=settings.gt_trading_fee_rate, extra={"reversal_pct": settings.gt_zigzag_reversal_pct}, ) tolerance = args.tolerance or settings.gt_align_tolerance_bars logging.info( "기법 실행: %s %s, %s일, tolerance=%s봉", settings.symbol, interval_label(params.interval_min), params.lookback_days, tolerance, ) technique_count = len(technique_ids) if technique_ids else len(list_technique_ids()) print( f"\n=== 2단계 기법 실행 시작 ({technique_count}종) ===", flush=True, ) completed = 0 def _on_result(result) -> None: nonlocal completed save_technique_result(result, settings.techniques_dir) completed += 1 pct = completed / technique_count * 100.0 if technique_count else 100.0 align = result.alignment or {} print( f"[기법] {completed}/{technique_count} ({pct:.1f}%) — " f"{result.technique_id} score={align.get('score', 0)*100:.1f}", flush=True, ) results = run_all_techniques( db_path=settings.db_path, symbol=settings.symbol, params=params, gt_result=gt_result, tolerance_bars=tolerance, technique_ids=technique_ids, on_result=_on_result, ) saved_paths: list[Path] = [] for result in results: path = settings.techniques_dir / f"{result.technique_id}.json" saved_paths.append(path) align = result.alignment or {} legs = align.get("legs", {}) print( f" [{result.technique_id}] {result.technique_name}: " f"레그 {result.summary.get('leg_count', 0)}개, " f"수익 {result.pnl.get('total_return_pct', 0):+.1f}%, " f"GT정합 score={align.get('score', 0)*100:.1f} " f"(leg recall {legs.get('leg_recall', 0)*100:.0f}%)" ) print(f"\n=== 2단계 인과 정합 분석 완료 ({len(results)}개) ===") print(f"저장: {settings.techniques_dir}/") for path in saved_paths: print(f" - {path.name}") if not args.no_report: report = build_comparison_report(results, gt_result, settings.symbol) json_path = save_comparison_report(report, settings.analysis_report_json) html_path = render_comparison_html(report, settings.analysis_report_html) print(f"\n=== GT 정합 순위 (상위 3) ===") gt_return = report["gt"]["return_pct"] print(f"GT 벤치마크: {gt_return:+.1f}%") for idx, row in enumerate(report["ranking"][:3], start=1): print( f" {idx}. {row['technique_name']}: " f"score {row['score']*100:.1f}, " f"수익 {row['tech_return_pct']:+.1f}%, " f"leg recall {row['leg_recall']*100:.0f}%" ) print(f"리포트 JSON: {json_path}") print(f"리포트 HTML: {html_path}") print(f"\n등록 기법: {', '.join(list_technique_ids())}") return 0 if __name__ == "__main__": raise SystemExit(main())