#!/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 ( ORDERBOOK_TPE_AXIS_KEYS, RATCHET_TPE_AXIS_KEYS, WHIPSAW_TPE_AXIS_KEYS, r1, r2, r3, r4, suggest_orderbook_entry_tpe, suggest_ratchet_tiers_pct, suggest_whipsaw_tpe, ) # 래칫: 꼬리는 소폭 % (기존 메뉴 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", "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_TPE_AXIS_KEYS 와 동일 (아래 합침) TAIL_TPE_AXIS_KEYS: List[str] = ( list(_TAIL_BASE_KEYS) + list(RATCHET_TPE_AXIS_KEYS) + list(ORDERBOOK_TPE_AXIS_KEYS) + list(WHIPSAW_TPE_AXIS_KEYS) ) def tail_tpe_axis_keys(entry_mode: Optional[str] = None) -> List[str]: """align 스터디에는 limit_atr_mult 없음(지정가 모드 전용).""" keys = list(TAIL_TPE_AXIS_KEYS) if normalize_tpe_tail_entry_mode(entry_mode) == "limit_atr": if "limit_atr_mult" not in keys: keys.append("limit_atr_mult") return 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) # align 에서는 지정가 ATR 배수가 체결에 안 들어감 → 탐색 축에서 제외(스터디 공간 고정). if combo["entry_mode"] == "limit_atr": 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 # 3분 직전대비 등락. 구구간 max=-0.2 는 반등봉(+0.x%)을 한 번도 못 봄. # 실매 DB -10.0 / -0.5 포함. min 상한 -3 < max 하한 -2 → 항상 min= 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