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:
Your Name
2026-08-28 16:46:27 +09:00
parent a0fe66bc11
commit 1387fbdf47
10 changed files with 583 additions and 136 deletions

View File

@@ -366,15 +366,17 @@ def _make_sampler(name: str, seed: Optional[int]):
return TPESampler(seed=seed, multivariate=True, warn_independent_sampling=False)
def _scalp_objective_value(result: Dict[str, Any], sort_by: str) -> float:
pnl = float(result["total_pnl"])
if sort_by == "score":
mdd_floor = get_env_float("SCALP_SCORE_MDD_FLOOR", 5000.0)
mdd = float(result.get("mdd") or 0)
return pnl / max(mdd, mdd_floor)
if sort_by == "win_rate":
return float(result["win_rate"])
return pnl
def _scalp_objective_value(
result: Dict[str, Any],
sort_by: str,
*,
start: Any = None,
end: Any = None,
) -> float:
from kis_trader.backtest.optuna_common import optuna_objective_value
return float(optuna_objective_value(
result, sort_by, start=start, end=end, strategy="scalp",
))
def run_scalp_optuna(
@@ -386,7 +388,7 @@ def run_scalp_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,
@@ -438,23 +440,21 @@ def run_scalp_optuna(
if result is None:
trial.set_user_attr("gates_ok", False)
return _FAIL_OBJECTIVE
obj = _scalp_objective_value(result, sort_by)
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(
"score",
float(obj if sort_by == "score" else _scalp_objective_value(result, "score")),
)
trial.set_user_attr("total_trades", int(result["total_trades"]))
trial.set_user_attr(
"merged_json",
json.dumps(result.get("merged_params") or {}, ensure_ascii=False),
)
from kis_trader.backtest.optuna_common import optuna_store_trial_score_user_attrs
set_optuna_trial_stability_attrs(trial, result)
return float(obj)
return optuna_store_trial_score_user_attrs(
trial, result, sort_by, start=ctx.start, end=ctx.end, strategy="scalp",
)
logger.info(
"🔬 Optuna SCALP | study=%s | mode=%s | trials=%d | sort=%s | universe=%s | ticks=%s",
@@ -475,6 +475,7 @@ def run_scalp_optuna(
)
elapsed = time.time() - t0
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:
@@ -494,18 +495,15 @@ def run_scalp_optuna(
"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),
"score": float(trial.user_attrs.get("score") 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 == "score":
passing.sort(key=lambda r: (-r["score"], -r["total_pnl"], -r["win_rate"]))
elif sort_by == "win_rate":
passing.sort(key=lambda r: (-r["win_rate"], -r["total_pnl"]))
else:
passing.sort(key=lambda r: (-r["total_pnl"], -r["win_rate"]))
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)
@@ -540,6 +538,8 @@ def run_scalp_optuna(
"tick_backtest": ctx.tick_backtest_meta,
**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_path = os.path.join(_results_dir_for_write(), f"optuna_scalp_{ctx.mode}_{ts}.json")