#!/usr/bin/env python3 """ optuna_tail_tpe_space.py — 꼬리 Optuna 연속(TPE) 탐색 공간 기존 ``_tail_grids()`` categorical 유지. ``skip_hts_scan_dupes=False`` 고정. 비율 축은 엔진과 동일(소수, UI% 아님) — evaluate/apply 경로와 맞춤. multivariate TPE: 모든 축을 **먼저** suggest 한 뒤, 제약(ATR min/max·래칫 오름차순 등)만 맨 끝에서 TrialPruned. 중간 prune 시 뒤쪽 키가 trial마다 빠져 independent sampling 경고가 난다. """ from __future__ import annotations from typing import Any, Dict, List import optuna from kis_trader.backtest.optuna_tpe_common import ( RATCHET_TPE_AXIS_KEYS, r1, r2, r3, r4, suggest_ratchet_tiers_pct, ) # 래칫: 꼬리는 소폭 % (기존 메뉴 0.3~2.0 대역) — 숫자축 + 조립 문자열 # 휩쏘: 실매 DB 기본(enabled=false, subbar=30, lookback=90, dip=0.003, recovery=0.001) 그리드 포함 _TAIL_BASE_KEYS: List[str] = [ "entry_mode", "cand_limit", "max_daily_change", "min_drop_rate", "min_recovery_ratio", "tail_ratio_min", "tail_pct_min", "max_rec_3m", "shoulder_min_high", "shoulder_cut_pct", "stop_atr_mult", "target_atr_mult", "atr_sl_min_pct", "atr_sl_max_pct", "atr_tp_min_pct", "atr_tp_max_pct", "tail_vol_mult", "tail_vol_win", "limit_atr_mult", "symbol_daily_loss_limit_pct", "symbol_daily_loss_limit_krw", "reentry_min_edge_krw", "reentry_require_nonneg", "max_daily", "cooldown_min", "bar_chg_min_pct", "bar_chg_max_pct", "rsi_threshold", "pattern_pin", "pattern_engulfing", "pattern_piercing", "pattern_harami", "pattern_doji", "pattern_morning_star", # 당일손익 다단트레일(trail_tiers/drop/arm) — 운영 리스크 손잡이. TPE·apply 탐색 제외(DB/UI 고정). "max_loss_krw", "whipsaw_enabled", "whipsaw_subbar_sec", "whipsaw_lookback_sec", "whipsaw_dip_pct", "whipsaw_recovery_tol_pct", ] TAIL_TPE_AXIS_KEYS: List[str] = list(_TAIL_BASE_KEYS) + list(RATCHET_TPE_AXIS_KEYS) def tail_tpe_axis_keys() -> List[str]: return list(TAIL_TPE_AXIS_KEYS) def suggest_tail_params_tpe(trial: optuna.Trial) -> Dict[str, Any]: combo: Dict[str, Any] = {} combo["entry_mode"] = "align" combo["cand_limit"] = trial.suggest_categorical("cand_limit", [0, 20]) combo["max_daily_change"] = r1( trial.suggest_float("max_daily_change", 20.0, 45.0, step=1.0), ) combo["min_drop_rate"] = r3( trial.suggest_float("min_drop_rate", 0.01, 0.08, step=0.005), ) combo["min_recovery_ratio"] = r2( trial.suggest_float("min_recovery_ratio", 0.08, 0.35, step=0.01), ) combo["tail_ratio_min"] = r2(trial.suggest_float("tail_ratio_min", 0.4, 1.5, step=0.1)) combo["tail_pct_min"] = r4( trial.suggest_float("tail_pct_min", 0.0005, 0.01, step=0.0005), ) combo["max_rec_3m"] = r2(trial.suggest_float("max_rec_3m", 0.7, 0.98, step=0.01)) combo["shoulder_min_high"] = r4( trial.suggest_float("shoulder_min_high", 0.002, 0.015, step=0.001), ) combo["shoulder_cut_pct"] = r4( trial.suggest_float("shoulder_cut_pct", 0.0005, 0.005, step=0.0005), ) combo["stop_atr_mult"] = r2(trial.suggest_float("stop_atr_mult", 0.8, 3.0, step=0.1)) combo["target_atr_mult"] = r2( trial.suggest_float("target_atr_mult", 0.8, 3.5, step=0.1), ) combo["atr_sl_min_pct"] = r2(trial.suggest_float("atr_sl_min_pct", 0.3, 1.0, step=0.1)) combo["atr_sl_max_pct"] = r1(trial.suggest_float("atr_sl_max_pct", 1.0, 8.0, step=0.5)) combo["atr_tp_min_pct"] = r2(trial.suggest_float("atr_tp_min_pct", 0.2, 1.0, step=0.1)) combo["atr_tp_max_pct"] = r1(trial.suggest_float("atr_tp_max_pct", 1.5, 6.0, step=0.5)) combo["tail_vol_mult"] = r2(trial.suggest_float("tail_vol_mult", 0.0, 4.0, step=0.1)) combo["tail_vol_win"] = trial.suggest_int("tail_vol_win", 2, 8) combo["limit_atr_mult"] = r2(trial.suggest_float("limit_atr_mult", 0.8, 2.5, step=0.1)) # 꼬리 래칫: OFF=\"off\" / gain·cut 소폭% (엔진 문자열과 동일) combo.update( suggest_ratchet_tiers_pct( trial, off_token="off", n_max=3, gain_low=0.3, gain_high=3.0, gain_step=0.1, cut_low=0.15, cut_high=0.5, cut_step=0.05, ), ) combo["symbol_daily_loss_limit_pct"] = r2( trial.suggest_float("symbol_daily_loss_limit_pct", 0.0, 3.0, step=0.5), ) combo["symbol_daily_loss_limit_krw"] = trial.suggest_int( "symbol_daily_loss_limit_krw", 0, 80000, step=10000, ) combo["reentry_min_edge_krw"] = trial.suggest_int( "reentry_min_edge_krw", 0, 1000, step=100, ) combo["reentry_require_nonneg"] = False combo["max_daily"] = trial.suggest_int("max_daily", 5, 60, step=5) combo["cooldown_min"] = r1(trial.suggest_float("cooldown_min", 0.0, 15.0, step=1.0)) combo["bar_chg_min_pct"] = r1( trial.suggest_float("bar_chg_min_pct", -15.0, -3.0, step=0.5), ) combo["bar_chg_max_pct"] = r2( trial.suggest_float("bar_chg_max_pct", -2.0, -0.2, step=0.1), ) combo["rsi_threshold"] = r1(trial.suggest_float("rsi_threshold", 70.0, 95.0, step=1.0)) for pk in ( "pattern_pin", "pattern_engulfing", "pattern_piercing", "pattern_harami", "pattern_doji", "pattern_morning_star", ): combo[pk] = trial.suggest_categorical(pk, [False, True]) combo["max_loss_krw"] = trial.suggest_int("max_loss_krw", 50000, 300000, step=25000) # 휩쏘 TRIGGER — 실매값(False/30/90/0.003/0.001) 포함. ON·OFF·초·% 축 분리. combo["whipsaw_enabled"] = trial.suggest_categorical( "whipsaw_enabled", [False, True], ) combo["whipsaw_subbar_sec"] = trial.suggest_categorical( "whipsaw_subbar_sec", [15, 30, 45, 60], ) combo["whipsaw_lookback_sec"] = trial.suggest_categorical( "whipsaw_lookback_sec", [60, 90, 120, 180], ) combo["whipsaw_dip_pct"] = r4( trial.suggest_float("whipsaw_dip_pct", 0.001, 0.01, step=0.001), ) combo["whipsaw_recovery_tol_pct"] = r4( trial.suggest_float("whipsaw_recovery_tol_pct", 0.0005, 0.003, step=0.0005), ) combo["skip_hts_scan_dupes"] = False # --- 제약 prune: 모든 suggest 이후에만 (키 공간 고정) --- if combo["atr_sl_min_pct"] >= combo["atr_sl_max_pct"]: raise optuna.TrialPruned("atr_sl min>=max") if combo["atr_tp_min_pct"] >= combo["atr_tp_max_pct"]: raise optuna.TrialPruned("atr_tp min>=max") if combo["bar_chg_min_pct"] >= combo["bar_chg_max_pct"]: raise optuna.TrialPruned("bar_chg invalid") if combo.pop("_ratchet_ascending_ok", True) is False: raise optuna.TrialPruned("ratchet gain not ascending") return combo