Files
kis_bot/kis_trader/backtest/optuna_rerun_postprocess.py
Your Name 0780b2cdd0 feat: Enhance Optuna integration and logging for backtesting framework
Changes:
- Added new API endpoints for continuing and confirming Optuna jobs, allowing for better management of ongoing studies.
- Introduced detailed logging for tick feed tracking and order book processing, improving traceability of vendor performance during backtests.
- Updated database schema to include new fields for managing Optuna study results, enhancing the ability to track study progress and outcomes.
- Refactored existing functions to utilize the new logging and tracking features, ensuring consistency across the backtesting framework.

Impact:
- These enhancements improve the robustness and transparency of the Optuna backtesting process, facilitating better analysis and optimization of trading strategies.
2026-08-21 19:05:23 +09:00

277 lines
10 KiB
Python

#!/usr/bin/env python3
"""구 Optuna JSON에 TopN 후처리(진입/익절/손절/휩쏘)를 다시 붙여 저장.
실매 엔진 미변경. 캔들/틱은 prepare_* 가 DB 재사용. REST 웜업은 기존 prepare 경로만.
"""
from __future__ import annotations
import argparse
import json
import logging
import sys
from pathlib import Path
from typing import Any, Callable, Dict, Optional, Tuple
_ROOT = Path(__file__).resolve().parents[2]
if str(_ROOT) not in sys.path:
sys.path.insert(0, str(_ROOT))
logger = logging.getLogger("optuna_rerun_postprocess")
EvalFn = Callable[[Dict[str, Any]], Optional[Dict[str, Any]]]
def _hist(data: Dict[str, Any]) -> Optional[str]:
return (
data.get("universe_history_source")
or data.get("_universe_history_source")
or data.get("history_source")
)
def build_replay_evaluate_fn(
data: Dict[str, Any],
) -> Tuple[Optional[EvalFn], Any]:
"""JSON 메타로 실매 Optuna와 같은 evaluate_fn + ctx. 실패 시 (None, None)."""
strat = str(data.get("strategy") or "").strip().lower()
start = str(data.get("start") or "").strip()
end = str(data.get("end") or "").strip()
mode = str(data.get("mode") or "tpe").strip().lower() or "tpe"
hist = _hist(data)
if not start or not end:
logger.warning("⚠️ start/end 없음 — 호가 재탐색만(체결 재실행 없음)")
return None, None
if strat in ("momentum", "us_momentum"):
from kis_trader.backtest.optuna_momentum import prepare_momentum_search_context
from kis_trader.backtest.param_search_momentum import evaluate_momentum_param_combo
mk = "US" if strat == "us_momentum" else "KR"
ctx = prepare_momentum_search_context(
start, end, mode,
history_source=hist,
market=mk,
symbol=str(data.get("symbol") or "") or None,
orderbook_filter="off",
)
if ctx is None:
return None, None
def _eval(combo: Dict[str, Any]) -> Optional[Dict[str, Any]]:
return evaluate_momentum_param_combo(
combo,
base_fixed=ctx.base_fixed,
grid_keys=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=ctx.log_verdict_by_code,
start_key=ctx.start_key,
end_key=ctx.end_key,
include_trades=True,
)
return _eval, ctx
if strat == "breakout":
from kis_trader.backtest.optuna_breakout import prepare_breakout_search_context
from kis_trader.backtest.param_search_breakout import evaluate_breakout_param_combo
ctx = prepare_breakout_search_context(
start, end, mode, history_source=hist, orderbook_filter="off",
)
if ctx is None:
return None, None
def _eval_b(combo: Dict[str, Any]) -> Optional[Dict[str, Any]]:
return evaluate_breakout_param_combo(
combo,
base_fixed=ctx.base_fixed,
grid_keys=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=ctx.log_verdict_by_code,
share_denom_by_code=ctx.share_denom_by_code,
include_trades=True,
)
return _eval_b, ctx
if strat in ("scalp", "scalping"):
from kis_trader.backtest.optuna_scalping import prepare_scalp_search_context
from kis_trader.backtest.param_search_scalping import evaluate_scalp_param_combo
ctx = prepare_scalp_search_context(
start, end, mode, history_source=hist, orderbook_filter="off",
)
if ctx is None:
return None, None
def _eval_s(combo: Dict[str, Any]) -> Optional[Dict[str, Any]]:
return evaluate_scalp_param_combo(
combo,
base_fixed=ctx.base_fixed,
grid_keys=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,
start_key=ctx.start_key,
end_key=ctx.end_key,
include_trades=True,
)
return _eval_s, ctx
if strat in ("tail", "short"):
from kis_trader.backtest.param_search_optuna import prepare_tail_search_context
from kis_trader.backtest.tail_param_search import evaluate_tail_param_combo
ctx = prepare_tail_search_context(
start, end, mode, history_source=hist, orderbook_filter="off",
)
if ctx is None:
return None, None
def _eval_t(combo: Dict[str, Any]) -> Optional[Dict[str, Any]]:
return evaluate_tail_param_combo(
combo,
base_params=ctx.base_params,
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,
)
return _eval_t, ctx
logger.warning("⚠️ 전략 %s 후처리 재실행 evaluate 미지원", strat)
return None, None
def rerun_postprocess_on_json(path: str, *, ob_n_trials: int = 0) -> Dict[str, Any]:
p = Path(path)
if not p.is_file():
raise FileNotFoundError(str(p))
data = json.loads(p.read_text(encoding="utf-8"))
eval_fn, ctx = build_replay_evaluate_fn(data)
try:
from kis_trader.backtest.optuna_postprocess_topn import attach_topn_postprocess
attach_topn_postprocess(
data,
evaluate_fn=eval_fn,
log=logger,
run_ob_whipsaw=True,
ob_n_trials=int(ob_n_trials or 0),
)
tmp = p.with_suffix(".tmp.json")
tmp.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
tmp.replace(p)
topn = data.get("postprocess_topn") or {}
n = len(topn.get("postprocess_by_anchor") or [])
logger.info("📌 후처리 재저장 %s anchors=%d overfit=%s", p, n, topn.get("apply_overfit_pct"))
try:
from kis_trader.backtest.optuna_study_store import (
count_study_states,
save_payload,
)
import optuna
from kis_trader.backtest.optuna_common import resolve_optuna_storage_url
sname = str(data.get("optuna_study_name") or "")
n_c = int(data.get("optuna_n_complete") or 0)
if sname:
try:
st = optuna.load_study(
study_name=sname,
storage=resolve_optuna_storage_url(None),
)
n_c, _nr, n_f = count_study_states(st)
except Exception:
n_f = n_c
save_payload(
study_name=sname,
out_data=data,
n_complete=n_f if n_f else n_c,
pp_status="done",
)
except Exception as exc:
logger.warning("⚠️ optuna_study_result 반영 실패: %s", exc)
return {"ok": True, "path": str(p), "anchors": n, "run_ob_whipsaw": True}
finally:
if ctx is not None:
try:
from kis_trader.backtest.optuna_common import release_shared_tick_store
release_shared_tick_store(ctx, log=logger)
except Exception as exc:
logger.warning("⚠️ tick store 해제: %s", exc)
def main() -> int:
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
stream=sys.stdout,
)
ap = argparse.ArgumentParser(description="Optuna JSON TopN 후처리 재실행")
ap.add_argument("--result-json", required=True)
ap.add_argument("--ob-axis-trials", type=int, default=0, help="0=DB OPTUNA_OB_* trial 수")
args = ap.parse_args()
out = rerun_postprocess_on_json(args.result_json, ob_n_trials=int(args.ob_axis_trials or 0))
logger.info("OK %s", out)
return 0 if out.get("ok") else 1
if __name__ == "__main__":
raise SystemExit(main())