feat(옵투나): score·min_trades·후처리 재탐색 및 웹 job 개선
PnL/(MDD+ADD) score·legacy 정렬·거래일×min_trades 게이트를 공통화한다. 후처리 ob_modes·study store·4전략 TPE 순차 스크립트와 문서를 갱신한다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -474,7 +474,7 @@ def run_tail_optuna(
|
||||
min_trades: int,
|
||||
min_win_rate: float,
|
||||
min_pf: float,
|
||||
sort_by: str = "pnl",
|
||||
sort_by: str = "score",
|
||||
sampler_name: str = "tpe",
|
||||
seed: Optional[int] = None,
|
||||
n_jobs: int = 1,
|
||||
@@ -529,17 +529,21 @@ def run_tail_optuna(
|
||||
trial.set_user_attr("gates_ok", False)
|
||||
return _FAIL_OBJECTIVE
|
||||
|
||||
from kis_trader.backtest.optuna_common import (
|
||||
optuna_score_fields_from_trial,
|
||||
optuna_store_trial_score_user_attrs,
|
||||
)
|
||||
trial.set_user_attr("gates_ok", True)
|
||||
trial.set_user_attr("total_pnl", float(result["total_pnl"]))
|
||||
trial.set_user_attr("win_rate", float(result["win_rate"]))
|
||||
trial.set_user_attr("pf", float(result.get("pf") or 0))
|
||||
trial.set_user_attr("mdd", float(result.get("mdd") or 0))
|
||||
trial.set_user_attr("total_trades", int(result["total_trades"]))
|
||||
trial.set_user_attr("params_json", json.dumps(result["params"], ensure_ascii=False))
|
||||
set_optuna_trial_stability_attrs(trial, result)
|
||||
|
||||
if sort_by == "win_rate":
|
||||
return float(result["win_rate"])
|
||||
return float(result["total_pnl"])
|
||||
return optuna_store_trial_score_user_attrs(
|
||||
trial, result, sort_by, start=ctx.start, end=ctx.end, strategy="tail",
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"🔬 Optuna 시작 | study=%s | trials=%d | sampler=%s | storage=%s | n_jobs=%d",
|
||||
@@ -561,6 +565,7 @@ def run_tail_optuna(
|
||||
logger.info("✅ Optuna 완료 | %.1f초 | 완료 trial %d", elapsed, len(study.trials))
|
||||
|
||||
# JSON export — study.user_attrs 기준 (n_jobs>1 에도 안전)
|
||||
from kis_trader.backtest.optuna_common import optuna_score_fields_from_trial
|
||||
passing: List[Dict[str, Any]] = []
|
||||
for trial in study.trials:
|
||||
if trial.state != optuna.trial.TrialState.COMPLETE:
|
||||
@@ -579,14 +584,15 @@ def run_tail_optuna(
|
||||
"win_rate": float(trial.user_attrs.get("win_rate") or 0),
|
||||
"total_pnl": float(trial.user_attrs.get("total_pnl") or 0),
|
||||
"pf": float(trial.user_attrs.get("pf") or 0),
|
||||
"mdd": float(trial.user_attrs.get("mdd") or 0),
|
||||
**optuna_score_fields_from_trial(trial),
|
||||
"period_daily_avg_pnl": float(trial.user_attrs.get("period_daily_avg_pnl") or 0),
|
||||
"optuna_trial_number": trial.number,
|
||||
}
|
||||
row.update(stability_fields_from_trial_attrs(trial))
|
||||
passing.append(row)
|
||||
if sort_by == "pnl":
|
||||
passing.sort(key=lambda r: (-float(r["total_pnl"]), -float(r["win_rate"])))
|
||||
else:
|
||||
passing.sort(key=lambda r: (-float(r["win_rate"]), -float(r["total_pnl"])))
|
||||
from kis_trader.backtest.optuna_common import _sort_optuna_rows
|
||||
passing = _sort_optuna_rows(passing, sort_by)
|
||||
|
||||
tiers = build_optuna_result_tiers(passing, sort_by=sort_by)
|
||||
|
||||
@@ -621,6 +627,8 @@ def run_tail_optuna(
|
||||
"elapsed_sec": round(elapsed, 1),
|
||||
**tiers,
|
||||
}
|
||||
from kis_trader.backtest.optuna_common import annotate_optuna_period_daily_avg
|
||||
annotate_optuna_period_daily_avg(out_data)
|
||||
|
||||
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
out_name = f"optuna_tail_{ctx.mode}_{ts}.json"
|
||||
@@ -723,7 +731,7 @@ def run_tail_optuna(
|
||||
|
||||
def apply_best_trial(study: optuna.Study, ctx: TailSearchContext) -> bool:
|
||||
"""사후게이트 통과 trial → env_config (총손익≤0 스킵)."""
|
||||
trial = pick_gated_apply_trial(study, sort_by="pnl", fail_objective=_FAIL_OBJECTIVE)
|
||||
trial = pick_gated_apply_trial(study, sort_by="score", fail_objective=_FAIL_OBJECTIVE)
|
||||
if trial is None:
|
||||
logger.warning(
|
||||
"⚠️ 사후게이트(results_gated) 통과 trial 없음 — DB 미적용"
|
||||
@@ -808,7 +816,7 @@ def main() -> None:
|
||||
parser.add_argument(
|
||||
"--sort-by", default=None,
|
||||
dest="sort_by",
|
||||
help="목적함수: tail/breakout pnl|win_rate, momentum score|pnl|win_rate (미지정=전략 기본)",
|
||||
help="목적함수: score|score_legacy|pnl|daily_avg|win_rate (미지정=score 전 전략 공통)",
|
||||
)
|
||||
add_search_filter_cli_args(parser)
|
||||
# Optuna: 탐색 중 승률·PF 게이트 OFF(0) — TPE가 PnL 차이를 학습. 사후 results_gated 로 후보 분리.
|
||||
@@ -918,6 +926,9 @@ def main() -> None:
|
||||
logger.error("❌ --strategy 는 tail/momentum/us_momentum/breakout/scalp 중 하나")
|
||||
sys.exit(2)
|
||||
|
||||
# CLI --min_trades 와 사후 results_gated 거래수 게이트 정렬 (seq: tail=1 / 타전략=18 등)
|
||||
os.environ["PARAM_SEARCH_OPTUNA_REPORT_MIN_TRADES"] = str(max(1, int(args.min_trades)))
|
||||
|
||||
allowed_modes = STRATEGY_MODES.get(strategy, [])
|
||||
mode = (args.mode or "fast").strip().lower()
|
||||
if mode not in allowed_modes:
|
||||
@@ -926,14 +937,15 @@ def main() -> None:
|
||||
|
||||
sort_by = (args.sort_by or "").strip().lower()
|
||||
if not sort_by:
|
||||
sort_by = "score" if strategy in ("momentum", "us_momentum", "scalp") else "pnl"
|
||||
momentum_sort = {"score", "pnl", "win_rate"}
|
||||
basic_sort = {"pnl", "win_rate"}
|
||||
if strategy in ("momentum", "us_momentum", "scalp") and sort_by not in momentum_sort:
|
||||
logger.error("❌ %s --sort-by 는 score|pnl|win_rate", strategy)
|
||||
sys.exit(2)
|
||||
if strategy in ("tail", "breakout") and sort_by not in basic_sort:
|
||||
logger.error("❌ %s --sort-by 는 pnl|win_rate", strategy)
|
||||
from kis_trader.backtest.optuna_common import OPTUNA_SORT_BY_DEFAULT
|
||||
sort_by = OPTUNA_SORT_BY_DEFAULT
|
||||
from kis_trader.backtest.optuna_common import (
|
||||
OPTUNA_SORT_BY_CHOICES,
|
||||
normalize_optuna_sort_by,
|
||||
)
|
||||
sort_by = normalize_optuna_sort_by(sort_by, web=False)
|
||||
if sort_by not in OPTUNA_SORT_BY_CHOICES:
|
||||
logger.error("❌ --sort-by 는 score|score_legacy|pnl|daily_avg|win_rate")
|
||||
sys.exit(2)
|
||||
|
||||
lock_name = optuna_run_lock_name(strategy)
|
||||
|
||||
Reference in New Issue
Block a user