옵투나 8방 후처리를 재탐색으로 변경하기 전전
This commit is contained in:
232
scripts/compare_momentum_mode_ob100_whip.py
Normal file
232
scripts/compare_momentum_mode_ob100_whip.py
Normal file
@@ -0,0 +1,232 @@
|
||||
#!/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())
|
||||
177
scripts/momentum_optuna_parity_merged_snap.py
Normal file
177
scripts/momentum_optuna_parity_merged_snap.py
Normal file
@@ -0,0 +1,177 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Optuna JSON 재평가 — 당시 merged_params 고정축 스냅샷 사용 (DB 미변경)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import time
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument(
|
||||
"--json",
|
||||
default="kis_trader/backtest/results/optuna_momentum_tpe_20260821_220227.json",
|
||||
)
|
||||
ap.add_argument("--out", required=True)
|
||||
args = ap.parse_args()
|
||||
|
||||
t0 = time.time()
|
||||
json_path = Path(args.json)
|
||||
out_path = Path(args.out)
|
||||
d = json.loads(json_path.read_text())
|
||||
start, end = d["start"], d["end"]
|
||||
slot = float(d["slot_money"])
|
||||
max_stocks = int(d["max_stocks"])
|
||||
budget = float(d["total_budget_krw"])
|
||||
grid_keys = list(d["grid_keys"])
|
||||
|
||||
t199 = next(x for x in d["results_all"] if x.get("optuna_trial_number") == 199)
|
||||
mp = dict(t199["merged_params"])
|
||||
p199 = dict(t199["params"])
|
||||
mode_params = dict(d["mode_combo"]["params"])
|
||||
|
||||
# Optuna 당시 고정축 (지금 DB 기본값과 다를 수 있음 — RSI 등)
|
||||
fixed_snap = {k: v for k, v in mp.items() if k not in p199}
|
||||
fixed_snap["_orderbook_filter_enabled"] = False
|
||||
for k in list(fixed_snap.keys()):
|
||||
if "whipsaw" in k.lower() and isinstance(fixed_snap[k], (bool, int)):
|
||||
fixed_snap[k] = False if isinstance(fixed_snap[k], bool) else 0
|
||||
|
||||
from kis_trader.backtest.optuna_momentum import prepare_momentum_search_context
|
||||
from kis_trader.backtest.param_search_momentum import evaluate_momentum_param_combo
|
||||
|
||||
print(
|
||||
f"prepare {start}~{end} OB=off merged-fixed "
|
||||
f"rsi={fixed_snap.get('mom_rsi_min')}/{fixed_snap.get('mom_rsi_max')}",
|
||||
flush=True,
|
||||
)
|
||||
ctx = prepare_momentum_search_context(
|
||||
start,
|
||||
end,
|
||||
"tpe",
|
||||
slot_money=slot,
|
||||
max_stocks=max_stocks,
|
||||
total_budget_krw=budget,
|
||||
orderbook_filter="off",
|
||||
market="KR",
|
||||
history_source="kiwoom",
|
||||
)
|
||||
if ctx is None:
|
||||
print("prepare failed", flush=True)
|
||||
return 1
|
||||
|
||||
base = dict(ctx.base_fixed)
|
||||
base.update(fixed_snap)
|
||||
base["_orderbook_filter_enabled"] = False
|
||||
|
||||
def run_one(label, combo, recorded):
|
||||
print(f"--- eval {label} ---", flush=True)
|
||||
r = evaluate_momentum_param_combo(
|
||||
combo,
|
||||
base_fixed=base,
|
||||
grid_keys=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=ctx.log_verdict_by_code,
|
||||
start_key=ctx.start_key,
|
||||
end_key=ctx.end_key,
|
||||
include_trades=False,
|
||||
)
|
||||
if not r:
|
||||
return {"label": label, "ok": False, "error": "None"}
|
||||
out = {
|
||||
"label": label,
|
||||
"ok": True,
|
||||
"reeval": {
|
||||
"total_pnl": r.get("total_pnl"),
|
||||
"total_trades": r.get("total_trades"),
|
||||
"win_rate": r.get("win_rate"),
|
||||
"pf": r.get("pf"),
|
||||
},
|
||||
"recorded": recorded,
|
||||
"delta_pnl": float(r.get("total_pnl") or 0) - float(recorded.get("total_pnl") or 0),
|
||||
"delta_trades": int(r.get("total_trades") or 0)
|
||||
- int(recorded.get("total_trades") or 0),
|
||||
"match": (
|
||||
int(r.get("total_pnl") or 0) == int(recorded.get("total_pnl") or 0)
|
||||
and int(r.get("total_trades") or 0)
|
||||
== int(recorded.get("total_trades") or 0)
|
||||
),
|
||||
}
|
||||
print(json.dumps(out, ensure_ascii=False), flush=True)
|
||||
return out
|
||||
|
||||
rows = [
|
||||
run_one(
|
||||
"#199",
|
||||
p199,
|
||||
{
|
||||
"total_pnl": t199["total_pnl"],
|
||||
"total_trades": t199["total_trades"],
|
||||
"win_rate": t199["win_rate"],
|
||||
"pf": t199["pf"],
|
||||
},
|
||||
),
|
||||
run_one(
|
||||
"mode_combo",
|
||||
mode_params,
|
||||
{
|
||||
"total_pnl": d["mode_combo"]["backtest"]["total_pnl"],
|
||||
"total_trades": d["mode_combo"]["backtest"]["total_trades"],
|
||||
"win_rate": d["mode_combo"]["backtest"]["win_rate"],
|
||||
"pf": d["mode_combo"]["backtest"]["pf"],
|
||||
},
|
||||
),
|
||||
]
|
||||
report = {
|
||||
"method": "merged_fixed_snapshot + combo",
|
||||
"source_json": str(json_path),
|
||||
"db_touched": False,
|
||||
"elapsed_sec": round(time.time() - t0, 1),
|
||||
"fixed_rsi": [fixed_snap.get("mom_rsi_min"), fixed_snap.get("mom_rsi_max")],
|
||||
"comparisons": rows,
|
||||
}
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
out_path.write_text(json.dumps(report, ensure_ascii=False, indent=2))
|
||||
print("WROTE", out_path, flush=True)
|
||||
print(
|
||||
"FINAL",
|
||||
json.dumps(
|
||||
{
|
||||
r["label"]: {
|
||||
"match": r.get("match"),
|
||||
"delta_pnl": r.get("delta_pnl"),
|
||||
"delta_trades": r.get("delta_trades"),
|
||||
"reeval": r.get("reeval"),
|
||||
}
|
||||
for r in rows
|
||||
},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
flush=True,
|
||||
)
|
||||
return 0 if all(r.get("match") for r in rows if r.get("ok")) else 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
raise SystemExit(1)
|
||||
205
scripts/momentum_optuna_parity_reeval.py
Normal file
205
scripts/momentum_optuna_parity_reeval.py
Normal file
@@ -0,0 +1,205 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Optuna JSON(#199·mode) vs 동일 엔진 재평가 — DB 미변경.
|
||||
|
||||
Usage:
|
||||
python3 -u scripts/momentum_optuna_parity_reeval.py \\
|
||||
--json kis_trader/backtest/results/optuna_momentum_tpe_20260821_220227.json \\
|
||||
--out logs/momentum_optuna_parity_OUT.json
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import time
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument(
|
||||
"--json",
|
||||
default="kis_trader/backtest/results/optuna_momentum_tpe_20260821_220227.json",
|
||||
)
|
||||
ap.add_argument("--out", required=True)
|
||||
args = ap.parse_args()
|
||||
|
||||
t0 = time.time()
|
||||
json_path = Path(args.json)
|
||||
out_path = Path(args.out)
|
||||
d = json.loads(json_path.read_text())
|
||||
start, end = d["start"], d["end"]
|
||||
slot = float(d["slot_money"])
|
||||
max_stocks = int(d["max_stocks"])
|
||||
budget = float(d["total_budget_krw"])
|
||||
grid_keys = list(d["grid_keys"])
|
||||
|
||||
t199 = next(x for x in d["results_all"] if x.get("optuna_trial_number") == 199)
|
||||
mode_params = dict(d["mode_combo"]["params"])
|
||||
p199 = dict(t199["params"])
|
||||
|
||||
from kis_trader.backtest.optuna_momentum import prepare_momentum_search_context
|
||||
from kis_trader.backtest.param_search_momentum import evaluate_momentum_param_combo
|
||||
|
||||
print(
|
||||
f"prepare {start}~{end} slot={slot} max={max_stocks} budget={budget} OB=off",
|
||||
flush=True,
|
||||
)
|
||||
ctx = prepare_momentum_search_context(
|
||||
start,
|
||||
end,
|
||||
"tpe",
|
||||
slot_money=slot,
|
||||
max_stocks=max_stocks,
|
||||
total_budget_krw=budget,
|
||||
orderbook_filter="off",
|
||||
market="KR",
|
||||
)
|
||||
if ctx is None:
|
||||
print("prepare failed", flush=True)
|
||||
return 1
|
||||
|
||||
# Optuna 학습과 동일: 호가 OFF. 휩쏘 키는 있으면 OFF (DB ON이어도 비교용 고정)
|
||||
base = dict(ctx.base_fixed)
|
||||
base["_orderbook_filter_enabled"] = False
|
||||
for k in list(base.keys()):
|
||||
if "whipsaw" in k.lower() and isinstance(base[k], (bool, int)):
|
||||
base[k] = False if isinstance(base[k], bool) else 0
|
||||
|
||||
print(
|
||||
f"ctx ready candles={len(ctx.codes_candles)} "
|
||||
f"ticks={'yes' if ctx.ticks_by_code else 'no'} "
|
||||
f"ob_snaps={len(ctx.orderbook_by_code)} fee={ctx.fee_rate} tax={ctx.sell_tax}",
|
||||
flush=True,
|
||||
)
|
||||
print(
|
||||
f"base tick_entry={base.get('backtest_use_tick_entry')} "
|
||||
f"tick_exit={base.get('backtest_use_tick_exit')} "
|
||||
f"ob={base.get('_orderbook_filter_enabled')} "
|
||||
f"whip_keys={[k for k in base if 'whip' in k.lower()]}",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
def run_one(label, combo, recorded):
|
||||
print(f"--- eval {label} ---", flush=True)
|
||||
r = evaluate_momentum_param_combo(
|
||||
combo,
|
||||
base_fixed=base,
|
||||
grid_keys=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=ctx.log_verdict_by_code,
|
||||
start_key=ctx.start_key,
|
||||
end_key=ctx.end_key,
|
||||
include_trades=False,
|
||||
)
|
||||
if not r:
|
||||
return {"label": label, "ok": False, "error": "evaluate returned None"}
|
||||
out = {
|
||||
"label": label,
|
||||
"ok": True,
|
||||
"reeval": {
|
||||
"total_pnl": r.get("total_pnl"),
|
||||
"total_trades": r.get("total_trades"),
|
||||
"win_rate": r.get("win_rate"),
|
||||
"pf": r.get("pf"),
|
||||
"mdd": r.get("mdd"),
|
||||
},
|
||||
"recorded": recorded,
|
||||
"delta_pnl": float(r.get("total_pnl") or 0) - float(recorded.get("total_pnl") or 0),
|
||||
"delta_trades": int(r.get("total_trades") or 0)
|
||||
- int(recorded.get("total_trades") or 0),
|
||||
"match": (
|
||||
int(r.get("total_pnl") or 0) == int(recorded.get("total_pnl") or 0)
|
||||
and int(r.get("total_trades") or 0)
|
||||
== int(recorded.get("total_trades") or 0)
|
||||
),
|
||||
}
|
||||
print(json.dumps(out, ensure_ascii=False), flush=True)
|
||||
return out
|
||||
|
||||
rows = [
|
||||
run_one(
|
||||
"#199",
|
||||
p199,
|
||||
{
|
||||
"total_pnl": t199["total_pnl"],
|
||||
"total_trades": t199["total_trades"],
|
||||
"win_rate": t199["win_rate"],
|
||||
"pf": t199["pf"],
|
||||
},
|
||||
),
|
||||
run_one(
|
||||
"mode_combo",
|
||||
mode_params,
|
||||
{
|
||||
"total_pnl": d["mode_combo"]["backtest"]["total_pnl"],
|
||||
"total_trades": d["mode_combo"]["backtest"]["total_trades"],
|
||||
"win_rate": d["mode_combo"]["backtest"]["win_rate"],
|
||||
"pf": d["mode_combo"]["backtest"]["pf"],
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
report = {
|
||||
"source_json": str(json_path),
|
||||
"period": {"start": start, "end": end},
|
||||
"portfolio": {
|
||||
"slot_money": slot,
|
||||
"max_stocks": max_stocks,
|
||||
"total_budget_krw": budget,
|
||||
},
|
||||
"orderbook": "off",
|
||||
"db_touched": False,
|
||||
"elapsed_sec": round(time.time() - t0, 1),
|
||||
"comparisons": rows,
|
||||
"note": (
|
||||
"Optuna evaluate 경로 재실행"
|
||||
"(웹엔진=run_momentum_backtest_web_aligned). DB 미변경."
|
||||
),
|
||||
"web_bt_note": (
|
||||
"지금 웹폼(호가ON+휩쏘ON)과 숫자 비교가 아님. "
|
||||
"정합=JSON기록 vs 동일조건 재시뮬."
|
||||
),
|
||||
}
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
out_path.write_text(json.dumps(report, ensure_ascii=False, indent=2))
|
||||
print("WROTE", out_path, flush=True)
|
||||
print(
|
||||
"FINAL",
|
||||
json.dumps(
|
||||
{
|
||||
r["label"]: {
|
||||
"match": r.get("match"),
|
||||
"delta_pnl": r.get("delta_pnl"),
|
||||
"delta_trades": r.get("delta_trades"),
|
||||
"reeval": r.get("reeval"),
|
||||
}
|
||||
for r in rows
|
||||
},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
flush=True,
|
||||
)
|
||||
return 0 if all(r.get("match") for r in rows if r.get("ok")) else 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
raise SystemExit(1)
|
||||
210
scripts/momentum_optuna_trade_diff.py
Normal file
210
scripts/momentum_optuna_trade_diff.py
Normal file
@@ -0,0 +1,210 @@
|
||||
#!/usr/bin/env python3
|
||||
"""#199 Optuna 기록 daily_pnl vs 재시뮬 체결 diff (DB 미변경)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import time
|
||||
import traceback
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _trade_key(t: dict) -> str:
|
||||
code = str(t.get("code") or t.get("ticker") or "")
|
||||
buy = str(t.get("buy_time") or t.get("entry_time") or t.get("entry_ts") or "")
|
||||
sell = str(t.get("sell_time") or t.get("exit_time") or t.get("exit_ts") or "")
|
||||
return f"{code}|{buy}|{sell}"
|
||||
|
||||
|
||||
def _day_of(t: dict) -> str:
|
||||
for k in ("sell_time", "exit_time", "buy_time", "entry_time"):
|
||||
v = str(t.get(k) or "")
|
||||
if len(v) >= 8 and v[:8].isdigit():
|
||||
return f"{v[:4]}-{v[4:6]}-{v[6:8]}"
|
||||
return "?"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument(
|
||||
"--json",
|
||||
default="kis_trader/backtest/results/optuna_momentum_tpe_20260821_220227.json",
|
||||
)
|
||||
ap.add_argument("--out", required=True)
|
||||
ap.add_argument("--trial", type=int, default=199)
|
||||
args = ap.parse_args()
|
||||
|
||||
t0 = time.time()
|
||||
d = json.loads(Path(args.json).read_text())
|
||||
trial = next(
|
||||
x for x in d["results_all"] if x.get("optuna_trial_number") == args.trial
|
||||
)
|
||||
grid_keys = list(d["grid_keys"])
|
||||
params = dict(trial["params"])
|
||||
mp = dict(trial["merged_params"])
|
||||
fixed = {k: v for k, v in mp.items() if k not in params}
|
||||
fixed["_orderbook_filter_enabled"] = False
|
||||
|
||||
from kis_trader.backtest.optuna_momentum import prepare_momentum_search_context
|
||||
from kis_trader.backtest.param_search_momentum import evaluate_momentum_param_combo
|
||||
|
||||
print(
|
||||
f"prepare {d['start']}~{d['end']} trial=#{args.trial} OB=off include_trades",
|
||||
flush=True,
|
||||
)
|
||||
ctx = prepare_momentum_search_context(
|
||||
d["start"],
|
||||
d["end"],
|
||||
"tpe",
|
||||
slot_money=float(d["slot_money"]),
|
||||
max_stocks=int(d["max_stocks"]),
|
||||
total_budget_krw=float(d["total_budget_krw"]),
|
||||
orderbook_filter="off",
|
||||
market="KR",
|
||||
history_source="kiwoom",
|
||||
)
|
||||
if ctx is None:
|
||||
print("prepare failed", flush=True)
|
||||
return 1
|
||||
|
||||
base = dict(ctx.base_fixed)
|
||||
base.update(fixed)
|
||||
base["_orderbook_filter_enabled"] = False
|
||||
|
||||
r = evaluate_momentum_param_combo(
|
||||
params,
|
||||
base_fixed=base,
|
||||
grid_keys=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=ctx.log_verdict_by_code,
|
||||
start_key=ctx.start_key,
|
||||
end_key=ctx.end_key,
|
||||
include_trades=True,
|
||||
)
|
||||
if not r:
|
||||
print("evaluate None", flush=True)
|
||||
return 1
|
||||
|
||||
trades = list(r.get("_trades") or [])
|
||||
daily_reeval: dict = defaultdict(float)
|
||||
by_code: dict = defaultdict(lambda: {"n": 0, "pnl": 0.0})
|
||||
slim = []
|
||||
for t in trades:
|
||||
pnl = float(t.get("pnl") or 0)
|
||||
day = _day_of(t)
|
||||
daily_reeval[day] += pnl
|
||||
code = str(t.get("code") or "")
|
||||
by_code[code]["n"] += 1
|
||||
by_code[code]["pnl"] += pnl
|
||||
slim.append(
|
||||
{
|
||||
"key": _trade_key(t),
|
||||
"code": code,
|
||||
"day": day,
|
||||
"pnl": pnl,
|
||||
"buy": t.get("buy_time") or t.get("entry_time"),
|
||||
"sell": t.get("sell_time") or t.get("exit_time"),
|
||||
"reason": t.get("sell_reason") or t.get("reason") or t.get("exit_reason"),
|
||||
}
|
||||
)
|
||||
|
||||
recorded_daily = dict(trial.get("daily_pnl") or {})
|
||||
days = sorted(set(recorded_daily) | set(daily_reeval))
|
||||
daily_diff = []
|
||||
for day in days:
|
||||
a = float(recorded_daily.get(day) or 0)
|
||||
b = float(daily_reeval.get(day) or 0)
|
||||
daily_diff.append(
|
||||
{
|
||||
"day": day,
|
||||
"optuna_recorded": a,
|
||||
"reeval": b,
|
||||
"delta": b - a,
|
||||
}
|
||||
)
|
||||
|
||||
# 코드별 상위 |pnl|
|
||||
code_rows = sorted(
|
||||
(
|
||||
{"code": c, "n": v["n"], "pnl": round(v["pnl"], 1)}
|
||||
for c, v in by_code.items()
|
||||
),
|
||||
key=lambda x: abs(x["pnl"]),
|
||||
reverse=True,
|
||||
)[:25]
|
||||
|
||||
report = {
|
||||
"db_touched": False,
|
||||
"trial": args.trial,
|
||||
"note": (
|
||||
"Optuna JSON에 체결원본 없음 → 기록 daily_pnl vs 재시뮬 체결 집계 diff. "
|
||||
"체결 키 목록은 재시뮬만."
|
||||
),
|
||||
"recorded": {
|
||||
"total_pnl": trial["total_pnl"],
|
||||
"total_trades": trial["total_trades"],
|
||||
"mdd": trial.get("mdd"),
|
||||
"win_rate": trial.get("win_rate"),
|
||||
"daily_pnl": recorded_daily,
|
||||
},
|
||||
"reeval": {
|
||||
"total_pnl": r.get("total_pnl"),
|
||||
"total_trades": r.get("total_trades"),
|
||||
"mdd": r.get("mdd"),
|
||||
"win_rate": r.get("win_rate"),
|
||||
"pf": r.get("pf"),
|
||||
"daily_pnl": {k: round(v, 1) for k, v in sorted(daily_reeval.items())},
|
||||
"n_trades_list": len(trades),
|
||||
},
|
||||
"delta_total_pnl": float(r.get("total_pnl") or 0) - float(trial["total_pnl"]),
|
||||
"delta_trades": int(r.get("total_trades") or 0) - int(trial["total_trades"]),
|
||||
"daily_diff": daily_diff,
|
||||
"reeval_top_codes": code_rows,
|
||||
"reeval_trades": slim,
|
||||
"elapsed_sec": round(time.time() - t0, 1),
|
||||
}
|
||||
out = Path(args.out)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
out.write_text(json.dumps(report, ensure_ascii=False, indent=2))
|
||||
print("WROTE", out, flush=True)
|
||||
print("DAILY_DIFF", json.dumps(daily_diff, ensure_ascii=False), flush=True)
|
||||
print(
|
||||
"SUMMARY",
|
||||
json.dumps(
|
||||
{
|
||||
"recorded_pnl": trial["total_pnl"],
|
||||
"reeval_pnl": r.get("total_pnl"),
|
||||
"delta_pnl": report["delta_total_pnl"],
|
||||
"recorded_tr": trial["total_trades"],
|
||||
"reeval_tr": r.get("total_trades"),
|
||||
"mdd_rec": trial.get("mdd"),
|
||||
"mdd_reeval": r.get("mdd"),
|
||||
},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
flush=True,
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
raise SystemExit(1)
|
||||
180
scripts/momentum_optuna_trades_export_smoke.py
Normal file
180
scripts/momentum_optuna_trades_export_smoke.py
Normal file
@@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Step1 검증: enrich_out_data_with_mode_combo 가 best/mode _trades 를 JSON에 남기는지.
|
||||
|
||||
기존 Optuna JSON의 #1·mode params 만 빌려, 동일 evaluate 경로로 1회 실측.
|
||||
DB 미변경. 전체 Optuna 재실행 아님.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import traceback
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument(
|
||||
"--json",
|
||||
default="kis_trader/backtest/results/optuna_momentum_tpe_20260821_220227.json",
|
||||
)
|
||||
ap.add_argument("--out", required=True)
|
||||
args = ap.parse_args()
|
||||
|
||||
src = Path(args.json)
|
||||
d = json.loads(src.read_text())
|
||||
results = list(d.get("results") or [])
|
||||
if not results:
|
||||
print("no results", flush=True)
|
||||
return 1
|
||||
|
||||
from kis_trader.backtest.optuna_momentum import prepare_momentum_search_context
|
||||
from kis_trader.backtest.optuna_mode_combo import enrich_out_data_with_mode_combo
|
||||
from kis_trader.backtest.param_search_momentum import evaluate_momentum_param_combo
|
||||
|
||||
print(
|
||||
f"prepare {d['start']}~{d['end']} for trades-export smoke",
|
||||
flush=True,
|
||||
)
|
||||
ctx = prepare_momentum_search_context(
|
||||
d["start"],
|
||||
d["end"],
|
||||
"tpe",
|
||||
slot_money=float(d["slot_money"]),
|
||||
max_stocks=int(d["max_stocks"]),
|
||||
total_budget_krw=float(d["total_budget_krw"]),
|
||||
orderbook_filter="off",
|
||||
market="KR",
|
||||
history_source="kiwoom",
|
||||
)
|
||||
if ctx is None:
|
||||
print("prepare failed", flush=True)
|
||||
return 1
|
||||
|
||||
grid_keys = list(d.get("grid_keys") or ctx.grid_keys)
|
||||
|
||||
def _eval(combo):
|
||||
return evaluate_momentum_param_combo(
|
||||
combo,
|
||||
base_fixed=ctx.base_fixed,
|
||||
grid_keys=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=ctx.log_verdict_by_code,
|
||||
start_key=ctx.start_key,
|
||||
end_key=ctx.end_key,
|
||||
include_trades=True,
|
||||
)
|
||||
|
||||
out_data = {
|
||||
"engine": "optuna_trades_smoke",
|
||||
"strategy": "momentum",
|
||||
"mode": d.get("mode"),
|
||||
"start": d["start"],
|
||||
"end": d["end"],
|
||||
"grid_keys": grid_keys,
|
||||
# enrich 가 results[0] 을 best 로 씀 — 원본 #199 행 유지
|
||||
"results": [deepcopy(results[0])],
|
||||
"source_json": str(src),
|
||||
"db_touched": False,
|
||||
# 후처리(호가 TPE)는 스킵 — evaluate 만 검증. attach_topn 은 enrich 안이므로
|
||||
# run_ob 을 막으려면 env? 그냥 돌리면 오래 걸림.
|
||||
# → enrich 전체가 후처리까지 함. 스모크는 후처리 OFF 필요.
|
||||
}
|
||||
|
||||
# 후처리 끄기: attach_topn_postprocess 의 run_ob_whipsaw — enrich 가 True 고정.
|
||||
# 스모크 시간 절약: enrich 대신 best/mode 직접 호출과 동일 검증.
|
||||
from kis_trader.backtest.optuna_common import slim_trades_for_optuna_json
|
||||
from kis_trader.backtest.optuna_mode_combo import _attach_best_trial_trades
|
||||
|
||||
_attach_best_trial_trades(out_data, _eval, log=__import__("logging").getLogger("smoke"))
|
||||
|
||||
mode_params = dict((d.get("mode_combo") or {}).get("params") or {})
|
||||
bt = _eval(mode_params) if mode_params else None
|
||||
mode_trades = []
|
||||
mode_summary = None
|
||||
if isinstance(bt, dict):
|
||||
mode_trades = slim_trades_for_optuna_json(list(bt.pop("_trades", None) or []))
|
||||
mode_summary = {
|
||||
"ok": True,
|
||||
"total_pnl": bt.get("total_pnl"),
|
||||
"total_trades": bt.get("total_trades"),
|
||||
"win_rate": bt.get("win_rate"),
|
||||
"pf": bt.get("pf"),
|
||||
"_trades": mode_trades,
|
||||
}
|
||||
out_data["mode_combo"] = {
|
||||
"params": mode_params,
|
||||
"backtest": mode_summary,
|
||||
"note": "smoke: mode params from source JSON, re-eval with _trades",
|
||||
}
|
||||
|
||||
best_tr = out_data.get("best_trial_trades") or []
|
||||
reeval = out_data.get("best_trial_reeval") or {}
|
||||
checks = {
|
||||
"best_trades_n": len(best_tr),
|
||||
"best_trades_eq_reeval_tr": len(best_tr) == int(reeval.get("total_trades") or -1),
|
||||
"mode_trades_n": len(mode_trades),
|
||||
"mode_trades_eq_summary_tr": (
|
||||
len(mode_trades) == int((mode_summary or {}).get("total_trades") or -1)
|
||||
if mode_summary
|
||||
else False
|
||||
),
|
||||
"best_has_aug21": any(
|
||||
str(t.get("sell_time") or "").startswith("20260821") for t in best_tr
|
||||
),
|
||||
"slim_keys_ok": (
|
||||
all(
|
||||
set(t.keys())
|
||||
>= {"code", "buy_time", "sell_time", "pnl", "sell_reason"}
|
||||
for t in best_tr[:3]
|
||||
)
|
||||
if best_tr
|
||||
else False
|
||||
),
|
||||
}
|
||||
out_data["smoke_checks"] = checks
|
||||
out_data["smoke_ok"] = all(
|
||||
[
|
||||
checks["best_trades_n"] > 0,
|
||||
checks["best_trades_eq_reeval_tr"],
|
||||
checks["mode_trades_n"] > 0,
|
||||
checks["mode_trades_eq_summary_tr"],
|
||||
checks["slim_keys_ok"],
|
||||
]
|
||||
)
|
||||
|
||||
out = Path(args.out)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
out.write_text(json.dumps(out_data, ensure_ascii=False, indent=2))
|
||||
print("WROTE", out, flush=True)
|
||||
print("CHECKS", json.dumps(checks, ensure_ascii=False), flush=True)
|
||||
print("SMOKE_OK", out_data["smoke_ok"], flush=True)
|
||||
print(
|
||||
"BEST_REEVAL",
|
||||
json.dumps(reeval, ensure_ascii=False),
|
||||
flush=True,
|
||||
)
|
||||
return 0 if out_data["smoke_ok"] else 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
raise SystemExit(1)
|
||||
499
scripts/multi_optuna_parity_reeval.py
Normal file
499
scripts/multi_optuna_parity_reeval.py
Normal file
@@ -0,0 +1,499 @@
|
||||
#!/usr/bin/env python3
|
||||
"""전 전략 Optuna JSON 정합 재실측 — DB 미변경.
|
||||
|
||||
Usage:
|
||||
python3 -u scripts/multi_optuna_parity_reeval.py \\
|
||||
--strategies scalp,breakout_atr,breakout_fixed,momentum,tail \\
|
||||
--out logs/multi_optuna_parity_OUT.json
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import time
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
|
||||
DEFAULT_JSON = {
|
||||
"scalp": "kis_trader/backtest/results/optuna_scalp_tpe_20260822_001653.json",
|
||||
"breakout_atr": "kis_trader/backtest/results/optuna_breakout_tpe_20260821_234943.json",
|
||||
"breakout_fixed": "kis_trader/backtest/results/optuna_breakout_tpe_20260821_233130.json",
|
||||
"momentum": "kis_trader/backtest/results/optuna_momentum_tpe_20260821_220227.json",
|
||||
"tail": "kis_trader/backtest/results/optuna_tail_tpe_20260821_225356.json",
|
||||
}
|
||||
|
||||
# 웹 최근 BT (참고 대조) — 재실측 대상에 web_job_params 포함 시
|
||||
WEB_BT = {
|
||||
"scalp": "kis_trader/backtest/results/scalp_bt_20260818_20260821_20260822_024250.json",
|
||||
"breakout": "kis_trader/backtest/results/breakout_bt_20260818_20260821_20260822_024507.json",
|
||||
}
|
||||
|
||||
|
||||
def _find_trial(d: dict, n: int) -> Optional[dict]:
|
||||
for key in ("results_all", "results", "results_gated", "results_stable"):
|
||||
for x in d.get(key) or []:
|
||||
if isinstance(x, dict) and x.get("optuna_trial_number") == n:
|
||||
return x
|
||||
return None
|
||||
|
||||
|
||||
def _rec(t: dict) -> dict:
|
||||
return {
|
||||
"total_pnl": float(t.get("total_pnl") or 0),
|
||||
"total_trades": int(t.get("total_trades") or 0),
|
||||
"win_rate": t.get("win_rate"),
|
||||
"pf": t.get("pf"),
|
||||
}
|
||||
|
||||
|
||||
def _cmp(label: str, recorded: dict, reeval: Optional[dict]) -> dict:
|
||||
if reeval is None:
|
||||
print(f" {label}: FAIL no result", flush=True)
|
||||
return {
|
||||
"label": label,
|
||||
"ok": False,
|
||||
"recorded": recorded,
|
||||
"reeval": None,
|
||||
"same": False,
|
||||
}
|
||||
dp = float(reeval["total_pnl"]) - float(recorded.get("total_pnl") or 0)
|
||||
dt = int(reeval["total_trades"]) - int(recorded.get("total_trades") or 0)
|
||||
same = abs(dp) < 0.5 and dt == 0
|
||||
print(
|
||||
f" {label}: recorded PnL={recorded.get('total_pnl')} tr={recorded.get('total_trades')} | "
|
||||
f"reeval PnL={reeval['total_pnl']:.0f} tr={reeval['total_trades']} | "
|
||||
f"Δpnl={dp:.0f} Δtr={dt} same={same}",
|
||||
flush=True,
|
||||
)
|
||||
return {
|
||||
"label": label,
|
||||
"ok": True,
|
||||
"recorded": recorded,
|
||||
"reeval": reeval,
|
||||
"delta_pnl": dp,
|
||||
"delta_trades": dt,
|
||||
"same": same,
|
||||
}
|
||||
|
||||
|
||||
def _pack_eval(r: Optional[dict]) -> Optional[dict]:
|
||||
if not r:
|
||||
return None
|
||||
return {
|
||||
"total_pnl": float(r["total_pnl"]),
|
||||
"total_trades": int(r["total_trades"]),
|
||||
"win_rate": float(r.get("win_rate") or 0),
|
||||
"pf": float(r.get("pf") or 0),
|
||||
}
|
||||
|
||||
|
||||
def run_scalp(d: dict, trials: List[int], include_web: bool) -> Dict[str, Any]:
|
||||
from kis_trader.backtest.optuna_scalping import prepare_scalp_search_context
|
||||
from kis_trader.backtest.param_search_scalping import evaluate_scalp_param_combo
|
||||
|
||||
start, end = d["start"], d["end"]
|
||||
slot, ms, bud = float(d["slot_money"]), int(d["max_stocks"]), float(d["total_budget_krw"])
|
||||
grid_keys = list(d.get("grid_keys") or [])
|
||||
print(f"[scalp] prepare {start}~{end} OB=off", flush=True)
|
||||
ctx = prepare_scalp_search_context(
|
||||
start, end, "tpe",
|
||||
slot_money=slot, max_stocks=ms, total_budget_krw=bud,
|
||||
orderbook_filter="off", history_source="kiwoom",
|
||||
)
|
||||
if ctx is None:
|
||||
return {"strategy": "scalp", "error": "prepare_failed"}
|
||||
base = dict(ctx.base_fixed)
|
||||
base["_orderbook_filter_enabled"] = False
|
||||
comparisons = []
|
||||
|
||||
def eval_combo(combo: dict) -> Optional[dict]:
|
||||
return evaluate_scalp_param_combo(
|
||||
combo,
|
||||
base_fixed=base,
|
||||
grid_keys=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,
|
||||
start_key=ctx.start_key, end_key=ctx.end_key,
|
||||
)
|
||||
|
||||
for n in trials:
|
||||
t = _find_trial(d, n)
|
||||
if not t:
|
||||
comparisons.append({"label": f"#{n}", "ok": False, "error": "missing"})
|
||||
continue
|
||||
print(f"--- scalp #{n} ---", flush=True)
|
||||
comparisons.append(_cmp(f"#{n}", _rec(t), _pack_eval(eval_combo(dict(t.get("params") or {})))))
|
||||
|
||||
mc = d.get("mode_combo") or {}
|
||||
if mc.get("params"):
|
||||
bt = mc.get("backtest") or {}
|
||||
print("--- scalp mode_combo ---", flush=True)
|
||||
comparisons.append(
|
||||
_cmp(
|
||||
"mode_combo",
|
||||
{
|
||||
"total_pnl": bt.get("total_pnl"),
|
||||
"total_trades": bt.get("total_trades"),
|
||||
"win_rate": bt.get("win_rate"),
|
||||
"pf": bt.get("pf"),
|
||||
},
|
||||
_pack_eval(eval_combo(dict(mc["params"]))),
|
||||
)
|
||||
)
|
||||
|
||||
if include_web:
|
||||
wp = Path(WEB_BT["scalp"])
|
||||
if wp.is_file():
|
||||
wj = json.loads(wp.read_text(encoding="utf-8"))
|
||||
wparams = dict(wj.get("params") or {})
|
||||
# Optuna UI 키만 덮어씀 (단위=웹 저장값 그대로)
|
||||
combo = {k: wparams[k] for k in grid_keys if k in wparams}
|
||||
# web 에만 있는 흔한 키
|
||||
for k in ("sl_pct", "tp_pct", "tp_max_pct", "drop_rate", "rsi_oversold", "rsi_overbought",
|
||||
"rsi_period", "shoulder_min_high", "shoulder_cut_pct", "vol_mult", "cooldown_min",
|
||||
"max_daily", "high_chase_thr", "max_daily_chg", "min_price", "max_loss_krw",
|
||||
"min_margin", "use_defense_filters", "require_reversal_candle"):
|
||||
if k in wparams:
|
||||
combo[k] = wparams[k]
|
||||
sm = wj.get("summary") or {}
|
||||
print("--- scalp web_job_params ---", flush=True)
|
||||
print(f" web combo keys={sorted(combo.keys())}", flush=True)
|
||||
print(
|
||||
f" web drop/rsi/sl={combo.get('drop_rate')}/{combo.get('rsi_oversold')}/"
|
||||
f"{combo.get('rsi_overbought')}/{combo.get('sl_pct')}",
|
||||
flush=True,
|
||||
)
|
||||
comparisons.append(
|
||||
_cmp(
|
||||
"web_job_params",
|
||||
{
|
||||
"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"),
|
||||
},
|
||||
_pack_eval(eval_combo(combo)),
|
||||
)
|
||||
)
|
||||
|
||||
return {
|
||||
"strategy": "scalp",
|
||||
"source_json": DEFAULT_JSON["scalp"],
|
||||
"comparisons": comparisons,
|
||||
"all_same": all(c.get("same") for c in comparisons if c.get("ok")),
|
||||
}
|
||||
|
||||
|
||||
def run_breakout(d: dict, *, label: str, sl_mode: str, trials: List[int], include_web: bool) -> Dict[str, Any]:
|
||||
from kis_trader.backtest.optuna_breakout import prepare_breakout_search_context
|
||||
from kis_trader.backtest.param_search_breakout import evaluate_breakout_param_combo
|
||||
|
||||
start, end = d["start"], d["end"]
|
||||
slot, ms, bud = float(d["slot_money"]), int(d["max_stocks"]), float(d["total_budget_krw"])
|
||||
grid_keys = list(d.get("grid_keys") or [])
|
||||
print(f"[{label}] prepare {start}~{end} sl_mode={sl_mode} OB=off", flush=True)
|
||||
ctx = prepare_breakout_search_context(
|
||||
start, end, "tpe",
|
||||
slot_money=slot, max_stocks=ms, total_budget_krw=bud,
|
||||
orderbook_filter="off", history_source="kiwoom", sl_mode=sl_mode,
|
||||
)
|
||||
if ctx is None:
|
||||
return {"strategy": label, "error": "prepare_failed"}
|
||||
base = dict(ctx.base_fixed)
|
||||
base["_orderbook_filter_enabled"] = False
|
||||
comparisons = []
|
||||
|
||||
def eval_combo(combo: dict) -> Optional[dict]:
|
||||
return evaluate_breakout_param_combo(
|
||||
combo,
|
||||
base_fixed=base,
|
||||
grid_keys=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),
|
||||
share_denom_by_code=getattr(ctx, "share_denom_by_code", None),
|
||||
)
|
||||
|
||||
for n in trials:
|
||||
t = _find_trial(d, n)
|
||||
if not t:
|
||||
comparisons.append({"label": f"#{n}", "ok": False, "error": "missing"})
|
||||
continue
|
||||
print(f"--- {label} #{n} ---", flush=True)
|
||||
comparisons.append(_cmp(f"#{n}", _rec(t), _pack_eval(eval_combo(dict(t.get("params") or {})))))
|
||||
|
||||
mc = d.get("mode_combo") or {}
|
||||
if mc.get("params"):
|
||||
bt = mc.get("backtest") or {}
|
||||
print(f"--- {label} mode_combo ---", flush=True)
|
||||
comparisons.append(
|
||||
_cmp(
|
||||
"mode_combo",
|
||||
{
|
||||
"total_pnl": bt.get("total_pnl"),
|
||||
"total_trades": bt.get("total_trades"),
|
||||
"win_rate": bt.get("win_rate"),
|
||||
"pf": bt.get("pf"),
|
||||
},
|
||||
_pack_eval(eval_combo(dict(mc["params"]))),
|
||||
)
|
||||
)
|
||||
|
||||
if include_web and sl_mode == "atr":
|
||||
wp = Path(WEB_BT["breakout"])
|
||||
if wp.is_file():
|
||||
wj = json.loads(wp.read_text(encoding="utf-8"))
|
||||
# 웹이 atr#57 과 동일하면 그 trial 재실측으로 충분 — web params 키 요약만
|
||||
sm = wj.get("summary") or {}
|
||||
t57 = _find_trial(d, 57)
|
||||
if t57 and abs(float(sm.get("total_pnl") or 0) - float(t57.get("total_pnl") or 0)) < 1:
|
||||
print("--- breakout web == atr#57 (skip separate web combo) ---", flush=True)
|
||||
comparisons.append(
|
||||
{
|
||||
"label": "web_matches_atr#57",
|
||||
"ok": True,
|
||||
"same": True,
|
||||
"recorded": _rec(t57),
|
||||
"note": "web BT PnL/trades identical to Optuna atr #57",
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"strategy": label,
|
||||
"sl_mode": sl_mode,
|
||||
"source_json": DEFAULT_JSON.get("breakout_atr" if sl_mode == "atr" else "breakout_fixed"),
|
||||
"comparisons": comparisons,
|
||||
"all_same": all(c.get("same") for c in comparisons if c.get("ok")),
|
||||
}
|
||||
|
||||
|
||||
def run_momentum(d: dict, trials: List[int]) -> Dict[str, Any]:
|
||||
from kis_trader.backtest.optuna_momentum import prepare_momentum_search_context
|
||||
from kis_trader.backtest.param_search_momentum import evaluate_momentum_param_combo
|
||||
|
||||
start, end = d["start"], d["end"]
|
||||
slot, ms, bud = float(d["slot_money"]), int(d["max_stocks"]), float(d["total_budget_krw"])
|
||||
grid_keys = list(d.get("grid_keys") or [])
|
||||
print(f"[momentum] prepare {start}~{end} OB=off", flush=True)
|
||||
ctx = prepare_momentum_search_context(
|
||||
start, end, "tpe",
|
||||
slot_money=slot, max_stocks=ms, total_budget_krw=bud,
|
||||
orderbook_filter="off", market="KR",
|
||||
)
|
||||
if ctx is None:
|
||||
return {"strategy": "momentum", "error": "prepare_failed"}
|
||||
base = dict(ctx.base_fixed)
|
||||
base["_orderbook_filter_enabled"] = False
|
||||
comparisons = []
|
||||
|
||||
def eval_combo(combo: dict) -> Optional[dict]:
|
||||
return evaluate_momentum_param_combo(
|
||||
combo,
|
||||
base_fixed=base,
|
||||
grid_keys=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=getattr(ctx, "program_by_code", None),
|
||||
start_key=ctx.start_key,
|
||||
end_key=ctx.end_key,
|
||||
)
|
||||
|
||||
for n in trials:
|
||||
t = _find_trial(d, n)
|
||||
if not t:
|
||||
comparisons.append({"label": f"#{n}", "ok": False, "error": "missing"})
|
||||
continue
|
||||
print(f"--- momentum #{n} ---", flush=True)
|
||||
comparisons.append(_cmp(f"#{n}", _rec(t), _pack_eval(eval_combo(dict(t.get("params") or {})))))
|
||||
|
||||
mc = d.get("mode_combo") or {}
|
||||
if mc.get("params"):
|
||||
bt = mc.get("backtest") or {}
|
||||
print("--- momentum mode_combo ---", flush=True)
|
||||
comparisons.append(
|
||||
_cmp(
|
||||
"mode_combo",
|
||||
{
|
||||
"total_pnl": bt.get("total_pnl"),
|
||||
"total_trades": bt.get("total_trades"),
|
||||
"win_rate": bt.get("win_rate"),
|
||||
"pf": bt.get("pf"),
|
||||
},
|
||||
_pack_eval(eval_combo(dict(mc["params"]))),
|
||||
)
|
||||
)
|
||||
|
||||
return {
|
||||
"strategy": "momentum",
|
||||
"source_json": DEFAULT_JSON["momentum"],
|
||||
"comparisons": comparisons,
|
||||
"all_same": all(c.get("same") for c in comparisons if c.get("ok")),
|
||||
"note": "과거 #199 JSON vs 재실측 갭은 08-21 일자 — 본 스크립트는 현재 엔진 재현 여부",
|
||||
}
|
||||
|
||||
|
||||
def run_tail(d: dict, trials: List[int]) -> Dict[str, Any]:
|
||||
from kis_trader.backtest.param_search_optuna import prepare_tail_search_context
|
||||
from kis_trader.backtest.tail_param_search import evaluate_tail_param_combo
|
||||
|
||||
start, end = d["start"], d["end"]
|
||||
slot, ms, bud = float(d["slot_money"]), int(d["max_stocks"]), float(d["total_budget_krw"])
|
||||
tf = int(d.get("timeframe") or 3)
|
||||
print(f"[tail] prepare {start}~{end} OB=off", flush=True)
|
||||
ctx = prepare_tail_search_context(
|
||||
start, end, "tpe", timeframe=tf,
|
||||
slot_money=slot, max_stocks=ms, total_budget_krw=bud,
|
||||
orderbook_filter="off", history_source="kiwoom", entry_mode="align",
|
||||
)
|
||||
if ctx is None:
|
||||
return {"strategy": "tail", "error": "prepare_failed"}
|
||||
base = dict(ctx.base_params)
|
||||
base["_orderbook_filter_enabled"] = False
|
||||
comparisons = []
|
||||
|
||||
def eval_combo(combo: dict) -> Optional[dict]:
|
||||
return evaluate_tail_param_combo(
|
||||
combo,
|
||||
base_params=base,
|
||||
candles_by_code=ctx.candles_by_code,
|
||||
fee_rate=ctx.fee_rate, sell_tax=ctx.sell_tax,
|
||||
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,
|
||||
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=ctx.log_verdict_by_code,
|
||||
)
|
||||
|
||||
for n in trials:
|
||||
t = _find_trial(d, n)
|
||||
if not t:
|
||||
comparisons.append({"label": f"#{n}", "ok": False, "error": "missing"})
|
||||
continue
|
||||
print(f"--- tail #{n} ---", flush=True)
|
||||
comparisons.append(_cmp(f"#{n}", _rec(t), _pack_eval(eval_combo(dict(t.get("params") or {})))))
|
||||
|
||||
mc = d.get("mode_combo") or {}
|
||||
if mc.get("params"):
|
||||
bt = mc.get("backtest") or {}
|
||||
print("--- tail mode_combo ---", flush=True)
|
||||
comparisons.append(
|
||||
_cmp(
|
||||
"mode_combo",
|
||||
{
|
||||
"total_pnl": bt.get("total_pnl"),
|
||||
"total_trades": bt.get("total_trades"),
|
||||
"win_rate": bt.get("win_rate"),
|
||||
"pf": bt.get("pf"),
|
||||
},
|
||||
_pack_eval(eval_combo(dict(mc["params"]))),
|
||||
)
|
||||
)
|
||||
|
||||
return {
|
||||
"strategy": "tail",
|
||||
"source_json": DEFAULT_JSON["tail"],
|
||||
"comparisons": comparisons,
|
||||
"all_same": all(c.get("same") for c in comparisons if c.get("ok")),
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument(
|
||||
"--strategies",
|
||||
default="scalp,breakout_atr,breakout_fixed,momentum,tail",
|
||||
)
|
||||
ap.add_argument("--out", required=True)
|
||||
ap.add_argument("--skip-web", action="store_true")
|
||||
args = ap.parse_args()
|
||||
t0 = time.time()
|
||||
include_web = not args.skip_web
|
||||
wanted = [s.strip() for s in args.strategies.split(",") if s.strip()]
|
||||
results: List[Dict[str, Any]] = []
|
||||
|
||||
try:
|
||||
for name in wanted:
|
||||
path = DEFAULT_JSON.get(name)
|
||||
if not path or not Path(path).is_file():
|
||||
results.append({"strategy": name, "error": f"missing_json:{path}"})
|
||||
continue
|
||||
d = json.loads(Path(path).read_text(encoding="utf-8"))
|
||||
best_n = int(d.get("optuna_best_trial_number") or 0)
|
||||
if name == "scalp":
|
||||
results.append(run_scalp(d, [best_n], include_web=include_web))
|
||||
elif name == "breakout_atr":
|
||||
# best + web-matching #57
|
||||
trials = [best_n]
|
||||
if _find_trial(d, 57):
|
||||
trials.append(57)
|
||||
results.append(
|
||||
run_breakout(d, label="breakout_atr", sl_mode="atr", trials=trials, include_web=include_web)
|
||||
)
|
||||
elif name == "breakout_fixed":
|
||||
results.append(
|
||||
run_breakout(d, label="breakout_fixed", sl_mode="fixed", trials=[best_n], include_web=False)
|
||||
)
|
||||
elif name == "momentum":
|
||||
results.append(run_momentum(d, [best_n]))
|
||||
elif name == "tail":
|
||||
# 이미 검증됨 — best+179
|
||||
trials = [best_n]
|
||||
if _find_trial(d, 179):
|
||||
trials.append(179)
|
||||
results.append(run_tail(d, trials))
|
||||
else:
|
||||
results.append({"strategy": name, "error": "unknown"})
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
return 1
|
||||
|
||||
payload = {
|
||||
"elapsed_sec": round(time.time() - t0, 1),
|
||||
"orderbook_filter": "off",
|
||||
"db_modified": False,
|
||||
"strategies": results,
|
||||
"all_same": all(r.get("all_same") for r in results if "comparisons" in r),
|
||||
}
|
||||
out = Path(args.out)
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
out.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||
print(f"wrote {out} all_same={payload['all_same']} elapsed={payload['elapsed_sec']}s", flush=True)
|
||||
for r in results:
|
||||
print(
|
||||
f" SUMMARY {r.get('strategy')}: all_same={r.get('all_same')} err={r.get('error')}",
|
||||
flush=True,
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -108,7 +108,7 @@ def main() -> int:
|
||||
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("--orderbook-filter", choices=["off", "on", "auto"], default="auto")
|
||||
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 반영 권장")
|
||||
|
||||
228
scripts/tail_optuna_parity_reeval.py
Normal file
228
scripts/tail_optuna_parity_reeval.py
Normal file
@@ -0,0 +1,228 @@
|
||||
#!/usr/bin/env python3
|
||||
"""꼬리 Optuna JSON(#124·#179·mode) vs 동일 엔진 재평가 — DB 미변경.
|
||||
|
||||
Usage:
|
||||
python3 -u scripts/tail_optuna_parity_reeval.py \\
|
||||
--json kis_trader/backtest/results/optuna_tail_tpe_20260821_225356.json \\
|
||||
--out logs/tail_optuna_parity_OUT.json
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import time
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _find_trial(d: dict, n: int):
|
||||
for key in ("results_all", "results", "results_gated", "results_stable"):
|
||||
for x in d.get(key) or []:
|
||||
if isinstance(x, dict) and x.get("optuna_trial_number") == n:
|
||||
return x
|
||||
return None
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument(
|
||||
"--json",
|
||||
default="kis_trader/backtest/results/optuna_tail_tpe_20260821_225356.json",
|
||||
)
|
||||
ap.add_argument("--out", required=True)
|
||||
ap.add_argument("--trials", default="124,179", help="comma trial numbers")
|
||||
args = ap.parse_args()
|
||||
|
||||
t0 = time.time()
|
||||
json_path = Path(args.json)
|
||||
out_path = Path(args.out)
|
||||
d = json.loads(json_path.read_text(encoding="utf-8"))
|
||||
start, end = d["start"], d["end"]
|
||||
slot = float(d["slot_money"])
|
||||
max_stocks = int(d["max_stocks"])
|
||||
budget = float(d["total_budget_krw"])
|
||||
tf = int(d.get("timeframe") or 3)
|
||||
entry_mode = "align"
|
||||
for t in (d.get("results_all") or d.get("results") or []):
|
||||
em = (t.get("params") or {}).get("entry_mode")
|
||||
if em:
|
||||
entry_mode = str(em)
|
||||
break
|
||||
|
||||
trial_ns = [int(x.strip()) for x in str(args.trials).split(",") if x.strip()]
|
||||
|
||||
from kis_trader.backtest.param_search_optuna import prepare_tail_search_context
|
||||
from kis_trader.backtest.tail_param_search import evaluate_tail_param_combo
|
||||
|
||||
print(
|
||||
f"prepare {start}~{end} slot={slot} max={max_stocks} budget={budget} "
|
||||
f"tf={tf} entry={entry_mode} OB=off hist=kiwoom",
|
||||
flush=True,
|
||||
)
|
||||
ctx = prepare_tail_search_context(
|
||||
start,
|
||||
end,
|
||||
"tpe",
|
||||
timeframe=tf,
|
||||
slot_money=slot,
|
||||
max_stocks=max_stocks,
|
||||
total_budget_krw=budget,
|
||||
orderbook_filter="off",
|
||||
history_source="kiwoom",
|
||||
entry_mode=entry_mode,
|
||||
)
|
||||
if ctx is None:
|
||||
print("prepare failed", flush=True)
|
||||
return 1
|
||||
|
||||
base = dict(ctx.base_params)
|
||||
base["_orderbook_filter_enabled"] = False
|
||||
for k in list(base.keys()):
|
||||
if "whipsaw" in k.lower() and isinstance(base[k], (bool, int)):
|
||||
base[k] = False if isinstance(base[k], bool) else 0
|
||||
|
||||
print(
|
||||
f"ctx ready candles={len(ctx.candles_by_code)} "
|
||||
f"univ_slots={len(ctx.universe_by_slot or {})} "
|
||||
f"ticks={'yes' if ctx.ticks_by_code else 'no'} "
|
||||
f"fee={ctx.fee_rate} tax={ctx.sell_tax}",
|
||||
flush=True,
|
||||
)
|
||||
print(
|
||||
f"base tick_entry={base.get('backtest_use_tick_entry') or base.get('backtest_use_tick_db')} "
|
||||
f"tick_exit={base.get('backtest_use_tick_exit')} "
|
||||
f"ob={base.get('_orderbook_filter_enabled')} "
|
||||
f"pattern_hammer={base.get('pattern_hammer')}",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
def run_one(label, combo, recorded):
|
||||
print(f"--- eval {label} ---", flush=True)
|
||||
r = evaluate_tail_param_combo(
|
||||
combo,
|
||||
base_params=base,
|
||||
candles_by_code=ctx.candles_by_code,
|
||||
fee_rate=ctx.fee_rate,
|
||||
sell_tax=ctx.sell_tax,
|
||||
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,
|
||||
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=ctx.log_verdict_by_code,
|
||||
include_trades=True,
|
||||
)
|
||||
if r is None:
|
||||
out = {
|
||||
"label": label,
|
||||
"ok": False,
|
||||
"recorded": recorded,
|
||||
"reeval": None,
|
||||
"delta_pnl": None,
|
||||
"delta_trades": None,
|
||||
"same": False,
|
||||
}
|
||||
print(f" FAIL gates / no result", flush=True)
|
||||
return out
|
||||
re = {
|
||||
"total_pnl": float(r["total_pnl"]),
|
||||
"total_trades": int(r["total_trades"]),
|
||||
"win_rate": float(r["win_rate"]),
|
||||
"pf": float(r.get("pf") or 0),
|
||||
}
|
||||
rec_pnl = float(recorded.get("total_pnl") or 0)
|
||||
rec_tr = int(recorded.get("total_trades") or 0)
|
||||
delta_pnl = re["total_pnl"] - rec_pnl
|
||||
delta_tr = re["total_trades"] - rec_tr
|
||||
same = abs(delta_pnl) < 0.5 and delta_tr == 0
|
||||
print(
|
||||
f" recorded PnL={rec_pnl:.0f} tr={rec_tr} | "
|
||||
f"reeval PnL={re['total_pnl']:.0f} tr={re['total_trades']} WR={re['win_rate']:.1f}% "
|
||||
f"| Δpnl={delta_pnl:.0f} Δtr={delta_tr} same={same}",
|
||||
flush=True,
|
||||
)
|
||||
return {
|
||||
"label": label,
|
||||
"ok": True,
|
||||
"recorded": {
|
||||
"total_pnl": rec_pnl,
|
||||
"total_trades": rec_tr,
|
||||
"win_rate": recorded.get("win_rate"),
|
||||
"pf": recorded.get("pf"),
|
||||
},
|
||||
"reeval": re,
|
||||
"delta_pnl": delta_pnl,
|
||||
"delta_trades": delta_tr,
|
||||
"same": same,
|
||||
"n_trades_exported": len(r.get("_trades") or []),
|
||||
}
|
||||
|
||||
comparisons = []
|
||||
try:
|
||||
for n in trial_ns:
|
||||
t = _find_trial(d, n)
|
||||
if not t:
|
||||
print(f"trial #{n} missing", flush=True)
|
||||
comparisons.append({"label": f"#{n}", "ok": False, "error": "missing"})
|
||||
continue
|
||||
comparisons.append(
|
||||
run_one(
|
||||
f"#{n}",
|
||||
dict(t.get("params") or {}),
|
||||
{
|
||||
"total_pnl": t.get("total_pnl"),
|
||||
"total_trades": t.get("total_trades"),
|
||||
"win_rate": t.get("win_rate"),
|
||||
"pf": t.get("pf"),
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
mc = d.get("mode_combo") or {}
|
||||
if mc.get("params"):
|
||||
bt = mc.get("backtest") or {}
|
||||
comparisons.append(
|
||||
run_one(
|
||||
"mode_combo",
|
||||
dict(mc["params"]),
|
||||
{
|
||||
"total_pnl": bt.get("total_pnl"),
|
||||
"total_trades": bt.get("total_trades"),
|
||||
"win_rate": bt.get("win_rate"),
|
||||
"pf": bt.get("pf"),
|
||||
},
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
return 1
|
||||
|
||||
payload = {
|
||||
"source_json": str(json_path),
|
||||
"start": start,
|
||||
"end": end,
|
||||
"slot_money": slot,
|
||||
"max_stocks": max_stocks,
|
||||
"total_budget_krw": budget,
|
||||
"orderbook_filter": "off",
|
||||
"history_source": "kiwoom",
|
||||
"entry_mode": entry_mode,
|
||||
"elapsed_sec": round(time.time() - t0, 1),
|
||||
"comparisons": comparisons,
|
||||
"all_same": all(c.get("same") for c in comparisons if c.get("ok")),
|
||||
}
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
out_path.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||
print(f"wrote {out_path} all_same={payload['all_same']} elapsed={payload['elapsed_sec']}s", flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user