233 lines
9.0 KiB
Python
233 lines
9.0 KiB
Python
#!/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())
|