40만 원 기준 시뮬·dry-run 정합 및 hybrid 체결 엔진 통합.
초기 자금 GT_INITIAL_CASH_KRW=400000과 원화 한도 비율(알림·LIVE_ORDER·일한도·손실한도)을 맞추고, dry-run/live 체결을 sim_causal_hybrid(replay)와 동일 경로로 통합한다. 시뮬 리포트 갱신, Phase C 슈퍼바이저·매수매도 리허설 스크립트를 추가한다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -10,6 +10,7 @@ Phase C dry-run 종료 후 모의 수익률(참고) 집계.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import runpy
|
||||
from datetime import datetime
|
||||
@@ -28,7 +29,12 @@ from config import ( # noqa: E402
|
||||
)
|
||||
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
|
||||
from deepcoin.ops.paper_portfolio import PaperPortfolio # noqa: E402
|
||||
from deepcoin.paths import ( # noqa: E402
|
||||
PAPER_FIRES_LOG,
|
||||
PAPER_WEEKLY_REPORT_JSON,
|
||||
PHASE_C_DAILY_DIR,
|
||||
)
|
||||
|
||||
_FEE_PCT = TRADING_FEE_RATE * 2 * 100
|
||||
|
||||
@@ -87,12 +93,22 @@ def attach_forward_returns(fires: pd.DataFrame, close_df: pd.DataFrame) -> pd.Da
|
||||
return fires
|
||||
|
||||
|
||||
def summarize(fires: pd.DataFrame) -> dict:
|
||||
"""집계 dict."""
|
||||
def summarize(fires: pd.DataFrame, *, report_kind: str = "daily") -> dict:
|
||||
"""
|
||||
집계 dict.
|
||||
|
||||
Args:
|
||||
fires: forward_ret_pct 포함 발화 DataFrame.
|
||||
report_kind: daily | final.
|
||||
|
||||
Returns:
|
||||
JSON 직렬화 가능 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"),
|
||||
"report_kind": report_kind,
|
||||
"symbol": SYMBOL,
|
||||
"forward_bars": MATCH_FORWARD_BARS,
|
||||
"fee_round_trip_pct": _FEE_PCT,
|
||||
@@ -101,10 +117,17 @@ def summarize(fires: pd.DataFrame) -> dict:
|
||||
"skipped_count": int(len(fires) - len(traded)),
|
||||
"labeled_count": int(len(with_ret)),
|
||||
"note": (
|
||||
"모의 forward 수익률. 실계좌·hybrid 복리 PnL 아님. "
|
||||
"매수·매도 leg 미결합 단순 합산."
|
||||
"forward %는 발화별 참고 지표. "
|
||||
"총보유금액(equity)은 paper_portfolio 모의 체결 기준."
|
||||
),
|
||||
}
|
||||
if not fires.empty and "ts" in fires.columns:
|
||||
out["log_from"] = str(fires["ts"].min())
|
||||
out["log_to"] = str(fires["ts"].max())
|
||||
buy_n = int((traded["side"] == "buy").sum()) if not traded.empty else 0
|
||||
sell_n = int((traded["side"] == "sell").sum()) if not traded.empty else 0
|
||||
out["buy_fires"] = buy_n
|
||||
out["sell_fires"] = sell_n
|
||||
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)
|
||||
@@ -125,13 +148,25 @@ def summarize(fires: pd.DataFrame) -> dict:
|
||||
return out
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""paper_fires 로드 → forward % → 리포트 저장."""
|
||||
fires = load_paper_fires(PAPER_FIRES_LOG)
|
||||
def build_phase_c_report(
|
||||
fires_path: Path | None = None,
|
||||
*,
|
||||
report_kind: str = "daily",
|
||||
) -> tuple[dict, pd.DataFrame]:
|
||||
"""
|
||||
paper_fires 로드 → forward % → 리포트 dict.
|
||||
|
||||
Args:
|
||||
fires_path: jsonl 경로 (기본 PAPER_FIRES_LOG).
|
||||
report_kind: daily | final.
|
||||
|
||||
Returns:
|
||||
(report, fires_with_returns) — 발화 없으면 ({}, empty DataFrame).
|
||||
"""
|
||||
path = fires_path or PAPER_FIRES_LOG
|
||||
fires = load_paper_fires(path)
|
||||
if fires.empty:
|
||||
print(f"[07] 발화 로그 없음: {PAPER_FIRES_LOG}")
|
||||
print(" Phase C 기간 06_execute_live.py (LIVE=0) 상시 실행 후 재시도")
|
||||
return
|
||||
return {}, fires
|
||||
|
||||
mon = Monitor(cooldown_file=None)
|
||||
df = mon.read_candles_from_db(SYMBOL, MATCH_PRIMARY_INTERVAL, max_rows=50000)
|
||||
@@ -141,24 +176,172 @@ def main() -> None:
|
||||
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",
|
||||
)
|
||||
report = summarize(fires, report_kind=report_kind)
|
||||
|
||||
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']}")
|
||||
mark = float(df["Close"].iloc[-1]) if not df.empty and "Close" in df.columns else 0.0
|
||||
paper = PaperPortfolio.load()
|
||||
report["paper_portfolio"] = paper.summary(mark)
|
||||
return report, fires
|
||||
|
||||
|
||||
def format_report_text(report: dict) -> str:
|
||||
"""사람이 읽기 쉬운 요약 텍스트."""
|
||||
kind = report.get("report_kind", "daily")
|
||||
title = "Phase C 최종 보고" if kind == "final" else "Phase C 중간 보고"
|
||||
lines = [
|
||||
f"=== {title} ({report.get('generated_at', '')}) ===",
|
||||
f"심볼: {report.get('symbol', '')}",
|
||||
]
|
||||
pf = report.get("paper_portfolio") or {}
|
||||
if pf:
|
||||
lines.extend(
|
||||
[
|
||||
"--- 모의 계좌 (dry-run, 빗썸 잔고 미사용) ---",
|
||||
f"초기 자금: ₩{pf.get('initial_cash_krw', 0):,.0f}",
|
||||
f"현금: ₩{pf.get('cash_krw', 0):,.0f} · "
|
||||
f"보유 {pf.get('qty', 0):.4f} {report.get('symbol', '')} "
|
||||
f"(평가단가 ₩{pf.get('mark_price', 0):,.0f})",
|
||||
f"코인 평가: ₩{pf.get('coin_value_krw', 0):,.0f}",
|
||||
f"총보유금액: ₩{pf.get('equity_krw', 0):,.0f} "
|
||||
f"(손익 ₩{pf.get('pnl_krw', 0):+,.0f} / {pf.get('pnl_pct', 0):+.2f}%)",
|
||||
]
|
||||
)
|
||||
lines.append(
|
||||
f"발화 합계: {report.get('total_signals', 0)} "
|
||||
f"(체결 {report.get('would_trade_count', 0)}, "
|
||||
f"매수 {report.get('buy_fires', 0)} / 매도 {report.get('sell_fires', 0)})"
|
||||
)
|
||||
if "log_from" in report:
|
||||
lines.append(f"로그 구간: {report['log_from']} ~ {report['log_to']}")
|
||||
if "sum_forward_ret_pct" in report:
|
||||
print(
|
||||
f" 모의 forward 합산: {report['sum_forward_ret_pct']}% "
|
||||
lines.append(
|
||||
f"모의 forward 합산: {report['sum_forward_ret_pct']}% "
|
||||
f"(평균 {report['mean_forward_ret_pct']}%, "
|
||||
f"{MATCH_FORWARD_BARS}봉 후, 참고용)"
|
||||
f"{report.get('forward_bars')}봉 후, 참고용)"
|
||||
)
|
||||
else:
|
||||
print(" forward 라벨 가능 건 없음 (봉 데이터 부족 또는 발화 없음)")
|
||||
lines.append("모의 forward: 라벨 가능 건 없음 (봉·발화 부족)")
|
||||
lines.append(report.get("note", ""))
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def write_report_outputs(
|
||||
report: dict,
|
||||
*,
|
||||
json_path: Path | None = None,
|
||||
text_path: Path | None = None,
|
||||
) -> None:
|
||||
"""JSON·텍스트 리포트 저장."""
|
||||
if json_path:
|
||||
json_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
json_path.write_text(
|
||||
json.dumps(report, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
if text_path:
|
||||
text_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
text_path.write_text(format_report_text(report), encoding="utf-8")
|
||||
|
||||
|
||||
def append_verification_log(report: dict, verification_md: Path) -> None:
|
||||
"""live_verification 일별 표 해당 날짜 행 갱신."""
|
||||
if not verification_md.is_file():
|
||||
return
|
||||
text = verification_md.read_text(encoding="utf-8")
|
||||
iso = report.get("generated_at", "")[:10]
|
||||
try:
|
||||
y, m, d = iso.split("-")
|
||||
day_label = f"{int(m)}/{int(d)}"
|
||||
except ValueError:
|
||||
return
|
||||
buy = report.get("buy_fires", 0)
|
||||
sell = report.get("sell_fires", 0)
|
||||
pf = report.get("paper_portfolio") or {}
|
||||
equity = pf.get("equity_krw", "-")
|
||||
pnl_pct = pf.get("pnl_pct", "-")
|
||||
kind = report.get("report_kind", "daily")
|
||||
memo = "C 최종" if kind == "final" else "중간보고"
|
||||
row = (
|
||||
f"| {day_label} | Y | - | Y | {buy} | {sell} | "
|
||||
f"총₩{equity} ({pnl_pct}%) {memo} |"
|
||||
)
|
||||
marker = "### 일별 기록"
|
||||
if marker not in text:
|
||||
return
|
||||
head, table = text.split(marker, 1)
|
||||
lines = table.splitlines()
|
||||
new_lines: list[str] = []
|
||||
replaced = False
|
||||
for line in lines:
|
||||
if line.startswith(f"| {day_label} |"):
|
||||
new_lines.append(row)
|
||||
replaced = True
|
||||
else:
|
||||
new_lines.append(line)
|
||||
if not replaced:
|
||||
new_lines.append(row)
|
||||
verification_md.write_text(head + marker + "\n".join(new_lines), encoding="utf-8")
|
||||
|
||||
|
||||
def run_report(
|
||||
*,
|
||||
report_kind: str = "daily",
|
||||
stamp: str | None = None,
|
||||
update_verification: bool = True,
|
||||
) -> dict:
|
||||
"""
|
||||
리포트 생성·저장·콘솔 출력.
|
||||
|
||||
Args:
|
||||
report_kind: daily | final.
|
||||
stamp: 파일명용 타임스탬프 (기본 now).
|
||||
update_verification: live_verification md 갱신 여부.
|
||||
|
||||
Returns:
|
||||
report dict (빈 dict 가능).
|
||||
"""
|
||||
report, fires = build_phase_c_report(report_kind=report_kind)
|
||||
if not report:
|
||||
print(f"[07] 발화 로그 없음: {PAPER_FIRES_LOG}")
|
||||
print(" Phase C 기간 06_execute_live.py (LIVE=0) 상시 실행 후 재시도")
|
||||
return {}
|
||||
|
||||
stamp = stamp or datetime.now().strftime("%Y%m%d_%H%M")
|
||||
daily_dir = PHASE_C_DAILY_DIR
|
||||
write_report_outputs(
|
||||
report,
|
||||
json_path=daily_dir / f"report_{stamp}_{report_kind}.json",
|
||||
text_path=daily_dir / f"report_{stamp}_{report_kind}.txt",
|
||||
)
|
||||
write_report_outputs(report, json_path=PAPER_WEEKLY_REPORT_JSON)
|
||||
if update_verification:
|
||||
append_verification_log(
|
||||
report,
|
||||
Path(__file__).resolve().parents[1]
|
||||
/ "docs/05_ops/live_verification_20260601.md",
|
||||
)
|
||||
|
||||
print(format_report_text(report))
|
||||
print(f"[07] JSON: {PAPER_WEEKLY_REPORT_JSON}")
|
||||
print(f"[07] 일별: {daily_dir}/report_{stamp}_{report_kind}.*")
|
||||
return report
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""CLI: paper_fires → forward % → 리포트 저장."""
|
||||
parser = argparse.ArgumentParser(description="Phase C 모의 forward % 집계")
|
||||
parser.add_argument(
|
||||
"--kind",
|
||||
choices=("daily", "final"),
|
||||
default="daily",
|
||||
help="daily=중간, final=금요일 최종",
|
||||
)
|
||||
parser.add_argument("--no-verification-md", action="store_true")
|
||||
args = parser.parse_args()
|
||||
run_report(
|
||||
report_kind=args.kind,
|
||||
update_verification=not args.no_verification_md,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user