ls증권 히스토리 구독 넣음
This commit is contained in:
240
scripts/run_strategy_backtest_cli.py
Normal file
240
scripts/run_strategy_backtest_cli.py
Normal file
@@ -0,0 +1,240 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
전략 백테 CLI 1회 — 웹 API 와 동일 핸들러 (Flask test_request_context).
|
||||
|
||||
python3 -u scripts/run_strategy_backtest_cli.py \\
|
||||
--strategy scalp|breakout|momentum|tail \\
|
||||
--start 2026-07-20 --end 2026-07-22
|
||||
|
||||
꼬리(tail)는 세밀 progress 가 필요하면 scripts/run_tail_backtest_cli.py 를 쓰세요.
|
||||
이 스크립트는 스캘핑·돌파·모멘텀(+ tail 폴백)용.
|
||||
DB 미저장. Optuna 아님.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
from urllib.parse import urlencode
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from kis_trader.utils.kr_trading_day import clamp_to_prev_kr_trading_day
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(levelname)s] %(message)s",
|
||||
datefmt="%H:%M:%S",
|
||||
)
|
||||
logger = logging.getLogger("strategy_bt_cli")
|
||||
|
||||
_ROUTE = {
|
||||
"scalp": ("/api/backtest/scalping", "api_backtest_scalping"),
|
||||
"breakout": ("/api/backtest/breakout", "api_backtest_breakout"),
|
||||
"momentum": ("/api/backtest/momentum", "api_backtest_momentum"),
|
||||
"tail": ("/api/backtest/tail", "api_backtest_tail"),
|
||||
}
|
||||
|
||||
|
||||
def _write_progress(
|
||||
path: Optional[str],
|
||||
*,
|
||||
pct: float,
|
||||
phase: str,
|
||||
message: str = "",
|
||||
) -> None:
|
||||
if not path:
|
||||
return
|
||||
try:
|
||||
payload = {
|
||||
"pct": int(max(0, min(100, round(float(pct))))),
|
||||
"phase": str(phase or ""),
|
||||
"message": str(message or ""),
|
||||
"ts": time.time(),
|
||||
}
|
||||
p = Path(path)
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp = p.with_suffix(".tmp")
|
||||
tmp.write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8")
|
||||
tmp.replace(p)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _bool_arg(val: Optional[str], default: Optional[bool] = None) -> Optional[bool]:
|
||||
if val is None or str(val).strip() == "":
|
||||
return default
|
||||
return str(val).strip().lower() in ("1", "true", "t", "y", "yes", "on")
|
||||
|
||||
|
||||
def _heartbeat(prog_file: Optional[str], stop_evt: threading.Event) -> None:
|
||||
"""엔진 중 조악 progress (웹 핸들러는 내부 progress 없음)."""
|
||||
pct = 30.0
|
||||
while not stop_evt.wait(2.0):
|
||||
pct = min(88.0, pct + 2.5)
|
||||
_write_progress(prog_file, pct=pct, phase="engine", message="웹엔진 실행중")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description="전략 백테 CLI (웹 동일 핸들러)")
|
||||
ap.add_argument(
|
||||
"--strategy",
|
||||
required=True,
|
||||
choices=sorted(_ROUTE.keys()),
|
||||
help="scalp|breakout|momentum|tail",
|
||||
)
|
||||
ap.add_argument("--start", required=True)
|
||||
ap.add_argument("--end", required=True)
|
||||
ap.add_argument("--timeframe", type=int, default=0, help="꼬리만 사용(3/5/15/60), 0=기본")
|
||||
ap.add_argument(
|
||||
"--universe",
|
||||
default="history",
|
||||
help="history|all|sim (전략별 웹 파서 따름)",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--universe-history-source",
|
||||
default="",
|
||||
choices=["", "kiwoom", "ls"],
|
||||
help="이력 테이블: kiwoom(기본) | ls (BACKTEST_UNIVERSE_HISTORY_SOURCE)",
|
||||
)
|
||||
ap.add_argument("--job-id", default="")
|
||||
ap.add_argument("--out-dir", default="")
|
||||
ap.add_argument("--progress-file", default="")
|
||||
ap.add_argument("--orderbook-filter", choices=["off", "on", "auto"], default="off")
|
||||
ap.add_argument("--tick-db", default="", help="1/0/빈칸")
|
||||
ap.add_argument("--env-timeline", action="store_true")
|
||||
ap.add_argument("--params-json", default="", help="미사용(예약) — 웹폼 저장 후 DB 반영 권장")
|
||||
args = ap.parse_args()
|
||||
|
||||
strat = args.strategy.strip().lower()
|
||||
start = clamp_to_prev_kr_trading_day(str(args.start).strip())
|
||||
end = clamp_to_prev_kr_trading_day(str(args.end).strip())
|
||||
if start > end:
|
||||
start, end = end, start
|
||||
|
||||
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
job_id = (args.job_id or f"{strat}_bt_cli_{start.replace('-', '')}_{end.replace('-', '')}_{ts}").strip()
|
||||
out_dir = Path(args.out_dir) if args.out_dir else (ROOT / "kis_trader" / "backtest" / "results")
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
out_json = out_dir / f"{job_id}.json"
|
||||
prog_file = (args.progress_file or "").strip() or None
|
||||
|
||||
logger.info(
|
||||
"🚀 %s BT CLI 시작 job=%s | %s~%s univ=%s hist_src=%s",
|
||||
strat, job_id, start, end, args.universe, args.universe_history_source or "env/kiwoom",
|
||||
)
|
||||
t0 = time.time()
|
||||
_write_progress(prog_file, pct=5, phase="init", message="초기화")
|
||||
|
||||
q: Dict[str, Any] = {
|
||||
"start": start,
|
||||
"end": end,
|
||||
"universe": args.universe or "history",
|
||||
}
|
||||
if (args.universe_history_source or "").strip():
|
||||
q["universe_history_source"] = str(args.universe_history_source).strip()
|
||||
if args.timeframe and int(args.timeframe) > 0:
|
||||
q["timeframe"] = int(args.timeframe)
|
||||
if args.env_timeline:
|
||||
q["env_timeline"] = "1"
|
||||
tick = _bool_arg(args.tick_db, None)
|
||||
if tick is not None:
|
||||
q["backtest_use_tick_db"] = "1" if tick else "0"
|
||||
ob = (args.orderbook_filter or "off").strip().lower()
|
||||
if ob == "off":
|
||||
q["ob_filter"] = "0"
|
||||
elif ob == "on":
|
||||
q["ob_filter"] = "1"
|
||||
|
||||
qs = urlencode({k: str(v) for k, v in q.items() if v is not None and str(v) != ""})
|
||||
path, fn_name = _ROUTE[strat]
|
||||
|
||||
_write_progress(prog_file, pct=20, phase="import", message="웹모듈 로드")
|
||||
import backtest_web as bw # noqa: WPS433 — CLI 전용, 웹과 동일 핸들러
|
||||
|
||||
fn = getattr(bw, fn_name, None)
|
||||
if not callable(fn):
|
||||
logger.error("핸들러 없음: %s", fn_name)
|
||||
_write_progress(prog_file, pct=100, phase="error", message=f"핸들러 없음 {fn_name}")
|
||||
return 1
|
||||
|
||||
stop_hb = threading.Event()
|
||||
hb = threading.Thread(target=_heartbeat, args=(prog_file, stop_hb), daemon=True)
|
||||
hb.start()
|
||||
_write_progress(prog_file, pct=30, phase="engine", message="웹엔진 실행중")
|
||||
try:
|
||||
with bw.app.test_request_context(f"{path}?{qs}"):
|
||||
resp = fn()
|
||||
if isinstance(resp, tuple):
|
||||
resp = resp[0]
|
||||
data = resp.get_json(silent=True) if hasattr(resp, "get_json") else None
|
||||
if not isinstance(data, dict):
|
||||
raise RuntimeError(f"응답 JSON 아님: {type(resp)}")
|
||||
if data.get("error") and not data.get("summary") and not data.get("ok", True):
|
||||
raise RuntimeError(str(data.get("error")))
|
||||
finally:
|
||||
stop_hb.set()
|
||||
try:
|
||||
hb.join(timeout=1.0)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
elapsed = time.time() - t0
|
||||
summary = data.get("summary") or {}
|
||||
trades = data.get("trades") or []
|
||||
out = dict(data)
|
||||
out["ok"] = True
|
||||
out["job_id"] = job_id
|
||||
out["kind"] = "strategy_bt_cli"
|
||||
out["strategy"] = strat
|
||||
out["start"] = start
|
||||
out["end"] = end
|
||||
out["elapsed_sec"] = round(elapsed, 1)
|
||||
out["n_trades"] = len(trades) if isinstance(trades, list) else int(summary.get("total_trades") or 0)
|
||||
out["note"] = f"웹 {path} 동일 핸들러 · DB 미저장 · Optuna 아님"
|
||||
out_json.write_text(json.dumps(out, ensure_ascii=False, indent=2, default=str), encoding="utf-8")
|
||||
|
||||
_write_progress(
|
||||
prog_file,
|
||||
pct=100,
|
||||
phase="done",
|
||||
message=f"완료 trades={out['n_trades']} PnL={summary.get('total_pnl')}",
|
||||
)
|
||||
logger.info(
|
||||
"✅ 완료 %.1fs | trades=%s WR=%s PnL=%s | %s",
|
||||
elapsed,
|
||||
out["n_trades"],
|
||||
summary.get("win_rate"),
|
||||
summary.get("total_pnl"),
|
||||
out_json,
|
||||
)
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"ok": True,
|
||||
"job_id": job_id,
|
||||
"strategy": strat,
|
||||
"result_json": str(out_json),
|
||||
"summary": summary,
|
||||
"elapsed_sec": round(elapsed, 1),
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except Exception as exc:
|
||||
logger.exception("실패: %s", exc)
|
||||
raise SystemExit(1)
|
||||
Reference in New Issue
Block a user