- Optuna web jobs/TPE/apply snapshot·틱로더 정합, jobs limit·감사로그 - 백테 UI 호가모드·후보 적용 흐름, feed_collect_stats API/탭 - 가설검증·교차검증 룰, 4전략 스모크·OB slot41 진단 스크립트 Co-authored-by: Cursor <cursoragent@cursor.com>
153 lines
5.8 KiB
Python
153 lines
5.8 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
optuna_breakout_tpe_space.py — 돌파 Optuna 연속(TPE) 탐색 공간
|
||
기존 ``_breakout_grids()`` categorical 경로는 유지. ``skip_hts`` 항상 False.
|
||
|
||
호가 ON/OFF 는 trial 축이 아님 — CLI ``--orderbook-filter on|off`` 스터디 스위치.
|
||
(fixed/atr 손절과 같이 한 스터디에 섞지 않음 → 최대 fixed|atr × off|on = 4순차)
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
from typing import Any, Dict, List, Optional
|
||
|
||
import optuna
|
||
|
||
from kis_trader.backtest.optuna_tpe_common import (
|
||
ORDERBOOK_TPE_THRESHOLD_KEYS,
|
||
RATCHET_TPE_AXIS_KEYS,
|
||
r1,
|
||
r2,
|
||
suggest_orderbook_thresholds_tpe,
|
||
suggest_ratchet_tiers_pct,
|
||
)
|
||
|
||
# 매수 시간창(time_end_hm) 은 TPE 제외 — 운영 DB/골든타임 고정. 다단래칫·청산축만 탐색.
|
||
_BREAKOUT_BASE_KEYS: List[str] = [
|
||
"max_daily_chg",
|
||
"vol_mult",
|
||
"vol_window",
|
||
"min_turnover_1m_pct",
|
||
"prev_chg_min",
|
||
"prev_chg_max",
|
||
"min_price",
|
||
"tp_pct",
|
||
"sl_mode",
|
||
"trail_pct",
|
||
"trail_arm_pct",
|
||
"shoulder_min_high_pct",
|
||
"shoulder_cut_pct",
|
||
"lookback_min",
|
||
"confirm_margin_pct",
|
||
"body_min_pct",
|
||
"max_hold_bars",
|
||
]
|
||
# 돌파: 본축 + 래칫. 호가 ON/OFF·임계는 스터디 스위치(아래 breakout_tpe_axis_keys).
|
||
# 휩쏘는 전략 특성상 TPE 스킵 — 기존과 동일.
|
||
BREAKOUT_TPE_AXIS_KEYS: List[str] = (
|
||
list(_BREAKOUT_BASE_KEYS) + list(RATCHET_TPE_AXIS_KEYS)
|
||
)
|
||
|
||
|
||
def normalize_tpe_breakout_sl_mode(raw: Optional[Any] = None) -> str:
|
||
"""TPE는 손절모드를 탐색하지 않고 스터디마다 고정. fixed | atr."""
|
||
s = str(raw or "").strip().lower()
|
||
if s in ("atr", "atr_sl", "dynamic"):
|
||
return "atr"
|
||
return "fixed"
|
||
|
||
|
||
def normalize_tpe_breakout_ob_mode(raw: Optional[Any] = None) -> str:
|
||
"""돌파 TPE 호가 스터디 스위치. off | on (auto→on). trial 축 아님."""
|
||
s = str(raw or "off").strip().lower()
|
||
if s in ("on", "1", "true", "yes", "auto"):
|
||
return "on"
|
||
return "off"
|
||
|
||
|
||
def breakout_tpe_study_extra(
|
||
sl_mode: Optional[str] = None,
|
||
orderbook_filter: Optional[str] = None,
|
||
) -> str:
|
||
"""study 이름용 — 예: fixed_ob_off / atr_ob_on (한 스터디에 손절·호가 안 섞음)."""
|
||
sm = normalize_tpe_breakout_sl_mode(sl_mode)
|
||
om = normalize_tpe_breakout_ob_mode(orderbook_filter)
|
||
return f"{sm}_ob_{om}"
|
||
|
||
|
||
def breakout_tpe_axis_keys(
|
||
sl_mode: Optional[str] = None,
|
||
orderbook_filter: Optional[str] = None,
|
||
) -> List[str]:
|
||
"""fixed=sl_pct 만, atr=atr_sl_mult 만. 호가 ON 스터디만 임계값 축 추가."""
|
||
keys = list(BREAKOUT_TPE_AXIS_KEYS)
|
||
if normalize_tpe_breakout_sl_mode(sl_mode) == "atr":
|
||
keys.append("atr_sl_mult")
|
||
else:
|
||
keys.append("sl_pct")
|
||
if normalize_tpe_breakout_ob_mode(orderbook_filter) == "on":
|
||
keys.extend(list(ORDERBOOK_TPE_THRESHOLD_KEYS))
|
||
return keys
|
||
|
||
|
||
def suggest_breakout_params_tpe(
|
||
trial: optuna.Trial,
|
||
sl_mode: Optional[str] = None,
|
||
orderbook_filter: Optional[str] = None,
|
||
) -> Dict[str, Any]:
|
||
combo: Dict[str, Any] = {}
|
||
combo["max_daily_chg"] = r1(trial.suggest_float("max_daily_chg", 20.0, 55.0, step=1.0))
|
||
combo["vol_mult"] = r2(trial.suggest_float("vol_mult", 1.0, 5.0, step=0.1)) # 확장: 1.5→1.0, 4.0→5.0
|
||
combo["vol_window"] = trial.suggest_int("vol_window", 1, 15)
|
||
combo["min_turnover_1m_pct"] = r2(
|
||
trial.suggest_float("min_turnover_1m_pct", 0.05, 0.5, step=0.05),
|
||
)
|
||
combo["prev_chg_min"] = r2(trial.suggest_float("prev_chg_min", 0.2, 1.0, step=0.1))
|
||
combo["prev_chg_max"] = r1(trial.suggest_float("prev_chg_max", 5.0, 30.0, step=0.5))
|
||
if combo["prev_chg_min"] >= combo["prev_chg_max"]:
|
||
raise optuna.TrialPruned("prev_chg invalid")
|
||
|
||
combo["min_price"] = trial.suggest_int("min_price", 1000, 5000, step=500)
|
||
combo["tp_pct"] = r2(trial.suggest_float("tp_pct", 1.0, 25.0, step=0.5)) # 확장: 2.0→1.0, 18.0→25.0
|
||
# 한 스터디=한 손절모드. categorical 혼입 금지(웹 체크 2개면 잡 2개 순차).
|
||
combo["sl_mode"] = normalize_tpe_breakout_sl_mode(sl_mode)
|
||
if combo["sl_mode"] == "atr":
|
||
combo["atr_sl_mult"] = r2(trial.suggest_float("atr_sl_mult", 1.5, 3.5, step=0.1))
|
||
else:
|
||
combo["sl_pct"] = r2(trial.suggest_float("sl_pct", 1.0, 8.0, step=0.1)) # 확장: 2.0→1.0, 6.0→8.0
|
||
combo["trail_pct"] = r2(trial.suggest_float("trail_pct", 0.0, 6.0, step=0.1)) # 확장: 1.0→0.0, 4.0→6.0
|
||
combo["trail_arm_pct"] = r2(trial.suggest_float("trail_arm_pct", 0.0, 6.0, step=0.1)) # 확장: 1.0→0.0, 4.0→6.0
|
||
combo["shoulder_min_high_pct"] = r2(
|
||
trial.suggest_float("shoulder_min_high_pct", 1.0, 6.0, step=0.1),
|
||
)
|
||
combo["shoulder_cut_pct"] = r2(
|
||
trial.suggest_float("shoulder_cut_pct", 0.3, 1.5, step=0.1),
|
||
)
|
||
combo.update(
|
||
suggest_ratchet_tiers_pct(
|
||
trial,
|
||
off_token="",
|
||
n_max=3,
|
||
gain_low=2.0,
|
||
gain_high=15.0,
|
||
gain_step=0.5,
|
||
cut_low=0.5,
|
||
cut_high=3.0,
|
||
cut_step=0.1,
|
||
),
|
||
)
|
||
combo["lookback_min"] = trial.suggest_int("lookback_min", 1, 10)
|
||
combo["confirm_margin_pct"] = r2(
|
||
trial.suggest_float("confirm_margin_pct", 0.0, 1.0, step=0.1),
|
||
)
|
||
combo["body_min_pct"] = r2(trial.suggest_float("body_min_pct", 0.0, 0.5, step=0.1))
|
||
combo["max_hold_bars"] = trial.suggest_int("max_hold_bars", 0, 180, step=10)
|
||
|
||
combo["skip_hts_scan_dupes"] = False
|
||
# 호가: 스터디 스위치 (trial categorical ON/OFF 금지 — TPE가 OFF만 편애하던 구멍)
|
||
ob_on = normalize_tpe_breakout_ob_mode(orderbook_filter) == "on"
|
||
combo["_orderbook_filter_enabled"] = ob_on
|
||
combo["ob_filter_enabled"] = ob_on
|
||
if ob_on:
|
||
combo.update(suggest_orderbook_thresholds_tpe(trial))
|
||
return combo
|