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>
This commit is contained in:
Your Name
2026-08-27 15:23:44 +09:00
parent 5e44b86f8b
commit 8fbba264ba
30 changed files with 2973 additions and 186 deletions

View File

@@ -18,6 +18,7 @@ from kis_trader.backtest import breakout_backtest_common as bbc
from kis_trader.backtest.optuna_search_space import breakout_grid_axis_keys, suggest_breakout_params
from kis_trader.backtest.optuna_breakout_tpe_space import (
breakout_tpe_axis_keys,
normalize_tpe_breakout_ob_mode,
normalize_tpe_breakout_sl_mode,
suggest_breakout_params_tpe,
)
@@ -80,6 +81,7 @@ class BreakoutSearchContext:
cache_holder: Dict[str, Any] = field(default_factory=dict)
shared_tick_store: Any = None # ws_ticks 공유메모리 핸들 (종료 시 unlink)
tpe_sl_mode: str = "fixed" # TPE 고정 손절모드(탐색 축 아님)
tpe_ob_mode: str = "off" # TPE 호가 스터디 스위치 on|off (탐색 축 아님)
def prepare_breakout_search_context(
@@ -115,16 +117,35 @@ def prepare_breakout_search_context(
base_fixed["skip_hts_scan_dupes"] = False
apply_session_to_fixed(base_fixed, time_start_hm=time_start_hm, time_end_hm=time_end_hm)
_ob_mode = (orderbook_filter or "off").strip().lower()
# 돌파 TPE: 호가=스터디 스위치(--orderbook-filter on|off). trial categorical 금지.
tpe_ob_mode = "off"
if mode == "tpe":
tpe_ob_mode = normalize_tpe_breakout_ob_mode(orderbook_filter)
_ob_mode = tpe_ob_mode
else:
_ob_mode = (orderbook_filter or "off").strip().lower()
if _ob_mode == "off":
base_fixed["_orderbook_filter_enabled"] = False
elif _ob_mode == "on":
elif _ob_mode in ("on", "auto"):
base_fixed["_orderbook_filter_enabled"] = True
from kis_trader.backtest.optuna_tpe_common import optuna_tpe_needs_orderbook_feed
if mode == "tpe":
# 글로벌 INCLUDE_ORDERBOOK 때문에 off 스터디까지 스냅 로드하던 구멍 차단
need_ob_feed = tpe_ob_mode == "on"
else:
need_ob_feed = optuna_tpe_needs_orderbook_feed(mode, _ob_mode)
ob_filter_on = bool(base_fixed.get("_orderbook_filter_enabled")) or _ob_mode == "auto"
if need_ob_feed:
base_fixed["backtest_use_trigger_snapshot_db"] = True
# 돌파 filter_eval 거의 없음 → 호가 ON 스터디는 kiwoom_0d 본체 재계산
base_fixed["backtest_use_kiwoom_body_snapshot"] = True
base_fixed["_backtest_use_kiwoom_body"] = True
logger.info(
"📌 호가필터: %s (%s)",
"📌 호가필터: %s (%s)%s",
_ob_mode.upper(),
"적용" if ob_filter_on else "스킵 — 코어 파라미터 순수 탐색",
"스냅로드" if need_ob_feed else ("적용" if ob_filter_on else "스킵 — 코어 파라미터 순수 탐색"),
" · 돌파TPE 스터디스위치" if mode == "tpe" else "",
)
db = TradeDB()
@@ -158,8 +179,9 @@ def prepare_breakout_search_context(
tpe_sl_mode = normalize_tpe_breakout_sl_mode(sl_mode)
base_fixed["sl_mode"] = tpe_sl_mode
logger.info(
"📌 TPE 손절모드 고정: %s (탐색 축 아님 · atr_sl_*/sl_pct 중 해당만)",
"📌 TPE 손절모드 고정: %s · 호가스위치: %s (둘 다 탐색 축 아님)",
tpe_sl_mode,
tpe_ob_mode,
)
from kis_trader.backtest.universe_history_source import (
@@ -238,25 +260,42 @@ def prepare_breakout_search_context(
pass
# grid 는 상단에서 mode별 설정 (tpe=빈 dict). grids[mode] 재조회 금지.
_ob_axes = ("max_spread_pct", "min_bid_ask_ratio", "ask_wall_max_qty")
# TPE=ask_max_mult · Grid 레거시=ask_wall_max_qty — 둘 다 스윕 축으로 인정
_ob_axes = ("max_spread_pct", "min_bid_ask_ratio", "ask_max_mult", "ask_wall_max_qty")
_ob_sweeping = any(len(set(grid.get(k) or [])) > 1 for k in _ob_axes)
if ob_filter_on and _ob_sweeping:
base_fixed["backtest_use_kiwoom_body_snapshot"] = True
base_fixed["_backtest_use_kiwoom_body"] = True
# engine_probe 는 위에서 만들었으므로 body/스냅 플래그를 동기화 (로드·trial 공통)
for _k in (
"backtest_use_trigger_snapshot_db",
"backtest_use_kiwoom_body_snapshot",
"_backtest_use_kiwoom_body",
):
if _k in base_fixed:
engine_probe[_k] = base_fixed[_k]
orderbook_by_code: Dict[str, Any] = {}
program_by_code: Dict[str, Any] = {}
log_verdict_by_code: Dict[str, Any] = {}
_snap_db = TradeDB()
try:
from kis_trader.backtest.trigger_snapshot_loader import load_trigger_snapshots_by_code
orderbook_by_code, program_by_code, trigger_snap_meta = load_trigger_snapshots_by_code(
_snap_db, start_key, end_key, set(codes_candles.keys()),
engine_params=engine_probe, strategy="BREAKOUT",
)
log_verdict_by_code = trigger_snap_meta.get("log_verdict_by_code") or {}
finally:
_snap_db.close()
if need_ob_feed:
_snap_db = TradeDB()
try:
from kis_trader.backtest.trigger_snapshot_loader import load_trigger_snapshots_by_code
_load_params = dict(engine_probe)
_load_params["_orderbook_filter_enabled"] = True
orderbook_by_code, program_by_code, trigger_snap_meta = load_trigger_snapshots_by_code(
_snap_db, start_key, end_key, set(codes_candles.keys()),
engine_params=_load_params, strategy="BREAKOUT",
)
log_verdict_by_code = trigger_snap_meta.get("log_verdict_by_code") or {}
ob_rows = int(trigger_snap_meta.get("ws_orderbook_rows_loaded") or 0)
logger.info("✅ 호가ON 스터디용 ws_orderbook %s", f"{ob_rows:,}")
finally:
_snap_db.close()
else:
logger.info("📌 호가OFF 스터디 — 호가 스냅 로드 생략")
universe_by_slot = None
fallback_sim_interval = 5
@@ -315,12 +354,17 @@ def prepare_breakout_search_context(
total_budget_krw=total_budget_f,
period_days=period_days,
portfolio=portfolio,
grid_keys=breakout_tpe_axis_keys(tpe_sl_mode) if mode == "tpe" else breakout_grid_axis_keys(mode),
grid_keys=(
breakout_tpe_axis_keys(tpe_sl_mode, orderbook_filter=tpe_ob_mode)
if mode == "tpe"
else breakout_grid_axis_keys(mode)
),
start_key=start_key,
end_key=end_key,
cache_holder=cache_holder,
shared_tick_store=shared_tick_store,
tpe_sl_mode=tpe_sl_mode,
tpe_ob_mode=tpe_ob_mode,
)
@@ -365,7 +409,11 @@ def run_breakout_optuna(
def objective(trial: optuna.Trial) -> float:
if ctx.mode == "tpe":
combo = suggest_breakout_params_tpe(trial, sl_mode=ctx.tpe_sl_mode)
combo = suggest_breakout_params_tpe(
trial,
sl_mode=ctx.tpe_sl_mode,
orderbook_filter=ctx.tpe_ob_mode,
)
else:
combo = suggest_breakout_params(trial, ctx.mode)
result = evaluate_breakout_param_combo(