Files
kis_bot/scripts/compare_momentum_mode_ob100_whip.py

233 lines
9.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Optuna mode 타점 vs mode+100호가 vs mode+100+휩쏘 — 실엔진 백테 비교.
후처리 표의 「필터후 +23,337」은 체결 재시뮬이고,
이 스크립트는 mode params 로 포트폴리오 백테를 다시 돌린다.
"""
from __future__ import annotations
import json
import sys
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, Optional
ROOT = Path("/home/hoon/kis_bot")
sys.path.insert(0, str(ROOT))
JSON_PATH = ROOT / "kis_trader/backtest/results/optuna_momentum_tpe_20260821_220227.json"
OUT_PATH = ROOT / "kis_trader/backtest/results" / (
f"compare_mom_mode_ob100_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
)
def _pack(label: str, rec: Optional[Dict[str, Any]], note: str = "") -> Dict[str, Any]:
if not isinstance(rec, dict):
return {"label": label, "ok": False, "note": note or "evaluate None"}
return {
"label": label,
"ok": True,
"total_pnl": rec.get("total_pnl"),
"total_trades": rec.get("total_trades"),
"win_rate": rec.get("win_rate"),
"pf": rec.get("pf"),
"mdd": rec.get("mdd"),
"stability_score": rec.get("stability_score"),
"n_losing_days": rec.get("n_losing_days"),
"worst_day_pnl": rec.get("worst_day_pnl"),
"note": note,
}
def main() -> int:
print(f"[1] load {JSON_PATH}", flush=True)
data = json.loads(JSON_PATH.read_text(encoding="utf-8"))
mc = data.get("mode_combo") or {}
mode_params = dict(mc.get("params") or {})
if not mode_params:
print("❌ mode_combo.params 없음", flush=True)
return 1
mode_a = next(
(a for a in ((data.get("postprocess_topn") or {}).get("postprocess_by_anchor") or [])
if a.get("role") == "mode"),
None,
)
if not mode_a:
print("❌ mode 후처리 앵커 없음", flush=True)
return 1
combos = ((mode_a.get("orderbook") or {}).get("combos") or {})
e = combos.get("e") or combos.get("100") or {}
if not e.get("ok"):
print("❌ 100방(e) 없음", flush=True)
return 1
ep = dict(e.get("params") or {})
ws = e.get("whipsaw") if isinstance(e.get("whipsaw"), dict) else None
if not (ws and ws.get("ok")):
ws = mode_a.get("whipsaw") or {}
wp = dict((ws or {}).get("params") or {}) if (ws or {}).get("ok") else {}
start, end = str(data["start"]), str(data["end"])
slot = float(data.get("slot_money") or 300000)
ms = int(data.get("max_stocks") or 8)
bud = float(data.get("total_budget_krw") or 1200000)
grid_keys = list(data.get("grid_keys") or [])
print(
f"[2] prepare momentum {start}~{end} OB로드=on slot={slot} max={ms} bud={bud}",
flush=True,
)
print(f" 100방: spread≤{ep.get('orderbook_max_spread_pct')} ratio≥{ep.get('orderbook_min_bid_ask_ratio')} ask×{ep.get('orderbook_entry_ask_max_mult')}", flush=True)
print(f" 휩쏘: {wp}", flush=True)
print(
f" 후처리참고 필터후 OB {((e.get('recommended_stats') or {}))} / 휩쏘 {((ws or {}).get('recommended_stats') or {})}",
flush=True,
)
from kis_trader.backtest.optuna_momentum import prepare_momentum_search_context
from kis_trader.backtest.param_search_momentum import evaluate_momentum_param_combo
ctx = prepare_momentum_search_context(
start, end, str(data.get("mode") or "tpe"),
slot_money=slot, max_stocks=ms, total_budget_krw=bud,
orderbook_filter="on", # 호가·틱 로드 (필터 ON/OFF는 콤보에서 덮어씀)
history_source="kiwoom",
market="KR",
)
if ctx is None:
print("❌ prepare 실패", flush=True)
return 1
# prepare 가 base_fixed 에 필터 ON 을 넣어도, 케이스별로 명시 덮어씀
base = dict(ctx.base_fixed)
def _eval(combo: Dict[str, Any]) -> Optional[Dict[str, Any]]:
return evaluate_momentum_param_combo(
combo,
base_fixed=base,
grid_keys=grid_keys or list(ctx.grid_keys),
codes_candles=ctx.codes_candles,
min_trades=1,
min_win_rate=0.0,
min_pf=0.0,
universe_by_slot=ctx.universe_by_slot,
slot_money=ctx.slot_money,
max_stocks=ctx.max_stocks,
total_budget_krw=ctx.total_budget_krw,
fee_rate=ctx.fee_rate,
sell_tax=ctx.sell_tax,
period_days=ctx.period_days,
cache_holder=ctx.cache_holder,
ticks_by_code=ctx.ticks_by_code,
orderbook_by_code=ctx.orderbook_by_code,
program_by_code=ctx.program_by_code,
log_verdict_by_code=getattr(ctx, "log_verdict_by_code", None),
start_key=ctx.start_key,
end_key=ctx.end_key,
include_trades=False,
)
cases = []
# A) 타점만 (호가·휩쏘 OFF)
a = dict(mode_params)
a["_orderbook_filter_enabled"] = False
a["whipsaw_enabled"] = False
print("[3A] mode 타점만 (호가OFF·휩쏘OFF)", flush=True)
cases.append(_pack("A_mode_only", _eval(a), "호가OFF·휩쏘OFF"))
# B) +100 진입호가
b = dict(mode_params)
b["_orderbook_filter_enabled"] = True
b["_ob_max_spread_pct"] = float(ep["orderbook_max_spread_pct"])
b["_ob_min_bid_ask_ratio"] = float(ep["orderbook_min_bid_ask_ratio"])
if ep.get("orderbook_entry_ask_max_mult") is not None:
b["_ob_ask_max_mult"] = float(ep["orderbook_entry_ask_max_mult"])
b["whipsaw_enabled"] = False
print("[3B] mode + 100진입호가 (휩쏘OFF)", flush=True)
cases.append(_pack("B_mode_ob100", _eval(b), "호가100 ON · 휩쏘OFF"))
# C) +100 + 휩쏘
c = dict(b)
c["whipsaw_enabled"] = True
if wp.get("whipsaw_subbar_sec") is not None:
c["whipsaw_subbar_sec"] = int(wp["whipsaw_subbar_sec"])
if wp.get("whipsaw_lookback_sec") is not None:
c["whipsaw_lookback_sec"] = int(wp["whipsaw_lookback_sec"])
if wp.get("whipsaw_dip_pct") is not None:
c["whipsaw_dip_pct"] = float(wp["whipsaw_dip_pct"])
print("[3C] mode + 100진입호가 + 휩쏘", flush=True)
cases.append(_pack("C_mode_ob100_whip", _eval(c), "호가100 ON · 휩쏘ON"))
# D) 웹 폼 방금 끝난 결과(참고)
web_path = ROOT / "kis_trader/backtest/results/momentum_bt_20260818_20260821_20260823_152820.json"
web_row = {"label": "D_web_form_done", "ok": False, "note": "파일없음"}
if web_path.is_file():
wj = json.loads(web_path.read_text(encoding="utf-8"))
sm = wj.get("summary") or wj
web_row = {
"label": "D_web_form_done",
"ok": True,
"total_pnl": sm.get("total_pnl"),
"total_trades": sm.get("total_trades"),
"win_rate": sm.get("win_rate"),
"pf": sm.get("profit_factor") or sm.get("pf"),
"mdd": sm.get("max_drawdown") or sm.get("mdd"),
"note": "웹 폼(다른 호가/휩쏘 DB값) 방금 완료 — Optuna 100방과 다름",
}
cases.append(web_row)
# E) Optuna JSON mode 실측 + 후처리 표 숫자(참고)
bt = mc.get("backtest") or {}
cases.append({
"label": "E_optuna_mode_recorded",
"ok": True,
"total_pnl": bt.get("total_pnl"),
"total_trades": bt.get("total_trades"),
"win_rate": bt.get("win_rate"),
"pf": bt.get("pf"),
"note": "Optuna JSON mode_combo.backtest (호가탐색 전 타점 실측)",
})
cases.append({
"label": "F_post_ob100_filter_sim",
"ok": True,
"total_pnl": (e.get("recommended_stats") or {}).get("pnl"),
"total_trades": (e.get("recommended_stats") or {}).get("count"),
"win_rate": (e.get("recommended_stats") or {}).get("win_rate"),
"note": "후처리 필터시뮬(체결 재필터) — 엔진 재백테 아님",
})
cases.append({
"label": "G_post_ob100_whip_filter_sim",
"ok": bool(ws and ws.get("ok")),
"total_pnl": ((ws or {}).get("recommended_stats") or {}).get("pnl"),
"total_trades": ((ws or {}).get("recommended_stats") or {}).get("count"),
"win_rate": ((ws or {}).get("recommended_stats") or {}).get("win_rate"),
"note": "후처리 휩쏘 필터시뮬 — 엔진 재백테 아님",
})
out = {
"created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"source_json": str(JSON_PATH),
"period": f"{start}~{end}",
"ob100": ep,
"whip": wp,
"cases": cases,
}
OUT_PATH.write_text(json.dumps(out, indent=2, ensure_ascii=False), encoding="utf-8")
print("\n===== 비교 =====", flush=True)
for row in cases:
print(
f"{row['label']:28s} ok={row.get('ok')} "
f"trades={row.get('total_trades')} WR={row.get('win_rate')} "
f"PF={row.get('pf')} PnL={row.get('total_pnl')} "
f"| {row.get('note','')}",
flush=True,
)
print(f"\n💾 {OUT_PATH}", flush=True)
print("OK", flush=True)
return 0
if __name__ == "__main__":
raise SystemExit(main())