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

@@ -25,11 +25,91 @@ from typing import Any, Dict, List, Optional
ROOT = Path(__file__).resolve().parents[2]
JOBS_DIR = ROOT / "logs" / "optuna_web_jobs"
RESULTS_DIR = ROOT / "kis_trader" / "backtest" / "results"
# Optuna 웹/API 적용 감사 — 한 줄 JSON (역추적: source=mode|gated · tp/sl)
APPLY_AUDIT_PATH = ROOT / "logs" / "optuna_apply_audit.jsonl"
PY = ROOT / ".venv" / "bin" / "python"
_STRATS = ("momentum", "us_momentum", "tail", "breakout", "scalp")
def _pct_keys_from_params(params: Optional[Dict[str, Any]]) -> Dict[str, Any]:
"""적용 로그용 — 익절/손절 등 UI% 축만 짧게."""
if not isinstance(params, dict):
return {}
out: Dict[str, Any] = {}
for k in (
"tp_pct", "sl_pct", "tp_max_pct", "drop_rate",
"trail_pct", "trail_arm_pct", "trail_trigger", "trail_stop",
"shoulder_min_high", "shoulder_min_high_pct",
"shoulder_cut_pct",
):
if k in params and params[k] is not None:
out[k] = params[k]
return out
def record_optuna_apply_audit(
*,
ok: bool,
strategy: str = "",
source: str = "",
rank: int = 0,
upto: str = "",
trial: Any = None,
job_id: Optional[str] = None,
study_name: str = "",
result_json: str = "",
params: Optional[Dict[str, Any]] = None,
metrics: Optional[Dict[str, Any]] = None,
error: str = "",
note: str = "",
) -> None:
"""적용 성공/실패를 JSONL + 표준 로그에 남김 (HTTP access body 없음 보완)."""
import logging
lg = logging.getLogger("optuna_apply")
row = {
"ts": _now_iso(),
"ok": bool(ok),
"strategy": str(strategy or ""),
"source": str(source or ""),
"rank": int(rank or 0),
"upto": str(upto or ""),
"trial": trial,
"job_id": job_id or None,
"study_name": str(study_name or "") or None,
"result_json": str(result_json or "") or None,
"params": _pct_keys_from_params(params),
"metrics": {
k: (metrics or {}).get(k)
for k in (
"total_pnl", "total_trades", "win_rate", "pf",
"optuna_trial_number",
)
if metrics and k in metrics
} or None,
"error": (error or "")[:500] or None,
"note": (note or "")[:300] or None,
}
try:
APPLY_AUDIT_PATH.parent.mkdir(parents=True, exist_ok=True)
with APPLY_AUDIT_PATH.open("a", encoding="utf-8") as f:
f.write(json.dumps(row, ensure_ascii=False) + "\n")
except Exception as exc:
lg.warning("optuna apply audit file write failed: %s", exc)
if ok:
lg.info(
"OPTUNA_APPLY ok strat=%s source=%s rank=%s upto=%s trial=%s params=%s job=%s",
row["strategy"], row["source"], row["rank"], row["upto"],
row["trial"], row["params"], row["job_id"],
)
else:
lg.warning(
"OPTUNA_APPLY fail strat=%s source=%s rank=%s err=%s job=%s",
row["strategy"], row["source"], row["rank"], row["error"], row["job_id"],
)
def _result_study_name(meta: Optional[Dict[str, Any]]) -> str:
"""테이블 PK. 순차 잡의 seq_* 이름은 쓰지 않고 현재/활성 study."""
m = meta or {}
@@ -135,6 +215,42 @@ def _normalize_breakout_sl_modes(raw: Any) -> List[str]:
return items or ["fixed"]
def _normalize_breakout_ob_modes(raw: Any) -> List[str]:
"""웹/CLI: off, on. 빈값이면 off 1개(호가OFF 스터디)."""
from kis_trader.backtest.optuna_breakout_tpe_space import (
normalize_tpe_breakout_ob_mode,
)
items: List[str] = []
if raw is None or raw is False:
seq: List[Any] = []
elif isinstance(raw, str):
seq = [x for x in raw.replace(",", " ").split() if x.strip()]
elif isinstance(raw, (list, tuple)):
seq = list(raw)
else:
seq = [raw]
for x in seq:
om = normalize_tpe_breakout_ob_mode(x)
if om not in items:
items.append(om)
return items or ["off"]
def _parse_breakout_seq_extra(extra: str) -> Tuple[str, str]:
"""순차 extra → (sl_mode, ob_mode). 예: fixed_ob_off / atr_ob_on / fixed(구형)."""
from kis_trader.backtest.optuna_breakout_tpe_space import (
normalize_tpe_breakout_ob_mode,
normalize_tpe_breakout_sl_mode,
)
e = str(extra or "").strip().lower()
if "_ob_" in e:
sm, _, om = e.partition("_ob_")
return normalize_tpe_breakout_sl_mode(sm), normalize_tpe_breakout_ob_mode(om)
return normalize_tpe_breakout_sl_mode(e or "fixed"), "off"
def _normalize_tail_entry_modes(raw: Any) -> List[str]:
"""웹/CLI: align, limit_atr. 빈값이면 align 1개(기존 TPE와 동일)."""
from kis_trader.backtest.optuna_tail_tpe_space import normalize_tpe_tail_entry_mode
@@ -507,13 +623,16 @@ _SEQ_START_RE = re.compile(
def _seq_step_catalog(meta: Dict[str, Any]) -> List[Dict[str, str]]:
"""순차 한 칸 = 전략(+꼬리 진입/+돌파 손절). 스크립트 run_one 과 동일 순서."""
"""순차 한 칸 = 전략(+꼬리 진입/+돌파 손절×호가). 스크립트 run_one 과 동일 순서."""
from kis_trader.backtest.optuna_breakout_tpe_space import breakout_tpe_study_extra
picked = list(meta.get("strategies") or [])
if not picked:
raw = str(meta.get("strategy") or "")
picked = [s.strip() for s in raw.split(",") if s.strip()]
tail_ems = list(meta.get("tail_entry_modes") or ["align"])
bo_sms = list(meta.get("breakout_sl_modes") or ["fixed"])
bo_oms = list(meta.get("breakout_ob_modes") or ["off"])
out: List[Dict[str, str]] = []
for s in picked:
s = str(s or "").strip().lower()
@@ -522,7 +641,11 @@ def _seq_step_catalog(meta: Dict[str, Any]) -> List[Dict[str, str]]:
out.append({"strategy": "tail", "extra": str(em or "align")})
elif s == "breakout":
for sm in bo_sms:
out.append({"strategy": "breakout", "extra": str(sm or "fixed")})
for om in bo_oms:
out.append({
"strategy": "breakout",
"extra": breakout_tpe_study_extra(sm, om),
})
elif s:
out.append({"strategy": s, "extra": ""})
return out
@@ -659,8 +782,23 @@ def _join_argv_for_study(
ems = list(meta.get("tail_entry_modes") or ["align"])
argv.extend(["--entry-mode", extra or str(ems[0] if ems else "align")])
if strat == "breakout":
sms = list(meta.get("breakout_sl_modes") or ["fixed"])
argv.extend(["--sl-mode", extra or str(sms[0] if sms else "fixed")])
if extra:
sm, om = _parse_breakout_seq_extra(extra)
else:
sms = list(meta.get("breakout_sl_modes") or ["fixed"])
oms = list(meta.get("breakout_ob_modes") or ["off"])
sm = str(sms[0] if sms else "fixed")
om = str(oms[0] if oms else "off")
argv.extend(["--sl-mode", sm])
# 베이스 argv 의 --orderbook-filter off 를 스터디 스위치로 덮어씀
if "--orderbook-filter" in argv:
_i = argv.index("--orderbook-filter")
if _i + 1 < len(argv):
argv[_i + 1] = om
else:
argv.extend(["--orderbook-filter", om])
else:
argv.extend(["--orderbook-filter", om])
for flag, key in (
("--candle-source", "candle_source"),
("--tick-source", "tick_source"),
@@ -864,6 +1002,133 @@ def _study_progress(study_name: str, trials_total: int) -> Dict[str, Any]:
return out
def _as_bool_opt(v: Any) -> Optional[bool]:
if v is None or v == "":
return None
if isinstance(v, bool):
return v
s = str(v).strip().lower()
if s in ("1", "true", "yes", "on"):
return True
if s in ("0", "false", "no", "off"):
return False
return None
def _ob_whip_ui_from_params(params: Optional[Dict[str, Any]]) -> Dict[str, Any]:
"""TopN 표용 — 본 TPE trial 호가·익절·손절·휩쏘 요약 (사후 8방과 무관).
상세 보기 없이 적용값을 고를 수 있게 ON/OFF + 핵심 수치를 전부 내려준다.
"""
p = params if isinstance(params, dict) else {}
ob_on = _as_bool_opt(p.get("_orderbook_filter_enabled"))
if ob_on is None:
ob_on = _as_bool_opt(p.get("ob_filter_enabled"))
whip_on = _as_bool_opt(p.get("whipsaw_enabled"))
if whip_on is None:
whip_on = _as_bool_opt(p.get("whipsaw_filter_enabled"))
def _f(key: str) -> Optional[float]:
v = p.get(key)
if v is None or v == "":
return None
try:
return float(v)
except (TypeError, ValueError):
return None
def _i(key: str) -> Optional[int]:
v = _f(key)
if v is None:
return None
try:
return int(round(v))
except (TypeError, ValueError):
return None
spread = _f("max_spread_pct")
if spread is None:
spread = _f("orderbook_max_spread_pct")
ratio = _f("min_bid_ask_ratio")
if ratio is None:
ratio = _f("orderbook_min_bid_ask_ratio")
ask = _f("ask_max_mult")
if ask is None:
ask = _f("orderbook_entry_ask_max_mult")
tp = _f("tp_pct")
if tp is None:
tp = _f("take_profit_pct")
tp_max = _f("tp_max_pct")
if tp_max is None:
tp_max = _f("take_profit_max_pct")
sl = _f("sl_pct")
if sl is None:
sl = _f("stop_loss_pct")
whip_sub = _i("whipsaw_subbar_sec")
whip_lb = _i("whipsaw_lookback_sec")
whip_dip = _f("whipsaw_dip_pct")
whip_tol = _f("whipsaw_recovery_tol_pct")
lines: List[str] = []
if ob_on is True:
ob_bits = ["호가ON"]
if spread is not None:
ob_bits.append(f"spr{spread:.1f}")
if ratio is not None:
ob_bits.append(f"r{ratio:.2f}")
if ask is not None:
ob_bits.append(f"ask×{ask:.0f}")
lines.append(" ".join(ob_bits))
elif ob_on is False:
lines.append("호가OFF")
exit_bits: List[str] = []
if tp is not None:
exit_bits.append(f"익절{tp:.1f}%")
if tp_max is not None:
exit_bits.append(f"상한{tp_max:.1f}%")
if sl is not None:
exit_bits.append(f"손절{sl:.1f}%")
if exit_bits:
lines.append(" ".join(exit_bits))
if whip_on is True:
w_bits = ["휩쏘ON"]
if whip_sub is not None:
w_bits.append(f"sub{whip_sub}s")
if whip_lb is not None:
w_bits.append(f"lb{whip_lb}s")
if whip_dip is not None:
# 저장값 0.007 → 화면 0.70% (비율→퍼센트)
dip_pct = whip_dip * 100.0 if whip_dip < 0.5 else whip_dip
w_bits.append(f"dip{dip_pct:.2f}%")
if whip_tol is not None:
tol_pct = whip_tol * 100.0 if whip_tol < 0.5 else whip_tol
w_bits.append(f"tol{tol_pct:.2f}%")
lines.append(" ".join(w_bits))
elif whip_on is False and (ob_on is not None or exit_bits):
lines.append("휩쏘OFF")
return {
"ob_on": ob_on,
"whip_on": whip_on,
"ob_spread": spread,
"ob_ratio": ratio,
"ob_ask": ask,
"tp_pct": tp,
"tp_max_pct": tp_max,
"sl_pct": sl,
"whip_subbar_sec": whip_sub,
"whip_lookback_sec": whip_lb,
"whip_dip_pct": whip_dip,
"whip_recovery_tol_pct": whip_tol,
"ob_summary": " · ".join(lines) if lines else None,
"ob_summary_lines": lines,
}
def _row_metrics(
row: Optional[Dict[str, Any]],
*,
@@ -900,6 +1165,11 @@ def _row_metrics(
out["overfit_verdict_ui"] = of.get("verdict_ui")
except Exception:
pass
try:
prm = row.get("merged_params") or row.get("params") or {}
out.update(_ob_whip_ui_from_params(prm if isinstance(prm, dict) else {}))
except Exception:
pass
return out
@@ -984,6 +1254,10 @@ def _summarize_result_data(
"daily_pnl": daily,
"rank": 1,
}
try:
mode_row.update(_ob_whip_ui_from_params(mc.get("params") or {}))
except Exception:
pass
try:
from kis_trader.backtest.optuna_common import overfit_risk_pct_for_row
of = overfit_risk_pct_for_row(data, mode_row)
@@ -1104,6 +1378,15 @@ def _summarize_result_data(
"top5_learn": top5_learn,
"top5_stable": top5_stable,
"top5_mode": top5_mode,
# 본 TPE 호가축 여부 · 사후8방 생략 안내
"tpe_includes_orderbook": (
"_orderbook_filter_enabled" in list(data.get("grid_keys") or [])
or "max_spread_pct" in list(data.get("grid_keys") or [])
),
"post_run_ob_whipsaw": (
bool(post_topn.get("run_ob_whipsaw"))
if isinstance(post_topn, dict) else None
),
"mode_combo_note": mc.get("note"),
"daily_trail_recommend": (
data.get("daily_trail_recommend")
@@ -1292,10 +1575,14 @@ def apply_optuna_result(
src = str(source or "gated").strip().lower()
rank = max(1, int(rank or 1))
upto_s = str(upto or "base").strip().lower() or "base"
# full/as_scored/trial = Top10「적용」: 그 trial 점수에 쓰인 타점·익절·손절·호가·휩쏘 전부
# (구 base=타점만+호가OFF 후처리 덮어쓰기 — TPE 호가축 ON일 때 호가가 꺼지는 원인)
_FULL_UPTO = frozenset({"full", "as_scored", "trial", "all"})
_allowed = (
"base", "entry", "exit", "stop", "whipsaw", "trail",
"e", "x", "s", "ex", "es", "xs", "exs",
"000", "100", "010", "001", "110", "101", "011", "111",
"full", "as_scored", "trial", "all",
)
# 방별 휩쏘: e+whip / base+whipsaw / 100+whip …
_chk = upto_s
@@ -1305,9 +1592,10 @@ def apply_optuna_result(
break
if _chk not in _allowed:
raise ValueError(
"upto/combo 는 base|e|x|s|ex|es|xs|exs|whipsaw|trail|000~111 "
"(구 entry/exit/stop · +whip/+whipsaw 방별휩쏘 포함) 만"
"upto/combo 는 full|base|e|x|s|ex|es|xs|exs|whipsaw|trail|000~111 "
"(full=줄전체 · 구 entry/exit/stop · +whip 방별휩쏘 포함) 만"
)
_apply_full_trial = upto_s in _FULL_UPTO
item: Optional[Dict[str, Any]] = None
merged: Dict[str, Any] = {}
@@ -1403,7 +1691,12 @@ def apply_optuna_result(
else:
raise ValueError(strat)
if not sym:
if _apply_full_trial:
# trial params 그대로 — 8방 후처리로 호가OFF 덮어쓰지 않음
axis_notes.append(
"upto=full · 본 trial 호가/휩쏘 ON·OFF·수치 유지(사후8방 미적용)"
)
elif not sym:
from kis_trader.backtest.optuna_postprocess_topn import build_upto_env_patch
axis_patch, axis_notes = build_upto_env_patch(
data=data, source=src, rank=rank, upto=upto_s, strategy=strat,
@@ -1449,6 +1742,41 @@ def apply_optuna_result(
pass
apply_target = f"stock_config:{sym}" if (strat == "us_momentum" and sym) else "global"
note = (
"TIME_* 는 session_env_patch 기본 OFF — 운영 시간창 유지"
+ (
""
if upto_s == "trail"
else (
" · 줄전체 적용(타점·익절·손절·호가·휩쏘)"
if _apply_full_trial
else " · 타점/후처리 적용(upto=" + upto_s + ")"
)
)
+ trail_note
+ axis_note
+ " · 웹 폼은 DB로 다시 채움(새로고침 불필요)"
)
study_nm = str(
(meta or {}).get("study_name")
or data.get("optuna_study_name")
or data.get("study_name")
or "",
)
record_optuna_apply_audit(
ok=True,
strategy=strat,
source=src,
rank=rank,
upto=upto_s,
trial=metrics.get("optuna_trial_number"),
job_id=(meta or {}).get("job_id") if meta else job_id,
study_name=study_nm,
result_json=str(path or ""),
params=merged if upto_s != "trail" else None,
metrics=metrics,
note=note,
)
return {
"ok": True,
"strategy": strat,
@@ -1464,13 +1792,7 @@ def apply_optuna_result(
"axis_patch": axis_patch,
"axis_notes": axis_notes,
"reload_forms": True,
"note": (
"TIME_* 는 session_env_patch 기본 OFF — 운영 시간창 유지"
+ ("" if upto_s == "trail" else " · 타점 적용(upto=" + upto_s + ")")
+ trail_note
+ axis_note
+ " · 웹 폼은 DB로 다시 채움(새로고침 불필요)"
),
"note": note,
}
@@ -1791,17 +2113,33 @@ def refresh_job_status(meta: Dict[str, Any]) -> Dict[str, Any]:
pp_log = str((rerun or {}).get("log_path") or "") if str((rerun or {}).get("status") or "") == "running" else ""
post = _parse_postprocess_progress(pp_log or active_log)
topn = ((m.get("result_summary") or {}).get("postprocess_topn") or {}) if isinstance(m.get("result_summary"), dict) else {}
if isinstance(topn, dict) and topn.get("postprocess_by_anchor") and topn.get("run_ob_whipsaw"):
if not alive and str((rerun or {}).get("status") or "") != "running":
post["ready"] = True
post["pct"] = 100.0
from kis_trader.backtest.optuna_postprocess_topn import ob_8way_web_hint
miss8 = ob_8way_web_hint(topn)
post["hint"] = (
("후처리 끝 · 8방 미산출(%s) · 「상세」 가능" % miss8)
if miss8 else "후처리 끝 · 「상세」 가능"
)
post["stage"] = post.get("stage") or "done"
# 본 TPE 호가축 ON → 사후 8방 기본 OFF. 웹「후처리」바는 8방 TPE 전용으로만 길게 보이게.
try:
from kis_trader.backtest.optuna_postprocess_topn import _run_ob_whipsaw_full
expect_ob_post = bool(_run_ob_whipsaw_full())
except Exception:
expect_ob_post = True
post["expect_ob"] = bool(expect_ob_post)
if isinstance(topn, dict) and topn.get("postprocess_by_anchor") is not None:
ran_ob = bool(topn.get("run_ob_whipsaw"))
if ran_ob:
if not alive and str((rerun or {}).get("status") or "") != "running":
post["ready"] = True
post["pct"] = 100.0
from kis_trader.backtest.optuna_postprocess_topn import ob_8way_web_hint
miss8 = ob_8way_web_hint(topn)
post["hint"] = (
("후처리 끝 · 8방 미산출(%s) · 「상세」 가능" % miss8)
if miss8 else "후처리 끝 · 「상세」 가능"
)
post["stage"] = post.get("stage") or "done"
else:
# A: 사후호가방 생략 — 바를「후처리」로 붙잡지 않음
if not alive:
post["ready"] = True
post["pct"] = 100.0
post["stage"] = "skipped_ob"
post["hint"] = "본TPE 호가포함 · 사후호가방 생략 · 「상세」가능"
trials_tot = int(prog.get("trials_total") or 0)
trials_done = int(prog.get("trials_done") or 0)
is_seq = m.get("kind") in ("seq4", "seq")
@@ -1816,14 +2154,21 @@ def refresh_job_status(meta: Dict[str, Any]) -> Dict[str, Any]:
if not post.get("hint"):
post["hint"] = "후처리 재실행 중 · 「상세」는 끝난 뒤"
elif alive and trial_finished and not post.get("ready"):
m["phase"] = "postprocess"
if not post.get("stage"):
post["stage"] = "wait"
post["hint"] = "학습 끝 · 후처리 시작 대기 · 「상세」는 아직"
if expect_ob_post:
m["phase"] = "postprocess"
if not post.get("stage"):
post["stage"] = "wait"
post["hint"] = "학습 끝 · 후처리 시작 대기 · 「상세」는 아직"
else:
# mode_combo·JSON 저장만 — 호가8방 TPE 아님
m["phase"] = "finalize"
post["stage"] = post.get("stage") or "finalize"
if not post.get("hint"):
post["hint"] = "학습 끝 · JSON 저장 중 (사후호가방 생략)"
elif alive:
m["phase"] = "trials"
if not post.get("hint"):
post["hint"] = "학습 trial 중 · 후처리는 그 다음"
post["hint"] = "학습 trial 중 · 후처리는 그 다음" if expect_ob_post else "학습 trial 중 (호가=본TPE)"
else:
m["phase"] = str(m.get("status") or "idle")
if post.get("ready") and not post.get("hint"):
@@ -1926,6 +2271,7 @@ def start_optuna_job(
ob_source: Optional[str] = None,
entry_modes: Optional[Any] = None,
sl_modes: Optional[Any] = None,
ob_modes: Optional[Any] = None,
study_trials: Optional[int] = None,
study_name_override: Optional[str] = None,
) -> Dict[str, Any]:
@@ -1937,7 +2283,8 @@ def start_optuna_job(
universe_history_source: kiwoom|ls (저장 후보 이력 테이블).
candle_source: ''|kis|kiwoom — CANDLE_SOURCE / --candle-source (실매 읽기쌍과 동일).
entry_modes: 꼬리 TPE 고정 진입 align|limit_atr. 둘 다=순차 2스터디(한 스터디에 섞지 않음).
sl_modes: 돌파 TPE 고정 손절 fixed|atr. 둘 다=순차 2스터디.
sl_modes: 돌파 TPE 고정 손절 fixed|atr. 둘 다=순차(호가와 곱).
ob_modes: 돌파 TPE 호가 스터디 스위치 off|on. 손절×호가 최대 4순차(한 스터디에 안 섞음).
"""
_ensure_dirs()
running = find_running_jobs()
@@ -2006,8 +2353,12 @@ def start_optuna_job(
bo_sms = _normalize_breakout_sl_modes(sl_modes)
if "breakout" not in picked:
bo_sms = ["fixed"]
bo_dual = "breakout" in picked and len(bo_sms) >= 2
use_seq = len(picked) >= 2 or tail_dual or bo_dual
bo_oms = _normalize_breakout_ob_modes(ob_modes)
if "breakout" not in picked:
bo_oms = ["off"]
bo_steps = (len(bo_sms) * len(bo_oms)) if "breakout" in picked else 0
bo_multi = bo_steps >= 2
use_seq = len(picked) >= 2 or tail_dual or bo_multi
seq_active = None
if use_seq:
@@ -2035,6 +2386,7 @@ def start_optuna_job(
env["UNIVERSE_HISTORY_SOURCE"] = hist_src
env["TAIL_OPTUNA_ENTRY_MODES"] = " ".join(tail_ems if "tail" in picked else ["align"])
env["BREAKOUT_OPTUNA_SL_MODES"] = " ".join(bo_sms if "breakout" in picked else ["fixed"])
env["BREAKOUT_OPTUNA_OB_MODES"] = " ".join(bo_oms if "breakout" in picked else ["off"])
if candle_source:
env["CANDLE_SOURCE"] = candle_source
if tick_source:
@@ -2046,7 +2398,8 @@ def start_optuna_job(
if "tail" in picked and tail_ems:
_lab = _lab.replace("꼬리", "꼬리(" + "+".join(tail_ems) + ")")
if "breakout" in picked and bo_sms:
_lab = _lab.replace("돌파", "돌파(" + "+".join(bo_sms) + ")")
_bo_bits = [f"{sm}×{om}" for sm in bo_sms for om in bo_oms]
_lab = _lab.replace("돌파", "돌파(" + "+".join(_bo_bits) + ")")
label = "순차(" + _lab + ")"
strat_field = ",".join(picked)
else:
@@ -2066,12 +2419,13 @@ def start_optuna_job(
log_path = ROOT / "logs" / f"optuna_web_{strat}_{_em}_{ts}.log"
label = f"꼬리({_em})"
elif strat == "breakout":
_sm = bo_sms[0]
from kis_trader.backtest.optuna_breakout_tpe_space import breakout_tpe_study_extra
_extra = breakout_tpe_study_extra(bo_sms[0], bo_oms[0])
study_name = (
f"{strat}_{_sm}_{mode}_{start.replace('-', '')}_{end.replace('-', '')}_{ts}"
f"{strat}_{_extra}_{mode}_{start.replace('-', '')}_{end.replace('-', '')}_{ts}"
)
log_path = ROOT / "logs" / f"optuna_web_{strat}_{_sm}_{ts}.log"
label = f"돌파({_sm})"
log_path = ROOT / "logs" / f"optuna_web_{strat}_{_extra}_{ts}.log"
label = f"돌파({_extra})"
else:
study_name = f"{strat}_{mode}_{start.replace('-', '')}_{end.replace('-', '')}_{ts}"
log_path = ROOT / "logs" / f"optuna_web_{strat}_{ts}.log"
@@ -2089,7 +2443,7 @@ def start_optuna_job(
"--min_trades", "1",
"--min_win_rate", "0",
"--min_pf", "0",
"--orderbook-filter", "off",
"--orderbook-filter", (bo_oms[0] if strat == "breakout" else "off"),
"--no-progress",
"--study-name", study_name,
"--sort-by", sort_by,
@@ -2157,6 +2511,7 @@ def start_optuna_job(
"ob_source": ob_source,
"tail_entry_modes": tail_ems if "tail" in picked else None,
"breakout_sl_modes": bo_sms if "breakout" in picked else None,
"breakout_ob_modes": bo_oms if "breakout" in picked else None,
"log_path": str(log_path),
"pid": int(proc.pid),
"status": "running",
@@ -2213,6 +2568,7 @@ def continue_optuna_job(job_id: str) -> Dict[str, Any]:
ob_source=meta.get("ob_source"),
entry_modes=meta.get("tail_entry_modes"),
sl_modes=meta.get("breakout_sl_modes"),
ob_modes=meta.get("breakout_ob_modes"),
study_trials=goal,
study_name_override=name,
)