Files
kis_bot/scripts/post_optuna_mid_enroll_verify.py
2026-07-30 18:05:07 +09:00

215 lines
7.6 KiB
Python
Executable File

#!/usr/bin/env python3
"""Optuna apply 이후 — 현재 DB로 웹정렬 백테 1회씩 + 요약 리포트.
.venv/bin/python -u scripts/post_optuna_mid_enroll_verify.py \\
--start 2026-07-21 --end 2026-07-23 --report logs/report.txt
"""
from __future__ import annotations
import argparse
import json
import subprocess
import sys
import time
from pathlib import Path
from typing import Any, Dict, List
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
def _run_kr_cli(strat: str, start: str, end: str) -> Dict[str, Any]:
py = ROOT / ".venv" / "bin" / "python"
if not py.exists():
py = Path(sys.executable)
out_json = ROOT / "logs" / f"verify_bt_{strat}_{int(time.time())}.json"
cmd = [
str(py), "-u", str(ROOT / "scripts" / "run_strategy_backtest_cli.py"),
"--strategy", strat,
"--start", start,
"--end", end,
"--universe", "history",
]
# CLI may print summary; also try to capture via env if supported
p = subprocess.run(cmd, cwd=str(ROOT), capture_output=True, text=True, timeout=3600)
text = (p.stdout or "") + "\n" + (p.stderr or "")
summary: Dict[str, Any] = {
"strategy": strat,
"rc": p.returncode,
"tail": "\n".join(text.strip().splitlines()[-40:]),
}
# parse common lines
for line in text.splitlines():
low = line.lower()
if "total_pnl" in low or "총손익" in line or "trades=" in low or "거래" in line:
summary.setdefault("hits", []).append(line.strip())
out_json.write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8")
summary["log_json"] = str(out_json)
return summary
def _run_us_web(start: str, end: str, symbol: str = "") -> Dict[str, Any]:
"""US 웹백테 — Flask 핸들러와 동일 경로가 아니면 엔진 params+포트폴리오 요약만."""
from database import TradeDB
from kis_trader.utils.env import invalidate_merged_env_cache
from kis_trader.backtest import scalping_backtest_common as sbc
from kis_trader.backtest.us_momentum_web_backtest import (
build_us_momentum_engine_params,
load_us_candles,
)
from kis_trader.engine.momentum_engine import get_momentum_defaults_from_db
invalidate_merged_env_cache()
db = TradeDB()
ui_def = get_momentum_defaults_from_db()
args: Dict[str, Any] = {"start": start, "end": end, "use_saved_history": True}
if symbol:
args["symbol"] = symbol
args["use_stock_cfg"] = True
try:
from kis_trader.backtest.us_momentum_web_backtest import run_us_momentum_web_backtest
out = run_us_momentum_web_backtest(args, db=db, ui_def=ui_def)
trades = out.get("trades") or out.get("trade_list") or []
if not isinstance(trades, list):
trades = []
pnl = sum(float(t.get("pnl") or 0) for t in trades)
wins = sum(1 for t in trades if float(t.get("pnl") or 0) > 0)
n = len(trades)
return {
"strategy": f"us_momentum:{symbol or 'GLOBAL'}",
"rc": 0,
"trades": n,
"wins": wins,
"pnl": pnl,
"wr": (wins / n * 100.0) if n else 0.0,
}
except Exception as e:
# 최소 로드 스모크
filt = [symbol] if symbol else None
codes_candles, _, _, codes = load_us_candles(db, start, end, codes_filter=filt)
return {
"strategy": f"us_momentum:{symbol or 'GLOBAL'}",
"rc": 1,
"error": str(e),
"codes": len(codes or []),
"candle_codes": len(codes_candles or {}),
"trades": 0,
"wins": 0,
"pnl": 0.0,
"wr": 0.0,
}
def _latest_optuna_json(prefix: str) -> str:
d = ROOT / "kis_trader" / "backtest" / "results"
files = sorted(d.glob(f"optuna_{prefix}*.json"), key=lambda p: p.stat().st_mtime)
return str(files[-1]) if files else ""
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--start", required=True)
ap.add_argument("--end", required=True)
ap.add_argument("--report", required=True)
ap.add_argument(
"--us-only",
action="store_true",
help="해외 모멘텀 스모크만 (국장 CLI 스킵)",
)
args = ap.parse_args()
lines: List[str] = []
lines.append(f"# mid-enroll verify {args.start}..{args.end}")
lines.append(f"ts={time.strftime('%Y-%m-%d %H:%M:%S')}")
if args.us_only:
lines.append("scope=us_only")
lines.append("")
# 최근 Optuna JSON 경로
prefs = [
"us_momentum_tpe_",
"us_momentum_tpe_fee_",
"us_momentum_QQQM_",
"us_momentum_SPCX_",
"us_momentum_SPYM_",
"us_momentum_TSLA_",
]
if not args.us_only:
prefs.extend(
(
"momentum_tpe_",
"tail_tpe_",
"breakout_tpe_",
"scalp_tpe_",
)
)
for pref in prefs:
p = _latest_optuna_json(pref)
if p:
lines.append(f"json:{pref} -> {p}")
if not args.us_only:
lines.append("")
lines.append("## KR web-aligned BT (current DB after apply)")
for strat in ("momentum", "tail", "breakout", "scalp"):
print(f"[verify] KR {strat} ...", flush=True)
try:
s = _run_kr_cli(strat, args.start, args.end)
lines.append(f"### {strat} rc={s.get('rc')}")
for h in s.get("hits") or []:
lines.append(f" {h}")
lines.append(f" log={s.get('log_json')}")
except Exception as e:
lines.append(f"### {strat} ERR {e}")
lines.append("")
lines.append("## US web BT (global + cfg symbols)")
for sym in ("", "QQQM", "SPCX", "SPYM", "TSLA"):
tag = sym or "GLOBAL"
print(f"[verify] US {tag} ...", flush=True)
try:
s = _run_us_web(args.start, args.end, sym)
lines.append(
f"### us:{tag} trades={s.get('trades')} wr={s.get('wr'):.1f}% "
f"pnl={s.get('pnl'):.4f}"
)
if s.get("error"):
lines.append(f" error={s.get('error')}")
except Exception as e:
lines.append(f"### us:{tag} ERR {e}")
# 실매 봇 재시작 (apply 반영)
lines.append("")
lines.append("## restart kis_trader_main")
try:
r = subprocess.run(
["sudo", "systemctl", "restart", "kis_trader_main.service"],
capture_output=True, text=True, timeout=120,
)
time.sleep(2)
a = subprocess.run(
["systemctl", "is-active", "kis_trader_main.service"],
capture_output=True, text=True, timeout=30,
)
lines.append(f"restart_rc={r.returncode} active={a.stdout.strip()}")
except Exception as e:
lines.append(f"restart ERR {e}")
# 실매 정합 메모
lines.append("")
lines.append("## live match notes")
lines.append("- ALIGN_DEFER_MID_BAR_ENROLL + LIVE_ALIGN_ENTRY_FROM_TICK_BUFFER 가 실매·백테 공통")
lines.append("- 장중 실매 체결 로그의 entry_src=ws_ticks|ohlc_open / 탈락-중편입시가애매 로 확인")
lines.append("- 미장 10시(22:00 KST) 전 us apply 완료 여부: master 로그 US 구간 END 시각 확인")
report = Path(args.report)
report.parent.mkdir(parents=True, exist_ok=True)
report.write_text("\n".join(lines) + "\n", encoding="utf-8")
print("\n".join(lines), flush=True)
print(f"REPORT={report}", flush=True)
return 0
if __name__ == "__main__":
raise SystemExit(main())