#!/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())