Files
kis_bot/kis_trader/backtest/optuna_breakout_tpe_space.py
Your Name 8fbba264ba feat(옵투나·웹): 후처리 재탐색·ob_modes·적용감사·수집통계
- Optuna web jobs/TPE/apply snapshot·틱로더 정합, jobs limit·감사로그
- 백테 UI 호가모드·후보 적용 흐름, feed_collect_stats API/탭
- 가설검증·교차검증 룰, 4전략 스모크·OB slot41 진단 스크립트

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-27 15:23:44 +09:00

153 lines
5.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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