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