#!/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, Optional 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 normalize_tpe_tail_entry_mode(raw: Optional[Any] = None) -> str: """TPE는 진입모드를 탐색하지 않고 스터디마다 고정. align | limit_atr.""" s = str(raw or "").strip().lower() if s in ("limit_atr", "limit", "atr_limit"): return "limit_atr" return "align" def suggest_tail_params_tpe( trial: optuna.Trial, entry_mode: Optional[str] = None, ) -> Dict[str, Any]: combo: Dict[str, Any] = {} # 한 스터디=한 모드. categorical 혼입 금지(웹 체크 2개면 잡 2개 순차). combo["entry_mode"] = normalize_tpe_tail_entry_mode(entry_mode) combo["cand_limit"] = trial.suggest_categorical("cand_limit", [0, 20]) combo["max_daily_change"] = r1( trial.suggest_float("max_daily_change", 10.0, 60.0, step=1.0), # 확장: 20→10, 45→60 ) combo["min_drop_rate"] = r3( trial.suggest_float("min_drop_rate", 0.005, 0.15, step=0.005), # 확장: 0.01→0.005, 0.08→0.15 ) 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, 12.0, step=0.5)) # 확장: 8.0→12.0 combo["atr_tp_min_pct"] = r2(trial.suggest_float("atr_tp_min_pct", 0.1, 2.0, step=0.1)) # 확장: 0.2→0.1 combo["atr_tp_max_pct"] = r1(trial.suggest_float("atr_tp_max_pct", 1.0, 10.0, step=0.5)) # 확장: 6.0→10.0 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", 3, 80, step=5) # 확장: 5→3, 60→80 combo["cooldown_min"] = r1(trial.suggest_float("cooldown_min", 0.0, 30.0, step=1.0)) # 확장: 15→30 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