""" 04단계 PDCA 파이프라인: 프로필 → 스캔 → 라벨 → 선별. """ from __future__ import annotations import argparse import json import sys import time from pathlib import Path import pandas as pd from config import CHART_LOOKBACK_DAYS, SYMBOL from deepcoin.data.mtf_bb import load_frames_from_db from deepcoin.matching.config import ( MATCHING_BACKTEST_HTML, MATCHING_FIRE_OUTCOMES, MATCHING_GT_OVERLAP, MATCHING_MATCHED_RULES, MATCHING_RULE_CANDIDATES, MATCHING_RULE_FIRES, ) from deepcoin.matching.label_outcomes import label_fire_outcomes from deepcoin.matching.profile_rules import build_rule_candidates, save_rule_candidates from deepcoin.matching.rule_eval import ( build_mtf_scan_frame, conditions_columns, scan_rule_fires, ) from deepcoin.matching.select_rules import ( select_matched_rules, write_backtest_summary_html, ) from deepcoin.ops.monitor import Monitor from deepcoin.paths import ensure_dirs def run_matching_pipeline( phase: str = "all", trades_csv: Path | None = None, ) -> None: """ 04a~04d 단계를 순서대로 실행합니다. Args: phase: all | profile | scan | label | select. trades_csv: 03b CSV 경로(선택). """ ensure_dirs() t0 = time.time() from config import MATCH_INCLUDE_ATOMIC, MATCH_LABEL_MODE print( f"=== 04 매칭 파이프라인 {SYMBOL} " f"(label={MATCH_LABEL_MODE}, atomic={MATCH_INCLUDE_ATOMIC}) ===" ) sys.stdout.flush() candidates_path = MATCHING_RULE_CANDIDATES fires_path = MATCHING_RULE_FIRES outcomes_path = MATCHING_FIRE_OUTCOMES candidates: dict | None = None fires: pd.DataFrame | None = None outcomes: pd.DataFrame | None = None frames = None if phase in ("all", "profile"): print("[04] Phase 4-1 Plan/Do: GT 프로필 → 규칙 후보") candidates = build_rule_candidates(trades_csv) save_rule_candidates(candidates, candidates_path) if phase == "profile": return if phase in ("all", "scan", "label"): if candidates is None: candidates = json.loads(candidates_path.read_text(encoding="utf-8")) rules = candidates.get("rules", []) print(f"[04] Phase 4-2 Do: 전구간 발화 스캔 ({len(rules)}규칙)") sys.stdout.flush() mon = Monitor(cooldown_file=None) frames = load_frames_from_db(mon, SYMBOL, lookback_days=CHART_LOOKBACK_DAYS) needed = conditions_columns(rules) scan_frame = build_mtf_scan_frame(frames, needed) fires = scan_rule_fires(scan_frame, rules) fires_path.parent.mkdir(parents=True, exist_ok=True) fires.to_csv(fires_path, index=False, encoding="utf-8-sig") print(f"[04-2] 저장: {fires_path} ({len(fires):,}행)") if phase == "scan": return if phase in ("all", "label", "select"): if fires is None: fires = pd.read_csv(fires_path) if phase in ("all", "label"): print("[04] Phase 4-3 Check: 발화별 forward PnL 라벨") if frames is None: mon = Monitor(cooldown_file=None) frames = load_frames_from_db(mon, SYMBOL, lookback_days=CHART_LOOKBACK_DAYS) outcomes = label_fire_outcomes(fires, frames) outcomes.to_csv(outcomes_path, index=False, encoding="utf-8-sig") print(f"[04-3] 저장: {outcomes_path}") if phase == "label": return if phase in ("all", "select"): if candidates is None: candidates = json.loads(candidates_path.read_text(encoding="utf-8")) if outcomes is None: outcomes = pd.read_csv(outcomes_path) print("[04] Phase 4-4 Act: EV 필터·규칙 선별") matched = select_matched_rules(outcomes, candidates) MATCHING_MATCHED_RULES.parent.mkdir(parents=True, exist_ok=True) MATCHING_MATCHED_RULES.write_text( json.dumps(matched, ensure_ascii=False, indent=2), encoding="utf-8", ) print(f"[04-4] 저장: {MATCHING_MATCHED_RULES}") overlap = matched.get("gt_overlap", {}) MATCHING_GT_OVERLAP.write_text( json.dumps(overlap, ensure_ascii=False, indent=2), encoding="utf-8", ) write_backtest_summary_html(matched, MATCHING_BACKTEST_HTML) print(f"완료 ({time.time() - t0:.0f}초)") def main() -> None: """CLI 진입.""" parser = argparse.ArgumentParser(description="04 GT+EV 매칭 파이프라인") parser.add_argument( "--phase", choices=("all", "profile", "scan", "label", "select"), default="all", ) parser.add_argument("--trades-csv", type=str, default="") args = parser.parse_args() csv = Path(args.trades_csv) if args.trades_csv else None run_matching_pipeline(phase=args.phase, trades_csv=csv) if __name__ == "__main__": main()