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.
277 lines
10 KiB
Python
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())
|