#!/usr/bin/env python3 """ Phase C dry-run 종료 후 모의 수익률(참고) 집계. - 입력: data/ops/paper_fires.jsonl (06 dry-run 발화 로그) - 출력: docs/05_ops/phase_c_paper_report.json + 콘솔 요약 주의: 실계좌 수익이 아님. 발화가 N봉 후 가격으로 계산한 forward % 합산(참고). hybrid 복리 PnL은 simulation_report.html 과 다릅니다. """ from __future__ import annotations import json import runpy from datetime import datetime from pathlib import Path import numpy as np import pandas as pd runpy.run_path(str(Path(__file__).resolve().parent / "_bootstrap.py")) from config import ( # noqa: E402 MATCH_FORWARD_BARS, MATCH_PRIMARY_INTERVAL, SYMBOL, TRADING_FEE_RATE, ) from deepcoin.matching.label_outcomes import _forward_ret_vectorized # noqa: E402 from deepcoin.ops.monitor import Monitor # noqa: E402 from deepcoin.paths import PAPER_FIRES_LOG, PAPER_WEEKLY_REPORT_JSON # noqa: E402 _FEE_PCT = TRADING_FEE_RATE * 2 * 100 def load_paper_fires(path: Path) -> pd.DataFrame: """paper_fires.jsonl → DataFrame.""" if not path.is_file(): return pd.DataFrame() rows = [] for line in path.read_text(encoding="utf-8").splitlines(): line = line.strip() if line: rows.append(json.loads(line)) if not rows: return pd.DataFrame() return pd.DataFrame(rows) def attach_forward_returns(fires: pd.DataFrame, close_df: pd.DataFrame) -> pd.DataFrame: """ would_trade=True 발화에 MATCH_FORWARD_BARS 기준 forward 수익률(%) 부여. Args: fires: paper 발화. close_df: 3분 종가 (datetime index). Returns: forward_ret_pct 컬럼 추가. """ if fires.empty or close_df.empty: fires["forward_ret_pct"] = np.nan return fires close_df = close_df.sort_index() if not isinstance(close_df.index, pd.DatetimeIndex): close_df.index = pd.to_datetime(close_df.index) close_ts_ns = close_df.index.astype(np.int64).values close_px = close_df["Close"].astype(float).values sub = fires[fires["would_trade"] == True].copy() # noqa: E712 if sub.empty: fires["forward_ret_pct"] = np.nan return fires sig = pd.to_datetime(sub["signal_dt"]) fire_ns = sig.astype(np.int64).values c0 = sub["close"].astype(float).values side = sub["side"].astype(str).values ret, valid = _forward_ret_vectorized( fire_ns, c0, close_ts_ns, close_px, side, MATCH_FORWARD_BARS, _FEE_PCT ) fires = fires.copy() fires["forward_ret_pct"] = np.nan idx = sub.index fires.loc[idx, "forward_ret_pct"] = np.where(valid, ret, np.nan) return fires def summarize(fires: pd.DataFrame) -> dict: """집계 dict.""" traded = fires[fires["would_trade"] == True] # noqa: E712 with_ret = traded[traded["forward_ret_pct"].notna()] out: dict = { "generated_at": datetime.now().isoformat(timespec="seconds"), "symbol": SYMBOL, "forward_bars": MATCH_FORWARD_BARS, "fee_round_trip_pct": _FEE_PCT, "total_signals": int(len(fires)), "would_trade_count": int(len(traded)), "skipped_count": int(len(fires) - len(traded)), "labeled_count": int(len(with_ret)), "note": ( "모의 forward 수익률. 실계좌·hybrid 복리 PnL 아님. " "매수·매도 leg 미결합 단순 합산." ), } if not with_ret.empty: out["mean_forward_ret_pct"] = round(float(with_ret["forward_ret_pct"].mean()), 4) out["sum_forward_ret_pct"] = round(float(with_ret["forward_ret_pct"].sum()), 4) by_side = ( with_ret.groupby("side")["forward_ret_pct"] .agg(["count", "mean", "sum"]) .round(4) ) out["by_side"] = {k: v.to_dict() for k, v in by_side.iterrows()} by_rule = ( traded.groupby("rule_id") .size() .to_dict() if not traded.empty else {} ) out["fires_by_rule"] = by_rule return out def main() -> None: """paper_fires 로드 → forward % → 리포트 저장.""" fires = load_paper_fires(PAPER_FIRES_LOG) if fires.empty: print(f"[07] 발화 로그 없음: {PAPER_FIRES_LOG}") print(" Phase C 기간 06_execute_live.py (LIVE=0) 상시 실행 후 재시도") return mon = Monitor(cooldown_file=None) df = mon.read_candles_from_db(SYMBOL, MATCH_PRIMARY_INTERVAL, max_rows=50000) if df.empty: df = mon.get_coin_some_data(SYMBOL, MATCH_PRIMARY_INTERVAL) if not isinstance(df.index, pd.DatetimeIndex): df = df.set_index(pd.to_datetime(df["datetime"])) fires = attach_forward_returns(fires, df) report = summarize(fires) PAPER_WEEKLY_REPORT_JSON.parent.mkdir(parents=True, exist_ok=True) PAPER_WEEKLY_REPORT_JSON.write_text( json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8", ) print(f"[07] 저장: {PAPER_WEEKLY_REPORT_JSON}") print(f" 기간 로그: {fires['ts'].min()} ~ {fires['ts'].max()}") print(f" 발화 {report['total_signals']} · 체결가정(would_trade) {report['would_trade_count']}") if "sum_forward_ret_pct" in report: print( f" 모의 forward 합산: {report['sum_forward_ret_pct']}% " f"(평균 {report['mean_forward_ret_pct']}%, " f"{MATCH_FORWARD_BARS}봉 후, 참고용)" ) else: print(" forward 라벨 가능 건 없음 (봉 데이터 부족 또는 발화 없음)") if __name__ == "__main__": main()