diff --git a/backtest_web.py b/backtest_web.py index 50eb6ec..3eee1ab 100644 --- a/backtest_web.py +++ b/backtest_web.py @@ -9854,6 +9854,17 @@ def api_optuna_stop(job_id: str): return jsonify({"ok": False, "error": str(e)}), 400 +@app.route("/api/optuna/ob_bias_hints", methods=["GET"]) +def api_optuna_ob_bias_hints(): + """전략별 최근 Optuna → 호가 ON/OFF 몰림 (웹 폼 녹색 표시용).""" + from kis_trader.backtest import optuna_web_jobs as owj + + try: + return jsonify(owj.get_ob_bias_hints()) + except Exception as e: + return jsonify({"ok": False, "error": str(e)}), 500 + + @app.route("/api/optuna/defaults", methods=["GET"]) def api_optuna_defaults(): """날짜 기본값(거래일).""" @@ -9872,6 +9883,7 @@ def api_optuna_defaults(): "mode": "tpe", "strategies": ["momentum", "tail", "breakout", "scalp", "all"], "note": "apply-best 없음. 탐색 게이트 WR/PF=0, 사후 results_gated.", + "period_hint": "전략 1개 「시작」= 1차(넓은 Grid) → 2차(밴드 축소) 자동 연쇄. 기간=위 시작·종료 date.", }) diff --git a/database.py b/database.py index ca4f44d..f488bee 100644 --- a/database.py +++ b/database.py @@ -303,6 +303,11 @@ ENV_CONFIG_KEYS = ( "TAIL_LIMIT_VALID_BARS", "TAIL_LIMIT_FILL_SLIP_PCT", "TAIL_MIN_INVEST_RATIO_OF_SLOT", "TAIL_PARAM_SEARCH_ENTRY_MODE", + "OPTUNA_MODE_POOL", + "OPTUNA_MODE_TOP_N", + "OPTUNA_MODE_BAND_DECAY_IQR", + "OPTUNA_MODE_REFINE_PHASE2_TRIALS", + "OPTUNA_MODE_REFINE_BAND_EXPAND_IQR", "TAIL_GRID_FAST_MAX_DAILY_CHG", "TAIL_GRID_COARSE_MAX_DAILY_CHG", "TAIL_GRID_FAST_SYMBOL_LOSS_PCT", "TAIL_GRID_FAST_SYMBOL_LOSS_KRW", "TAIL_GRID_FAST_REENTRY_MIN_EDGE", @@ -830,6 +835,10 @@ ENV_CONFIG_KEYS = ( "PSBL_RVSECNCL_TR_ID", "PSBL_RVSECNCL_INQR_DVSN_1", "PSBL_RVSECNCL_INQR_DVSN_2", + "CANCELABLE_CCLD_MAX_PAGES", + "SELL_LOCKED_ENQUEUE_COOLDOWN_SEC", + "CANCELABLE_RECONCILE_ENABLED", + "CANCELABLE_RECONCILE_CANCEL_RETRY", # 잔고 연속조회 최대 페이지 (1p=실전50/모의20종목) — 보유 많을 때 누락 방지 "BALANCE_MAX_PAGES", # WebSocket 실시간 가격 캐시 유효기간(초): 이 시간 이상 지나면 REST 재조회 @@ -4519,8 +4528,19 @@ class TradeDB: return saved def _insert_env_auth_row(self, snapshot: Dict[str, Any], created_at: str) -> Optional[int]: - """env_auth_config 1행 INSERT (앱키/시크릿/ID/계좌 전용).""" + """env_auth_config 1행 INSERT (앱키/시크릿/ID/계좌 전용). + + snapshot 에 실제 값이 있는 인증키가 하나도 없으면 빈 행 방지를 위해 INSERT 생략. + """ try: + # 빈 행 방지 가드: 실제 값을 가진 키가 하나도 없으면 저장 생략 + has_any = any( + snapshot.get(k) is not None and str(snapshot.get(k)).strip() != "" + for k in ENV_AUTH_KEYS + ) + if not has_any: + logger.debug("env_auth_config INSERT 생략 — snapshot에 인증키 값 없음 (빈 행 방지)") + return None key_list = ", ".join(f"`{k}`" for k in ENV_AUTH_KEYS) placeholders = ", ".join(["%s"] * (1 + len(ENV_AUTH_KEYS))) vals = [created_at] + [snapshot.get(k, None) for k in ENV_AUTH_KEYS] diff --git a/docs/like_mcp.md/db_erd.md b/docs/like_mcp.md/db_erd.md index 4613e22..ae514d5 100644 --- a/docs/like_mcp.md/db_erd.md +++ b/docs/like_mcp.md/db_erd.md @@ -263,6 +263,10 @@ CREATE TABLE `orders` ( `filled_at` varchar(30) DEFAULT NULL, `raw_json` mediumtext DEFAULT NULL, `is_mock` tinyint(1) DEFAULT NULL, + `broker_open_qty` int(11) DEFAULT NULL, + `broker_open_odno` varchar(30) DEFAULT NULL, + `broker_reconcile_at` varchar(30) DEFAULT NULL, + `broker_reconcile_note` varchar(200) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uq_ord_no_ctx` (`ord_no`,`strategy_id`,`code`,`side`,`ord_date`,`is_mock`), KEY `idx_strategy_date` (`strategy_id`,`ord_date`), @@ -2756,7 +2760,11 @@ CREATE TABLE `config_us_momentum` ( | `INTRADAY_HOLDINGS_DRIFT_AUTO_RECOVER` | `text` | NULL | NULL | 드리프트 자동복구 · OFF=알림만 · ON=active_trades qty 보정(기본 OFF) | | `GHOST_PURGE_ON_RECONCILE` | `text` | NULL | NULL | 고아복구 시 유령잔고 삭제 · ON=브로커 0주인데 active_trades 남은 종목 삭제(수동보호 제외) · Pre/Post EOD 동일 잔고조회에서 처리 | | `INQUIRE_PSBL_RVSECNCL_BEFORE_GHOST` | `text` | NULL | NULL | 유령정리 전 정정취소가능주문조회. 매도가능 0 ≠ 보유 0 | -| `PSBL_RVSECNCL_CACHE_TTL_SEC` | `text` | NULL | NULL | 정정취소가능 조회 캐시(초). 기본 5 | +| `PSBL_RVSECNCL_CACHE_TTL_SEC` | `text` | NULL | NULL | 정정취소가능 조회 캐시(초). 기본 30 | +| `CANCELABLE_CCLD_MAX_PAGES` | `text` | NULL | NULL | 모의 cancelable daily-ccld 페이지 상한. 기본 3 | +| `SELL_LOCKED_ENQUEUE_COOLDOWN_SEC` | `text` | NULL | NULL | sell_locked enqueue 쿨다운(초). 기본 20 | +| `CANCELABLE_RECONCILE_ENABLED` | `text` | NULL | NULL | cancelable_open DB↔브로커 reconcile ON/OFF | +| `CANCELABLE_RECONCILE_CANCEL_RETRY` | `text` | NULL | NULL | reconcile 시 cancel_order 재시도 | | `PSBL_RVSECNCL_MAX_PAGES` | `text` | NULL | NULL | 정정취소가능 연속조회 최대 페이지. 기본 5 | | `WS_GAP_ROLLUP_3M_FROM_1M` | `text` | NULL | NULL | 공통env: WS GAP ROLLUP 3M FROM 1M (WS_GAP_ROLLUP_3M_FROM_1M) | | `WS_GAP_FILL_CANDIDATE_MODE` | `text` | NULL | NULL | 공통env: WS GAP FILL CANDIDATE 모드 (WS_GAP_FILL_CANDIDATE_MODE) | diff --git a/kis_token_manager.py b/kis_token_manager.py index 8d9003e..fd57ad1 100644 --- a/kis_token_manager.py +++ b/kis_token_manager.py @@ -124,7 +124,7 @@ def token_covers_session( return exp_dt >= deadline -def get_token_status(is_mock: bool) -> dict: +def get_token_status(is_mock: bool, current_app_key: str = None) -> dict: """ 캐시 파일 상태 반환. 반환: valid=세션커버(ensure 재사용 기준), usable=만료 전 API 사용 가능 @@ -143,6 +143,18 @@ def get_token_status(is_mock: bool) -> dict: token = cache.get("access_token", "") expired_s = cache.get("access_token_token_expired", "") exp_dt = _parse_expired(expired_s) + app_key_prefix = cache.get("app_key_prefix", "") + + if current_app_key and app_key_prefix: + if not current_app_key.startswith(app_key_prefix): + return { + "valid": False, + "usable": False, + "token": "", + "expires": "앱키변경됨", + "expires_in_h": -999, + } + if not token or exp_dt is None: return { "valid": False, @@ -299,6 +311,7 @@ def _issue_token(app_key: str, app_secret: str, is_mock: bool) -> bool: "access_token_token_expired": exp, "mock": is_mock, "issued_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "app_key_prefix": app_key[:8] if app_key else "", }, ensure_ascii=False, indent=2), encoding="utf-8", ) @@ -321,7 +334,14 @@ def ensure_token(is_mock: bool, env: dict = None) -> bool: 단일 모드(실전/모의) 토큰: 오늘 세션을 덮으면 재사용, 아니면만 발급. 1일 1회 원칙 — 충분하면 tokenP 호출 없음. """ - status = get_token_status(is_mock) + if env is None: + env = _load_env() + + key_suffix = "MOCK" if is_mock else "REAL" + app_key = str(env.get(f"KIS_APP_KEY_{key_suffix}", "") or "").strip() + app_secret = str(env.get(f"KIS_APP_SECRET_{key_suffix}", "") or "").strip() + + status = get_token_status(is_mock, current_app_key=app_key) mode = "모의" if is_mock else "실전" if status["valid"]: @@ -351,7 +371,7 @@ def ensure_token(is_mock: bool, env: dict = None) -> bool: return False try: # 잠금 획득 후 다시 확인 (다른 프로세스가 갱신했을 수 있음) - status = get_token_status(is_mock) + status = get_token_status(is_mock, current_app_key=app_key) if status["valid"]: logger.info(f"🔑 {mode} 토큰 이미 갱신됨 (다른 프로세스) → 재사용") return True @@ -427,6 +447,15 @@ class KisTokenManager: self._lock = threading.Lock() self._token: Optional[str] = None self._expiry: Optional[datetime] = None + self._app_key_prefix: Optional[str] = None + + env = _load_env() + if env: + suffix = "MOCK" if is_mock else "REAL" + key = str(env.get(f"KIS_APP_KEY_{suffix}", "")).strip() + if key: + self._app_key_prefix = key[:8] + self._load_from_file() # 재시작 후에도 기존 토큰 재사용 # ── 내부 ────────────────────────────────────────────────────── @@ -438,6 +467,14 @@ class KisTokenManager: data = json.loads(self._cache_path.read_text(encoding="utf-8")) token = data.get("access_token", "") exp_dt = _parse_expired(data.get("access_token_token_expired", "")) + + # 앱키 변경 감지: 캐시된 app_key_prefix 가 있고, 현재 prefix 와 다르면 무시 + cached_prefix = data.get("app_key_prefix", "") + if self._app_key_prefix and cached_prefix: + if self._app_key_prefix != cached_prefix: + logger.warning("🔑 [%s] 앱키 변경 감지 → 기존 토큰 캐시 폐기", self._mode_str) + return + if token and exp_dt: self._token = token self._expiry = exp_dt diff --git a/kis_trader/backtest/optuna_briefing.py b/kis_trader/backtest/optuna_briefing.py index 7550168..a395c41 100644 --- a/kis_trader/backtest/optuna_briefing.py +++ b/kis_trader/backtest/optuna_briefing.py @@ -136,6 +136,197 @@ def _live_pnl_snapshot(strategy: str, start: str, end: str) -> str: pass +def _params_exit_ob_line(params: Optional[Dict[str, Any]]) -> str: + """trial params → 호가·익절·손절 한 줄 (브리핑용).""" + if not isinstance(params, dict): + return "—" + try: + from kis_trader.backtest.optuna_web_jobs import _ob_whip_ui_from_params + ui = _ob_whip_ui_from_params(params) + return str(ui.get("ob_summary") or "—") + except Exception: + tp = params.get("tp_pct") or params.get("take_profit_pct") + sl = params.get("sl_pct") or params.get("stop_loss_pct") + ob = params.get("ob_filter_enabled") + bits: List[str] = [] + if ob is True: + bits.append("호가ON") + elif ob is False: + bits.append("호가OFF") + if tp is not None: + bits.append(f"익절{float(tp):.1f}%") + if sl is not None: + bits.append(f"손절{float(sl):.1f}%") + return " ".join(bits) if bits else "—" + + +def _trial_metrics_line(row: Optional[Dict[str, Any]], *, tag: str) -> str: + if not isinstance(row, dict): + return f"- **{tag}**: 없음" + tn = row.get("optuna_trial_number") + tn_s = f"trial #{tn}" if tn is not None else "trial 없음(조립)" + pnl = _f(row.get("total_pnl")) + wr = _f(row.get("win_rate")) + pf = _f(row.get("pf")) + nt = _i(row.get("total_trades")) + stab = row.get("stability_score") + stab_s = f" · 안정점수 {float(stab):.0f}" if stab is not None else "" + prox = row.get("consensus_match_pct") + prox_s = "" + if prox is not None: + mn = row.get("consensus_match_n") + mo = row.get("consensus_match_of") + prox_s = f" · 근접 {prox}%" + if mn is not None and mo is not None: + prox_s += f" ({mn}/{mo}축 밴드안)" + prm = row.get("params") or row.get("merged_params") or {} + ob_line = _params_exit_ob_line(prm if isinstance(prm, dict) else {}) + return ( + f"- **{tag}** ({tn_s}): PnL {pnl:,.0f}원 · WR {wr:.1f}% · PF {pf:.2f} · " + f"거래 {nt}건{stab_s}{prox_s} · {ob_line}" + ) + + +def build_final_selection_briefing_lines(data: Dict[str, Any]) -> List[str]: + """ + mode Top10 / mode_combo / gated / stable 기준 최종 선택 후보 (규칙 기반). + 웹 하단 표와 동일 JSON 소스 — apply source 명시. + """ + from kis_trader.backtest.optuna_common import ( + resolve_results_mode_consensus, + resolve_results_stable, + ) + from kis_trader.backtest.optuna_mode_combo import ( + resolve_mode_pool_kind, + select_mode_pool_rows, + _build_mode_band_profile, + _row_param_value, + ) + from kis_trader.backtest.optuna_postprocess_topn import resolve_post_top_n + + lines: List[str] = [] + lines.append("## 최종 선택 후보 (규칙 · DB 적용 전 확인)") + lines.append( + "- 아래는 **역할이 다른** 4종 후보입니다. mode Top10 1위 ≠ 익절 대표 · " + "**DB 1순위는 사후합격(gated)** 입니다." + ) + pool_kind = resolve_mode_pool_kind() + lines.append(f"- mode pool: `{pool_kind}` · scoring: `band_proximity_p25_p75`") + lines.append("") + + top_n = resolve_post_top_n(10) + gated = list(data.get("results_gated") or []) + learn = list(data.get("results") or data.get("results_all") or []) + stable, stable_meta = resolve_results_stable(data, top_n=top_n) + mode_rows, mode_meta = resolve_results_mode_consensus(data, top_n=top_n) + mc = data.get("mode_combo") if isinstance(data.get("mode_combo"), dict) else {} + mc_bt = mc.get("backtest") if isinstance(mc.get("backtest"), dict) else {} + mc_params = mc.get("params") if isinstance(mc.get("params"), dict) else {} + + g0 = gated[0] if gated else None + s0 = stable[0] if stable else None + m0 = mode_rows[0] if mode_rows else None + + lines.append("### 후보 4종") + lines.append(_trial_metrics_line(g0, tag="① 사후합격 1위 (apply: gated)")) + lines.append(_trial_metrics_line(s0, tag="② 안정 1위 (apply: stable)")) + if mc_params or mc_bt: + mode_row = { + "optuna_trial_number": None, + "total_pnl": mc_bt.get("total_pnl"), + "win_rate": mc_bt.get("win_rate"), + "pf": mc_bt.get("pf"), + "total_trades": mc_bt.get("total_trades"), + "stability_score": mc_bt.get("stability_score"), + "params": mc_params, + } + freq_tp = (mc.get("freq") or {}).get("tp_pct") if isinstance(mc.get("freq"), dict) else None + extra = "" + if isinstance(freq_tp, dict) and freq_tp.get("value") is not None: + extra = f" · tp 최빈 {freq_tp.get('value')}% ({freq_tp.get('count')}/{freq_tp.get('of')})" + lines.append(_trial_metrics_line(mode_row, tag="③ mode_combo (apply: mode)") + extra) + else: + lines.append("- **③ mode_combo (apply: mode)**: 없음 (미산출·구 JSON)") + lines.append(_trial_metrics_line(m0, tag="④ mode Top10 1위 (apply: consensus · 밴드 전형 trial)")) + + # tp 밴드 — mode Top10 1위가 tp 밖인지 + if m0 and mode_meta.get("mode_pool_size"): + try: + pool = select_mode_pool_rows(learn, data=data) + keys: List[str] = list(data.get("grid_keys") or []) + if not keys and pool: + keys = list((pool[0].get("params") or {}).keys()) + prof = _build_mode_band_profile(pool, keys) + tp_band = prof.get("tp_pct") or prof.get("take_profit_pct") + if tp_band and tp_band.get("kind") == "numeric": + p25 = float(tp_band["p25"]) + p75 = float(tp_band["p75"]) + prm = m0.get("params") or {} + tp_v = _row_param_value(m0, "tp_pct") or _row_param_value(m0, "take_profit_pct") + if tp_v is not None: + tp_f = float(tp_v) + in_band = p25 <= tp_f <= p75 + lines.append("") + lines.append( + f"- mode pool tp_pct 밴드 p25~p75: **{p25:g}~{p75:g}%** · " + f"Top10 1위 tp={tp_f:g}% → " + f"{'밴드 **안**' if in_band else '밴드 **밖**(다른 축 보정으로 근접% 높음)'}" + ) + except Exception: + pass + + if mode_meta.get("mode_pool_size") is not None: + lines.append( + f"- mode Top10 meta: pool {mode_meta.get('mode_pool_size')}건 · " + f"축 {mode_meta.get('band_axes') or mode_meta.get('mode_params_keys')}개" + ) + + lines.append("") + lines.append("### 추천 (자동 · confirm 필수)") + if g0: + lines.append( + "1. **실매 DB 적용 1순위 → ① 사후합격 1위** (`optunaApply gated`) — " + "WR/PF·min_trades 사후 통과." + ) + if s0 and s0 is not g0: + lines.append( + "2. **변동성·손실일 줄이기 → ② 안정 1위** (`stable`) — " + "PnL보다 일별 안정 우선 시." + ) + else: + lines.append( + "1. **사후합격 0건 → DB 적용 비권장.** 현행 DB 유지 + 유니버스·틱·기간 재검증." + ) + if learn: + lines.append( + "2. 참고만: 학습 1위는 objective 최대일 뿐 사후 게이트 미통과일 수 있음." + ) + if mc_params: + lines.append( + f"{'3' if g0 else '2'}. **2차 narrow·대표 숫자 → ③ mode_combo** — " + "축별 최빈 조립 · trial 번호 없음 · 익절은 pool 최빈값 참고." + ) + if m0: + n = "4" if (g0 and mc_params) else ("3" if (g0 or mc_params) else "2") + lines.append( + f"{n}. **④ mode Top10 1위** — 양수 pool 전형 trial(다축 밴드 근접). " + "**익절 하나로 쓰지 말 것** · 표에서 「보기」→ consensus apply." + ) + + diag = data.get("overfit_diagnostics") if isinstance(data.get("overfit_diagnostics"), dict) else {} + risk = diag.get("overfit_risk_pct") + if risk is not None and float(risk) >= 55 and g0: + lines.append( + f"- ⚠ 과적합 위험 {risk}% — gated 적용 전 **웹백테 동일 기간 1회**·소액 관찰 권장." + ) + if stable_meta.get("fallback_rank_only"): + lines.append( + f"- ⚠ 안정 게이트 0건 → 안정 Top은 **점수순 폴백** ({stable_meta.get('fallback_note', '')})" + ) + lines.append("") + return lines + + def build_rule_briefing(data: Dict[str, Any]) -> str: """규칙 기반 — 이전 장 / 앞으로 장 코멘트.""" strategy = str(data.get("strategy") or "tail").strip().lower() @@ -227,6 +418,8 @@ def build_rule_briefing(data: Dict[str, Any]) -> str: lines.append(f"- _참고: {diag.get('note')}_") lines.append("") + lines.extend(build_final_selection_briefing_lines(data)) + lines.append("## 이전 장에서는") live = _live_pnl_snapshot(strategy, start, end) if live: @@ -306,13 +499,18 @@ def _briefing_prompt(rule_text: str, data: Dict[str, Any]) -> str: "optuna_best_value": data.get("optuna_best_value"), "top_gated": (data.get("results_gated") or [None])[0], "top_learning": (data.get("results") or [None])[0], + "top_stable": (data.get("results_stable") or [None])[0], + "top_mode_consensus": (data.get("results_mode") or [None])[0], + "mode_combo_params": (data.get("mode_combo") or {}).get("params"), "mode_combo_vs_best": (data.get("mode_combo") or {}).get("vs_best"), + "mode_consensus_meta": data.get("mode_consensus_meta"), } return ( "당신은 한국 주식 퀀트 헤지펀드 리스크 매니저입니다. " "아래 Optuna TPE 결과와 규칙 브리핑을 읽고, 초보자도 이해하게 " "「이전 장에서는」/「앞으로 장에서는」 두 절로만 한국어 코멘트를 쓰세요. " "과적합·표본부족·실매↔백테 괴리(고스트퍼지 등)를 분명히 경고하세요. " + "mode Top10 1위를 익절 대표로 단정하지 마세요 — DB 1순위는 사후합격(gated). " "특정 종목 매수 추천·확정 수익 약속 금지. 200~400자.\n\n" f"[규칙 브리핑]\n{rule_text}\n\n" f"[요약 JSON]\n{json.dumps(compact, ensure_ascii=False, default=str)[:6000]}" diff --git a/kis_trader/backtest/optuna_common.py b/kis_trader/backtest/optuna_common.py index 8ae328b..7d0d791 100644 --- a/kis_trader/backtest/optuna_common.py +++ b/kis_trader/backtest/optuna_common.py @@ -124,6 +124,14 @@ def annotate_optuna_period_daily_avg(out_data: Optional[Dict[str, Any]]) -> None out_data["n_trading_days"] = n_days if out_data.get("min_trades_per_day") is None: out_data["min_trades_per_day"] = optuna_min_trades_per_day() + try: + budget = float( + out_data.get("total_budget_krw") + or out_data.get("total_budget") + or 0 + ) + except (TypeError, ValueError): + budget = 0.0 keys = ( "results", "results_all", "results_gated", "results_stable", "results_mode", "mode_combo_results", @@ -141,6 +149,19 @@ def annotate_optuna_period_daily_avg(out_data: Optional[Dict[str, Any]]) -> None pnl = 0.0 r["n_period_trading_days"] = n_days r["period_daily_avg_pnl"] = round(pnl / float(n_days), 2) + if budget > 0: + r["period_daily_avg_pct"] = round( + pnl / budget * 100.0 / float(n_days), 3, + ) + elif r.get("daily_avg_pct") is not None: + r["period_daily_avg_pct"] = r.get("daily_avg_pct") + elif r.get("bot_pct") is not None: + try: + r["period_daily_avg_pct"] = round( + float(r["bot_pct"]) / float(n_days), 3, + ) + except (TypeError, ValueError): + pass def optuna_score_mdd_add() -> float: @@ -689,6 +710,35 @@ def resolve_results_stable( return stable, gates +def resolve_results_mode_consensus( + data: Optional[Dict[str, Any]], + *, + top_n: Optional[int] = None, +) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]: + """JSON results_mode 우선 · 없으면 mode Top10 즉시 재구성 (구 JSON 호환).""" + data = data or {} + try: + n = int(top_n) if top_n is not None else 10 + except (TypeError, ValueError): + n = 10 + n = max(1, n) + stored = [r for r in list(data.get("results_mode") or []) if isinstance(r, dict)] + meta = dict(data.get("mode_consensus_meta") or {}) + if stored: + return stored[:n], meta + allr = list(data.get("results_all") or data.get("results") or []) + from kis_trader.backtest.optuna_mode_combo import build_results_mode_consensus_tier + + rows, built_meta = build_results_mode_consensus_tier( + allr, + top_n=n, + grid_keys=list(data.get("grid_keys") or []), + data=data, + ) + meta.update(built_meta) + return rows, meta + + def build_optuna_result_tiers( rows: List[Dict[str, Any]], *, diff --git a/kis_trader/backtest/optuna_grid_narrow.py b/kis_trader/backtest/optuna_grid_narrow.py new file mode 100644 index 0000000..6b0cf25 --- /dev/null +++ b/kis_trader/backtest/optuna_grid_narrow.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 +""" +kis_trader/backtest/optuna_grid_narrow.py — 1차 Optuna JSON → 2차 TPE 좁힌 그리드 +================================================================================== +1차 trial pool(PnL 양수 등)의 축별 p25~p75 밴드로 Grid choices 를 줄인다. +OPTUNA_GRID_NARROW_JSON 경로가 있으면 optuna_search_space 가 해당 축만 덮어쓴다. +""" +from __future__ import annotations + +import json +import os +from pathlib import Path +from typing import Any, Dict, List, Optional + +from kis_trader.backtest.optuna_mode_combo import ( + _build_mode_band_profile, + _coerce_numeric, + select_mode_pool_rows, +) +from kis_trader.backtest.optuna_search_space import _dedupe_preserve_order +from kis_trader.utils.env import get_env_float, get_env_int + + +def _strategy_original_grid(strategy: str, mode: str) -> Dict[str, List[Any]]: + s = str(strategy or "").strip().lower() + m = str(mode or "tpe").strip().lower() or "tpe" + if s == "breakout": + from kis_trader.backtest.param_search_breakout import _breakout_grids + grids = _breakout_grids() + # TPE 는 categorical grid 없음 — fast 밴드 envelope 로 2차 narrow + grid_mode = m if m in grids else ("fast" if m == "tpe" else m) + return dict(grids.get(grid_mode) or {}) + if s == "momentum": + from kis_trader.backtest.param_search_momentum import _momentum_grids + return dict(_momentum_grids(market="KR").get(m) or {}) + if s == "us_momentum": + from kis_trader.backtest.param_search_momentum import _momentum_grids + return dict(_momentum_grids(market="US").get(m) or {}) + if s == "scalp": + from kis_trader.backtest.param_search_scalping import _scalp_grids + return dict(_scalp_grids().get(m) or {}) + if s == "tail": + from kis_trader.backtest.tail_param_search import _tail_grids + return dict(_tail_grids(m) or {}) + return {} + + +def _narrow_axis_values( + original: List[Any], + band: Dict[str, Any], + *, + min_choices: int = 2, + max_choices: int = 8, +) -> List[Any]: + if not original: + return list(original or []) + min_c = max(1, int(min_choices)) + max_c = max(min_c, int(max_choices)) + if band.get("kind") == "categorical": + mode_v = band.get("mode") + kept = [v for v in original if str(v) == str(mode_v)] + if not kept and mode_v is not None: + kept = [mode_v] + return _dedupe_preserve_order(kept)[:max_c] if kept else _dedupe_preserve_order(list(original))[:max_c] + + expand = max(0.0, float(get_env_float("OPTUNA_MODE_REFINE_BAND_EXPAND_IQR", 0.5))) + nums_map: Dict[float, Any] = {} + for v in original: + n = _coerce_numeric(v) + if n is not None: + nums_map[n] = v + if not nums_map: + mode_v = band.get("mode") + kept = [v for v in original if str(v) == str(mode_v)] + return _dedupe_preserve_order(kept or list(original))[:max_c] + + p25 = float(band["p25"]) + p50 = float(band["p50"]) + p75 = float(band["p75"]) + iqr = float(band["iqr"]) + lo = p25 - expand * iqr + hi = p75 + expand * iqr + kept: List[Any] = [] + for n, raw in sorted(nums_map.items()): + if lo <= n <= hi: + kept.append(raw) + if len(kept) < min_c: + by_dist = sorted(nums_map.items(), key=lambda t: abs(t[0] - p50)) + kept = [raw for _, raw in by_dist[:max_c]] + return _dedupe_preserve_order(kept)[:max_c] + + +def build_narrow_grid_from_optuna_data( + data: Dict[str, Any], + *, + mode: Optional[str] = None, + strategy: Optional[str] = None, +) -> Dict[str, Any]: + """ + 1차 JSON → 2차 TPE용 narrow grid dict + 메타. + + Returns: + {"grid": {axis: [choices]}, "meta": {...}} + """ + strat = str(strategy or data.get("strategy") or "").strip().lower() + m = str(mode or data.get("mode") or "tpe").strip().lower() or "tpe" + grid_keys = list(data.get("grid_keys") or []) + original = _strategy_original_grid(strat, m) + if not original: + return {"grid": {}, "meta": {"error": f"unknown strategy/mode: {strat}/{m}"}} + + rows = list(data.get("results_all") or data.get("results") or []) + pool = select_mode_pool_rows(rows, data=data) + keys = grid_keys or list(original.keys()) + profile = _build_mode_band_profile(pool, keys) + + narrow: Dict[str, List[Any]] = {} + stats: Dict[str, Any] = {} + for k in keys: + orig = list(original.get(k) or []) + if not orig: + continue + band = profile.get(k) + if not band: + narrow[k] = _dedupe_preserve_order(orig) + stats[k] = {"kept": len(narrow[k]), "of": len(orig), "reason": "no_band"} + continue + narrowed = _narrow_axis_values(orig, band) + if len(narrowed) >= 1: + narrow[k] = narrowed + stats[k] = {"kept": len(narrowed), "of": len(orig), "band": band.get("kind")} + else: + narrow[k] = _dedupe_preserve_order(orig) + stats[k] = {"kept": len(narrow[k]), "of": len(orig), "reason": "fallback_full"} + + return { + "grid": narrow, + "meta": { + "strategy": strat, + "mode": m, + "pool_size": len(pool), + "pool_kind": str(data.get("mode_pool_kind") or ""), + "band_axes": len(profile), + "axis_stats": stats, + }, + } + + +def write_narrow_grid_json(path: str, narrow_grid: Dict[str, List[Any]]) -> str: + p = Path(path) + p.parent.mkdir(parents=True, exist_ok=True) + p.write_text(json.dumps(narrow_grid, indent=2, ensure_ascii=False), encoding="utf-8") + return str(p) + + +def build_and_write_narrow_grid( + data: Dict[str, Any], + out_path: str, + *, + mode: Optional[str] = None, + strategy: Optional[str] = None, +) -> Dict[str, Any]: + built = build_narrow_grid_from_optuna_data(data, mode=mode, strategy=strategy) + grid = dict(built.get("grid") or {}) + write_narrow_grid_json(out_path, grid) + meta = dict(built.get("meta") or {}) + meta["path"] = str(out_path) + meta["n_axes"] = len(grid) + return meta + + +def load_narrow_grid_override() -> Dict[str, List[Any]]: + """OPTUNA_GRID_NARROW_JSON 파일 → axis choices (없으면 {}).""" + raw = str(os.environ.get("OPTUNA_GRID_NARROW_JSON") or "").strip() + if not raw or not Path(raw).is_file(): + return {} + try: + obj = json.loads(Path(raw).read_text(encoding="utf-8")) + except Exception: + return {} + if not isinstance(obj, dict): + return {} + out: Dict[str, List[Any]] = {} + for k, v in obj.items(): + if isinstance(v, list) and v: + out[str(k)] = list(v) + return out + + +def resolve_refine_phase2_trials(phase1_trials: int) -> int: + """2차 trial 수 — env OPTUNA_MODE_REFINE_PHASE2_TRIALS (0=1차와 동일).""" + env_v = int(get_env_int("OPTUNA_MODE_REFINE_PHASE2_TRIALS", 0)) + if env_v > 0: + return max(1, min(2000, env_v)) + return max(1, min(2000, int(phase1_trials or 200))) diff --git a/kis_trader/backtest/optuna_mode_combo.py b/kis_trader/backtest/optuna_mode_combo.py index 8b48a60..585fb40 100644 --- a/kis_trader/backtest/optuna_mode_combo.py +++ b/kis_trader/backtest/optuna_mode_combo.py @@ -18,7 +18,7 @@ import logging from collections import Counter from typing import Any, Callable, Dict, List, Optional -from kis_trader.utils.env import get_env_int +from kis_trader.utils.env import get_env_float, get_env_from_db, get_env_int from kis_trader.backtest.optuna_tpe_common import finalize_ratchet_combo logger = logging.getLogger("optuna_mode_combo") @@ -32,29 +32,51 @@ def resolve_mode_top_n(default: int = 20) -> int: return max(1, n) -def mode_combo_from_results( - results: List[Dict[str, Any]], - *, - top_n: int = 20, - grid_keys: Optional[List[str]] = None, - params_key: str = "params", -) -> Dict[str, Any]: +def resolve_mode_pool_kind() -> str: """ - Top-N(PnL) 축별 최빈 → mode_combo + 빈도 메타. + mode_combo / mode Top10 / 2차 그리드 밴드 풀. + positive(기본)=PnL>0 전체 · gated=results_gated · top_n=Top-N PnL. + """ + raw = str(get_env_from_db("OPTUNA_MODE_POOL", "positive") or "positive").strip().lower() + if raw in ("positive", "gated", "top_n"): + return raw + return "positive" - Returns: - { - "top_n": int, - "pool_size": int, - "params": {축: 최빈값}, - "freq": {축: {"value": ..., "count": n, "of": pool}}, - "top_pnls": [...], - } - """ - rows = [ + +def _valid_pnl_rows(results: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + return [ r for r in (results or []) if r.get("total_pnl") is not None and abs(float(r.get("total_pnl") or 0)) < 1e15 ] + + +def select_mode_pool_rows( + results: List[Dict[str, Any]], + *, + data: Optional[Dict[str, Any]] = None, + top_n: Optional[int] = None, +) -> List[Dict[str, Any]]: + """mode_combo·밴드·2차 narrow 공통 trial 풀.""" + kind = resolve_mode_pool_kind() + n = int(top_n) if top_n is not None else resolve_mode_top_n(20) + rows = _valid_pnl_rows(results) + if kind == "gated" and data: + gated = [r for r in list(data.get("results_gated") or []) if isinstance(r, dict)] + gated = _valid_pnl_rows(gated) + if gated: + return gated + if kind == "positive": + pos = [r for r in rows if float(r.get("total_pnl") or 0) > 0] + if pos: + return pos + rows.sort( + key=lambda r: ( + -float(r.get("total_pnl") or 0), + -float(r.get("win_rate") or 0), + -int(r.get("total_trades") or 0), + ) + ) + return rows[: max(1, n)] rows.sort( key=lambda r: ( -float(r.get("total_pnl") or 0), @@ -62,11 +84,40 @@ def mode_combo_from_results( -int(r.get("total_trades") or 0), ) ) - pool = rows[: max(1, int(top_n))] + return rows[: max(1, n)] + + +def mode_combo_from_results( + results: List[Dict[str, Any]], + *, + top_n: int = 20, + grid_keys: Optional[List[str]] = None, + params_key: str = "params", + data: Optional[Dict[str, Any]] = None, + pool_rows: Optional[List[Dict[str, Any]]] = None, +) -> Dict[str, Any]: + """ + 풀(PnL 양수 전체 등) 축별 최빈 → mode_combo + 빈도 메타. + + Returns: + { + "top_n": int, + "pool_size": int, + "pool_kind": str, + "params": {축: 최빈값}, + "freq": {축: {"value": ..., "count": n, "of": pool}}, + "top_pnls": [...], + } + """ + pool_kind = resolve_mode_pool_kind() + pool = list(pool_rows) if pool_rows is not None else select_mode_pool_rows( + results, data=data, top_n=top_n, + ) if not pool: return { "top_n": int(top_n), "pool_size": 0, + "pool_kind": pool_kind, "params": {}, "freq": {}, "top_pnls": [], @@ -105,6 +156,7 @@ def mode_combo_from_results( return { "top_n": int(top_n), "pool_size": len(pool), + "pool_kind": pool_kind, "params": params, "freq": freq, "top_pnls": [float(r.get("total_pnl") or 0) for r in pool[:10]], @@ -230,6 +282,7 @@ def enrich_out_data_with_mode_combo( top_n=n, grid_keys=keys or None, params_key=params_key, + data=out_data, ) # 래칫 숫자축 최빈 → 엔진용 ratchet_tiers 재조립 (불일치 방지) strat = str(out_data.get("strategy") or "").strip().lower() @@ -239,9 +292,10 @@ def enrich_out_data_with_mode_combo( off_token=off_tok, ) report: Dict[str, Any] = { - "method": "top_n_per_axis_mode", + "method": "pool_per_axis_mode", "top_n": mode_meta["top_n"], "pool_size": mode_meta["pool_size"], + "pool_kind": mode_meta.get("pool_kind") or resolve_mode_pool_kind(), "params": mode_params, "freq": mode_meta["freq"], "top_pnls": mode_meta["top_pnls"], @@ -258,8 +312,8 @@ def enrich_out_data_with_mode_combo( best_tr = int(res0.get("total_trades") or 0) lg.info( - "📊 [mode] Top-%d 최빈 추출 | pool=%d | top_pnls=%s", - mode_meta["top_n"], + "📊 [mode] pool(%s) 최빈 추출 | pool=%d | top_pnls=%s", + mode_meta.get("pool_kind") or resolve_mode_pool_kind(), mode_meta["pool_size"], mode_meta["top_pnls"][:5], ) @@ -376,4 +430,257 @@ def enrich_out_data_with_mode_combo( except Exception as exc2: lg.warning("⚠️ daily_trail_recommend 폴백 실패: %s", exc2) + # mode Top10 — 웹 표·apply source=consensus (구 JSON은 웹에서 재계산) + try: + from kis_trader.backtest.optuna_postprocess_topn import resolve_post_top_n + + ui_n = resolve_post_top_n(10) + mode_rows, mode_meta = build_results_mode_consensus_tier( + list(out_data.get("results_all") or out_data.get("results") or []), + top_n=ui_n, + grid_keys=keys or None, + params_key=params_key, + data=out_data, + ) + out_data["results_mode"] = mode_rows + out_data["mode_consensus_meta"] = mode_meta + except Exception as exc: + lg.warning("⚠️ results_mode(mode Top10) 첨부 실패: %s", exc) + return out_data + + +def _percentile_sorted(sorted_vals: List[float], p: float) -> float: + if not sorted_vals: + return 0.0 + if len(sorted_vals) == 1: + return sorted_vals[0] + idx = (len(sorted_vals) - 1) * p + lo = int(idx) + hi = min(lo + 1, len(sorted_vals) - 1) + w = idx - lo + return sorted_vals[lo] * (1.0 - w) + sorted_vals[hi] * w + + +def _row_param_value(row: Dict[str, Any], key: str, *, params_key: str = "params") -> Any: + params = row.get(params_key) or row.get("merged_params") or {} + if not isinstance(params, dict): + params = {} + v = params.get(key) + if v is None and params_key != "merged_params": + v = (row.get("merged_params") or {}).get(key) + return v + + +def _coerce_numeric(v: Any) -> Optional[float]: + if isinstance(v, bool): + return 1.0 if v else 0.0 + try: + return float(v) + except (TypeError, ValueError): + return None + + +def _build_mode_band_profile( + pool: List[Dict[str, Any]], + keys: List[str], + *, + params_key: str = "params", +) -> Dict[str, Dict[str, Any]]: + """ + Top pool 각 축 — 숫자면 p25~p75 밴드(흔한 구간), 아니면 categorical mode. + """ + profile: Dict[str, Dict[str, Any]] = {} + for k in keys: + raw_vals: List[Any] = [] + for row in pool: + v = _row_param_value(row, k, params_key=params_key) + if v is not None: + raw_vals.append(v) + if not raw_vals: + continue + nums: List[float] = [] + all_numeric = True + for v in raw_vals: + n = _coerce_numeric(v) + if n is None: + all_numeric = False + break + nums.append(n) + if all_numeric and nums: + s = sorted(nums) + p25 = _percentile_sorted(s, 0.25) + p50 = _percentile_sorted(s, 0.50) + p75 = _percentile_sorted(s, 0.75) + iqr = max(p75 - p25, abs(p50) * 0.05, 1e-9) + profile[k] = { + "kind": "numeric", + "p25": p25, + "p50": p50, + "p75": p75, + "iqr": iqr, + } + else: + c: Counter = Counter(str(v) for v in raw_vals) + mode_s, _cnt = c.most_common(1)[0] + sample = next(v for v in raw_vals if str(v) == mode_s) + profile[k] = {"kind": "categorical", "mode": sample} + return profile + + +def _trial_band_proximity( + row: Dict[str, Any], + profile: Dict[str, Dict[str, Any]], + *, + params_key: str = "params", +) -> Dict[str, Any]: + """ + trial ↔ pool 흔한 구간(p25~p75) 근접도. 100%=모든 축이 밴드 안 또는 매우 가까움. + (구: 축값 완전 일치 개수 — tp 22% vs 4% 뒤섞임 원인) + """ + decay_iqr = max(0.1, float(get_env_float("OPTUNA_MODE_BAND_DECAY_IQR", 1.5))) + scores: List[float] = [] + in_band = 0 + total = 0 + err_sum = 0.0 + for k, band in profile.items(): + rv = _row_param_value(row, k, params_key=params_key) + if rv is None: + continue + total += 1 + if band.get("kind") == "numeric": + nv = _coerce_numeric(rv) + if nv is None: + mode_v = band.get("mode") + if mode_v is not None: + axis_s = 1.0 if str(rv) == str(mode_v) else 0.0 + else: + axis_s = 0.0 + if axis_s >= 1.0: + in_band += 1 + else: + err_sum += 1.0 + scores.append(axis_s) + continue + p25 = float(band["p25"]) + p50 = float(band["p50"]) + p75 = float(band["p75"]) + iqr = float(band["iqr"]) + if p25 <= nv <= p75: + axis_s = 1.0 + in_band += 1 + err_sum += 0.0 + else: + dist = (p25 - nv) if nv < p25 else (nv - p75) + axis_s = max(0.0, 1.0 - dist / (iqr * decay_iqr)) + err_sum += dist / iqr + else: + mode_v = band.get("mode") + axis_s = 1.0 if str(rv) == str(mode_v) else 0.0 + if axis_s >= 1.0: + in_band += 1 + err_sum += 0.0 if axis_s >= 1.0 else 1.0 + scores.append(axis_s) + pct = (sum(scores) / float(len(scores)) * 100.0) if scores else 0.0 + mean_err = (err_sum / float(total)) if total else 0.0 + return { + "matched": in_band, + "total": total, + "pct": round(pct, 1), + "mean_band_err": round(mean_err, 4), + } + + +def _trial_consensus_match( + row: Dict[str, Any], + mode_params: Dict[str, Any], + *, + params_key: str = "params", +) -> Dict[str, Any]: + """trial params 가 mode(축별 최빈) 와 몇 축 일치하는지.""" + params = row.get(params_key) or row.get("merged_params") or {} + if not isinstance(params, dict): + params = {} + matched = 0 + total = 0 + for k, mv in (mode_params or {}).items(): + rv = params.get(k) + if rv is None and params_key != "merged_params": + rv = (row.get("merged_params") or {}).get(k) + if rv is None: + continue + total += 1 + if str(rv) == str(mv): + matched += 1 + pct = (float(matched) / float(total) * 100.0) if total else 0.0 + return {"matched": matched, "total": total, "pct": round(pct, 1)} + + +def build_results_mode_consensus_tier( + results: List[Dict[str, Any]], + *, + top_n: int = 10, + mode_top_n: Optional[int] = None, + grid_keys: Optional[List[str]] = None, + params_key: str = "params", + data: Optional[Dict[str, Any]] = None, +) -> tuple[List[Dict[str, Any]], Dict[str, Any]]: + """ + mode Top10 — 풀(PnL 양수 전체 등) p25~p75 밴드에 **가장 가까운** trial 순. + + 밴드는 pool 전체에서 계산 · 후보는 pool 전 trial(양수 전체)에서 근접도 순. + mode_combo(1회 실측·trial 없음)와 달리 **실제 trial 번호**가 있음. + """ + mode_n = int(mode_top_n) if mode_top_n is not None else resolve_mode_top_n(20) + pool_kind = resolve_mode_pool_kind() + learn_pool = select_mode_pool_rows(list(results or []), data=data, top_n=mode_n) + mode_meta = mode_combo_from_results( + list(results or []), + top_n=mode_n, + grid_keys=grid_keys, + params_key=params_key, + pool_rows=learn_pool, + ) + meta: Dict[str, Any] = { + "mode_top_n": mode_n, + "mode_pool_size": int(mode_meta.get("pool_size") or 0), + "mode_pool_kind": pool_kind, + "mode_params_keys": len(mode_meta.get("params") or {}), + "scoring": "band_proximity_p25_p75", + "note": f"pool={pool_kind} · 축별 p25~p75 밴드 근접도(오차↓) · OPTUNA_MODE_POOL", + } + if not mode_meta.get("pool_size"): + meta["note"] = "mode pool 없음 — results·grid_keys 확인" + return [], meta + + candidate_pool = list(learn_pool) + keys: List[str] = [] + if grid_keys: + keys = [k for k in grid_keys if k] + if not keys: + seen: set = set() + for r in learn_pool: + for k in (r.get(params_key) or {}).keys(): + if k not in seen: + seen.add(k) + keys.append(k) + profile = _build_mode_band_profile(learn_pool, keys, params_key=params_key) + meta["band_axes"] = len(profile) + scored: List[Dict[str, Any]] = [] + for r in candidate_pool: + m = _trial_band_proximity(r, profile, params_key=params_key) + row = dict(r) + row["consensus_match_pct"] = m["pct"] + row["consensus_match_n"] = m["matched"] + row["consensus_match_of"] = m["total"] + row["consensus_band_err"] = m.get("mean_band_err") + scored.append(row) + scored.sort( + key=lambda r: ( + -float(r.get("consensus_match_pct") or 0), + float(r.get("consensus_band_err") or 999.0), + -float(r.get("total_pnl") or 0), + -float(r.get("score") or 0), + ) + ) + return scored[: max(1, int(top_n))], meta diff --git a/kis_trader/backtest/optuna_mode_refine_runner.py b/kis_trader/backtest/optuna_mode_refine_runner.py new file mode 100644 index 0000000..5081ee1 --- /dev/null +++ b/kis_trader/backtest/optuna_mode_refine_runner.py @@ -0,0 +1,442 @@ +#!/usr/bin/env python3 +""" +kis_trader/backtest/optuna_mode_refine_runner.py — 1·2차 TPE 연쇄 (기간=폼 start/end) +===================================================================================== +1차: 넓은 Grid TPE → JSON +2차: 1차 JSON 밴드로 Grid 축소 + 동일 기간 TPE + +웹: start_optuna_job(단일) · run_optuna_4strat_tpe_seq.sh(순차) → subprocess 본 스크립트. +""" +from __future__ import annotations + +import argparse +import json +import logging +import os +import subprocess +import sys +import time +from datetime import datetime +from pathlib import Path +from typing import Any, Dict, List, Optional + +ROOT = Path(__file__).resolve().parents[2] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + +logger = logging.getLogger("optuna_mode_refine") + + +def _py() -> str: + cand = ROOT / ".venv" / "bin" / "python3" + if cand.is_file(): + return str(cand) + return sys.executable + + +def _read_json(path: str) -> Dict[str, Any]: + return json.loads(Path(path).read_text(encoding="utf-8")) + + +def _load_phase1_data( + *, + strategy: str, + phase1_json: Optional[str] = None, + phase1_study: Optional[str] = None, +) -> tuple: + """1차 narrow 입력. DB(study) 우선, 없으면 JSON 파일. (data, source_label).""" + study = str(phase1_study or "").strip() + if study: + from kis_trader.backtest.optuna_study_store import load_payload_dict, payload_has_rows + + data = load_payload_dict(study) + if data and payload_has_rows(data): + logger.info("📌 1차 payload DB study=%s", study) + return data, f"db:{study}" + logger.warning("⚠️ DB payload 없음 study=%s — JSON 폴백 시도", study) + path = str(phase1_json or "").strip() + if path and Path(path).is_file(): + return _read_json(path), path + if study: + found = _find_json_by_study(study, strategy) + if found and Path(found).is_file(): + return _read_json(found), found + raise FileNotFoundError( + f"1차 payload 없음 (study={study or '-'}, json={path or '-'})" + ) + + +def _persist_phase1_to_db(data: Dict[str, Any], *, job_id: str, study_name: str) -> None: + """1차 JSON → optuna_study_result (다른 PC 2차용).""" + try: + from kis_trader.backtest.optuna_study_store import ingest_out_data + + out = ingest_out_data(data, job_id=job_id, replace=False) + logger.info("📌 1차 DB 저장 study=%s ok=%s", study_name, out.get("ok")) + except Exception as exc: + logger.warning("⚠️ 1차 DB 저장 실패 study=%s: %s", study_name, exc) + + +def _write_state(path: Path, state: Dict[str, Any]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(state, indent=2, ensure_ascii=False), encoding="utf-8") + + +def _find_json_by_study(study_name: str, strategy: str) -> Optional[str]: + from kis_trader.backtest.tail_param_search import _results_dir_for_write + + out_dir = Path(_results_dir_for_write()) + if not out_dir.is_dir(): + return None + cands: List[Path] = [] + patterns = [ + f"optuna_{strategy}_*.json", + f"optuna_{strategy[:4]}_*.json", + "optuna_*.json", + ] + seen: set = set() + for pat in patterns: + for p in out_dir.glob(pat): + if str(p) in seen: + continue + seen.add(str(p)) + try: + d = json.loads(p.read_text(encoding="utf-8")) + except Exception: + continue + if str(d.get("optuna_study_name") or "") == study_name: + cands.append(p) + if not cands: + return None + cands.sort(key=lambda p: p.stat().st_mtime, reverse=True) + return str(cands[0]) + + +def _build_optuna_cmd( + *, + strategy: str, + mode: str, + start: str, + end: str, + trials: int, + study_name: str, + sort_by: str, + min_trades: int, + hist_src: str, + entry_mode: Optional[str] = None, + sl_mode: Optional[str] = None, + ob_mode: Optional[str] = None, + symbol: Optional[str] = None, + candle_source: Optional[str] = None, + tick_source: Optional[str] = None, + ob_source: Optional[str] = None, + study_trials: Optional[int] = None, +) -> List[str]: + cmd = [ + _py(), "-u", + str(ROOT / "kis_trader" / "backtest" / "param_search_optuna.py"), + "--strategy", strategy, + "--mode", mode, + "--start", start, + "--end", end, + "--trials", str(int(trials)), + "--min_trades", str(int(min_trades)), + "--min_win_rate", "0", + "--min_pf", "0", + "--no-progress", + "--study-name", study_name, + "--sort-by", sort_by, + "--universe-history-source", hist_src, + ] + if strategy == "tail" and entry_mode: + cmd.extend(["--entry-mode", entry_mode]) + if strategy == "breakout": + cmd.extend(["--sl-mode", sl_mode or "fixed"]) + cmd.extend(["--orderbook-filter", ob_mode or "off"]) + if symbol and strategy == "us_momentum": + cmd.extend(["--symbol", symbol]) + if candle_source: + cmd.extend(["--candle-source", candle_source]) + if tick_source: + cmd.extend(["--tick-source", tick_source]) + if ob_source: + cmd.extend(["--ob-source", ob_source]) + if study_trials and int(study_trials) > 0: + cmd.extend(["--study-trials", str(int(study_trials))]) + return cmd + + +def _run_phase( + cmd: List[str], + env: Dict[str, str], + log_path: Path, +) -> int: + log_path.parent.mkdir(parents=True, exist_ok=True) + with open(log_path, "w", encoding="utf-8") as log_f: + log_f.write("CMD: %s\n\n" % " ".join(cmd)) + log_f.flush() + proc = subprocess.Popen( + cmd, + cwd=str(ROOT), + env=env, + stdout=log_f, + stderr=subprocess.STDOUT, + ) + return int(proc.wait()) + + +def run_mode_refine( + *, + job_id: str, + strategy: str, + mode: str, + start: str, + end: str, + trials: int, + sort_by: str, + min_trades: int, + hist_src: str, + entry_mode: Optional[str] = None, + sl_mode: Optional[str] = None, + ob_mode: Optional[str] = None, + symbol: Optional[str] = None, + candle_source: Optional[str] = None, + tick_source: Optional[str] = None, + ob_source: Optional[str] = None, + study_trials: Optional[int] = None, + skip_phase1: bool = False, + phase1_json: Optional[str] = None, + phase1_study: Optional[str] = None, +) -> Dict[str, Any]: + from kis_trader.backtest.optuna_grid_narrow import ( + build_and_write_narrow_grid, + resolve_refine_phase2_trials, + ) + from kis_trader.backtest.optuna_mode_combo import resolve_mode_pool_kind + + ts = datetime.now().strftime("%Y%m%d_%H%M%S") + state_path = ROOT / "logs" / f"{job_id}_refine_state.json" + narrow_path = ROOT / "logs" / f"{job_id}_narrow_grid.json" + state: Dict[str, Any] = { + "job_id": job_id, + "strategy": strategy, + "mode": mode, + "start": start, + "end": end, + "phase": "phase1", + "pool_kind": resolve_mode_pool_kind(), + } + _write_state(state_path, state) + + env = os.environ.copy() + env["PYTHONUNBUFFERED"] = "1" + env["PYTHONPATH"] = str(ROOT) + (os.pathsep + env.get("PYTHONPATH", "")) + env["BACKTEST_UNIVERSE_HISTORY_SOURCE"] = hist_src + env["PARAM_SEARCH_OPTUNA_REPORT_MIN_TRADES"] = str(min_trades) + env["SORT_BY"] = sort_by + env["OPTUNA_WEB_JOB_ID"] = str(job_id) + env.pop("OPTUNA_GRID_NARROW_JSON", None) + + p1_study_arg = str(phase1_study or "").strip() + p1_study = f"{strategy}_{mode}_refine1_{start.replace('-', '')}_{end.replace('-', '')}_{ts}" + p1_log = ROOT / "logs" / f"optuna_refine1_{job_id}.log" + p1_json = str(phase1_json or "").strip() + + if skip_phase1 and (p1_study_arg or (p1_json and Path(p1_json).is_file())): + logger.info("⏭ 1차 스킵 — study=%s json=%s", p1_study_arg or "-", p1_json or "-") + data, src = _load_phase1_data( + strategy=strategy, + phase1_json=p1_json or None, + phase1_study=p1_study_arg or None, + ) + p1_study = str( + p1_study_arg + or data.get("optuna_study_name") + or data.get("study_name") + or p1_study + ).strip() + state["phase1_study"] = p1_study + state["phase1_json"] = p1_json or src + state["phase1_source"] = src + state["phase1_skipped"] = True + else: + cmd1 = _build_optuna_cmd( + strategy=strategy, + mode=mode, + start=start, + end=end, + trials=trials, + study_name=p1_study, + sort_by=sort_by, + min_trades=min_trades, + hist_src=hist_src, + entry_mode=entry_mode, + sl_mode=sl_mode, + ob_mode=ob_mode, + symbol=symbol, + candle_source=candle_source, + tick_source=tick_source, + ob_source=ob_source, + study_trials=study_trials, + ) + state["phase1_study"] = p1_study + state["phase1_log"] = str(p1_log) + state["phase"] = "phase1" + _write_state(state_path, state) + logger.info("▶ 1차 TPE 시작 study=%s %s~%s trials=%s", p1_study, start, end, trials) + rc1 = _run_phase(cmd1, env, p1_log) + state["phase1_exit"] = rc1 + state["phase1_log"] = str(p1_log) + state["phase1_study"] = p1_study + _write_state(state_path, state) + if rc1 != 0: + state["phase"] = "error" + state["error"] = f"phase1 exit {rc1}" + _write_state(state_path, state) + return state + p1_json = _find_json_by_study(p1_study, strategy) or "" + if not p1_json or not Path(p1_json).is_file(): + state["phase"] = "error" + state["error"] = "phase1 JSON not found" + _write_state(state_path, state) + return state + state["phase1_json"] = p1_json + state["phase1_study"] = p1_study + print(f"OPTUNA_PHASE1_STUDY={p1_study}", flush=True) + try: + _persist_phase1_to_db(_read_json(p1_json), job_id=job_id, study_name=p1_study) + except Exception: + pass + + try: + data = _load_phase1_data( + strategy=strategy, + phase1_json=p1_json or None, + phase1_study=str(state.get("phase1_study") or p1_study_arg or "").strip() or None, + )[0] + except FileNotFoundError as exc: + state["phase"] = "error" + state["error"] = str(exc) + _write_state(state_path, state) + return state + + if state.get("phase1_skipped"): + p1_study = str(state.get("phase1_study") or p1_study).strip() + state["phase1_study"] = p1_study + data["mode_pool_kind"] = resolve_mode_pool_kind() + narrow_meta = build_and_write_narrow_grid( + data, str(narrow_path), mode=mode, strategy=strategy, + ) + state["narrow_grid"] = narrow_meta + state["narrow_path"] = str(narrow_path) + + p2_trials = resolve_refine_phase2_trials(trials) + p2_study = f"{strategy}_{mode}_refine2_{start.replace('-', '')}_{end.replace('-', '')}_{ts}" + p2_log = ROOT / "logs" / f"optuna_refine2_{job_id}.log" + env2 = dict(env) + env2["OPTUNA_GRID_NARROW_JSON"] = str(narrow_path) + + cmd2 = _build_optuna_cmd( + strategy=strategy, + mode=mode, + start=start, + end=end, + trials=p2_trials, + study_name=p2_study, + sort_by=sort_by, + min_trades=min_trades, + hist_src=hist_src, + entry_mode=entry_mode, + sl_mode=sl_mode, + ob_mode=ob_mode, + symbol=symbol, + candle_source=candle_source, + tick_source=tick_source, + ob_source=ob_source, + study_trials=study_trials, + ) + logger.info( + "▶ 2차 TPE(밴드축소) study=%s trials=%s narrow=%s", + p2_study, p2_trials, narrow_path, + ) + state["phase"] = "phase2" + state["phase2_study"] = p2_study + state["phase2_log"] = str(p2_log) + state["phase2_trials"] = p2_trials + _write_state(state_path, state) + rc2 = _run_phase(cmd2, env2, p2_log) + state["phase2_exit"] = rc2 + state["phase2_log"] = str(p2_log) + state["phase2_study"] = p2_study + state["phase2_trials"] = p2_trials + p2_json = _find_json_by_study(p2_study, strategy) or "" + if p2_json: + state["phase2_json"] = p2_json + state["result_json"] = p2_json + state["phase"] = "done" if rc2 == 0 else "error" + if rc2 != 0: + state["error"] = f"phase2 exit {rc2}" + state["finished_at"] = datetime.now().isoformat(timespec="seconds") + _write_state(state_path, state) + return state + + +def main() -> int: + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(levelname)s %(message)s", + ) + ap = argparse.ArgumentParser(description="Optuna 1·2차 TPE 연쇄 (밴드 축소 2차)") + ap.add_argument("--job-id", required=True) + ap.add_argument("--strategy", required=True) + ap.add_argument("--mode", default="tpe") + ap.add_argument("--start", required=True) + ap.add_argument("--end", required=True) + ap.add_argument("--trials", type=int, default=200) + ap.add_argument("--sort-by", default="score") + ap.add_argument("--min-trades", type=int, default=1) + ap.add_argument("--universe-history-source", default="kiwoom") + ap.add_argument("--entry-mode", default="") + ap.add_argument("--sl-mode", default="fixed") + ap.add_argument("--ob-mode", default="off") + ap.add_argument("--symbol", default="") + ap.add_argument("--candle-source", default="") + ap.add_argument("--tick-source", default="") + ap.add_argument("--ob-source", default="") + ap.add_argument("--study-trials", type=int, default=0) + ap.add_argument("--skip-phase1", action="store_true") + ap.add_argument("--phase1-json", default="") + ap.add_argument("--phase1-study", default="", help="1차 study — MariaDB payload (다른 PC 2차)") + args = ap.parse_args() + + st = run_mode_refine( + job_id=str(args.job_id), + strategy=str(args.strategy).strip().lower(), + mode=str(args.mode).strip().lower() or "tpe", + start=str(args.start).strip(), + end=str(args.end).strip(), + trials=max(1, int(args.trials)), + sort_by=str(args.sort_by or "score"), + min_trades=max(1, int(args.min_trades)), + hist_src=str(args.universe_history_source or "kiwoom").strip().lower(), + entry_mode=str(args.entry_mode or "").strip() or None, + sl_mode=str(args.sl_mode or "fixed").strip() or "fixed", + ob_mode=str(args.ob_mode or "off").strip() or "off", + symbol=str(args.symbol or "").strip().upper() or None, + candle_source=str(args.candle_source or "").strip().lower() or None, + tick_source=str(args.tick_source or "").strip().lower() or None, + ob_source=str(args.ob_source or "").strip().lower() or None, + study_trials=int(args.study_trials) if int(args.study_trials or 0) > 0 else None, + skip_phase1=bool(args.skip_phase1), + phase1_json=str(args.phase1_json or "").strip() or None, + phase1_study=str(args.phase1_study or "").strip() or None, + ) + if st.get("phase") == "done": + logger.info("✅ 1·2차 완료 result=%s", st.get("result_json")) + return 0 + logger.error("❌ 1·2차 실패: %s", st.get("error")) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/kis_trader/backtest/optuna_postprocess_topn.py b/kis_trader/backtest/optuna_postprocess_topn.py index edd38a1..8ab8536 100644 --- a/kis_trader/backtest/optuna_postprocess_topn.py +++ b/kis_trader/backtest/optuna_postprocess_topn.py @@ -785,7 +785,7 @@ def append_learn_postprocess_anchors( (data or {}).get("results") or (data or {}).get("results_all") or [] - )[: max(1, int(top_n or 5))] + )[: max(1, int(top_n or 10))] if not learn: return lg.info( @@ -856,7 +856,7 @@ def append_stable_postprocess_anchors( if any(str(a.get("role") or "") == "stable" for a in anchors): return from kis_trader.backtest.optuna_common import resolve_results_stable - stable, _meta = resolve_results_stable(data, top_n=max(1, int(top_n or 5))) + stable, _meta = resolve_results_stable(data, top_n=max(1, int(top_n or 10))) # 후처리 중 JSON에 비어 있으면 재구성분 반영 (다음 요약·앵커 일치) if not list((data or {}).get("results_stable") or []) and stable: data["results_stable"] = list(stable) @@ -976,7 +976,7 @@ def attach_topn_postprocess( stable_preview: List[Any] = [] if _include_stable(): from kis_trader.backtest.optuna_common import resolve_results_stable - stable_preview, _sg = resolve_results_stable(data, top_n=max(1, int(top_n or 5))) + stable_preview, _sg = resolve_results_stable(data, top_n=max(1, int(top_n or 10))) if not list((data or {}).get("results_stable") or []) and stable_preview: data["results_stable"] = list(stable_preview) if _sg: @@ -985,7 +985,7 @@ def attach_topn_postprocess( if not gated: learn_preview = list( (data or {}).get("results") or (data or {}).get("results_all") or [] - )[: max(1, int(top_n or 5))] + )[: max(1, int(top_n or 10))] n_units = len(gated) + len(learn_preview) + (len(stable_preview) if _include_stable() else 0) if _include_mode(): n_units += 1 diff --git a/kis_trader/backtest/optuna_search_space.py b/kis_trader/backtest/optuna_search_space.py index 2f963d5..b4fdd2d 100644 --- a/kis_trader/backtest/optuna_search_space.py +++ b/kis_trader/backtest/optuna_search_space.py @@ -32,11 +32,17 @@ def _dedupe_preserve_order(values: List[Any]) -> List[Any]: def _suggest_from_grid(trial: optuna.Trial, grid: Dict[str, List[Any]]) -> Dict[str, Any]: + from kis_trader.backtest.optuna_grid_narrow import load_narrow_grid_override + + narrow = load_narrow_grid_override() combo: Dict[str, Any] = {} for key, values in grid.items(): if not values: continue - choices = _dedupe_preserve_order(list(values)) + src = narrow.get(key) if narrow.get(key) else values + choices = _dedupe_preserve_order(list(src)) + if not choices: + continue combo[key] = trial.suggest_categorical(key, choices) return combo diff --git a/kis_trader/backtest/optuna_study_store.py b/kis_trader/backtest/optuna_study_store.py index fa7a684..d44dfa1 100644 --- a/kis_trader/backtest/optuna_study_store.py +++ b/kis_trader/backtest/optuna_study_store.py @@ -298,6 +298,46 @@ def load_payload_dict(study_name: str) -> Optional[Dict[str, Any]]: return None +def load_phase1_study_for_job(job_id: str) -> Optional[str]: + """step job_id → 1차 study_name (MariaDB). refine1 우선.""" + jid = str(job_id or "").strip()[:64] + if not jid: + return None + try: + ensure_optuna_study_result_table() + cur = _db().conn.execute( + "SELECT study_name FROM optuna_study_result " + "WHERE job_id=%s AND study_name LIKE %s " + "ORDER BY updated_at DESC LIMIT 1", + (jid, "%refine1%"), + ) + row = cur.fetchone() + if row: + name = str(row.get("study_name") or "").strip() + if name: + return name + cur = _db().conn.execute( + "SELECT study_name FROM optuna_study_result " + "WHERE job_id=%s ORDER BY updated_at DESC LIMIT 1", + (jid,), + ) + row = cur.fetchone() + if row: + return str(row.get("study_name") or "").strip() or None + except Exception as exc: + logger.warning("⚠️ load_phase1_study_for_job 실패 job=%s: %s", jid, exc) + return None + + +def phase1_payload_ready(study_name: str) -> bool: + """1차 Top10 narrow 입력용 payload 가 DB에 있는지.""" + name = str(study_name or "").strip() + if not name: + return False + data = load_payload_dict(name) + return payload_has_rows(data) + + def upsert_counts( *, study_name: str, diff --git a/kis_trader/backtest/optuna_web_jobs.py b/kis_trader/backtest/optuna_web_jobs.py index 7aa4876..70b8dbc 100644 --- a/kis_trader/backtest/optuna_web_jobs.py +++ b/kis_trader/backtest/optuna_web_jobs.py @@ -20,7 +20,7 @@ import threading import time from datetime import datetime from pathlib import Path -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Tuple ROOT = Path(__file__).resolve().parents[2] JOBS_DIR = ROOT / "logs" / "optuna_web_jobs" @@ -122,6 +122,31 @@ def _result_study_name(meta: Optional[Dict[str, Any]]) -> str: def _attach_study_result_flags(m: Dict[str, Any]) -> None: + kind = str(m.get("kind") or "") + st = str(m.get("status") or "").strip().lower() + rs = m.get("refine_state") if isinstance(m.get("refine_state"), dict) else {} + prog = m.get("progress") if isinstance(m.get("progress"), dict) else {} + refine_phase = str(rs.get("phase") or prog.get("refine_phase") or "").strip().lower() + # 1·2차 refine 진행 중 — 1차 study 「목표 도달」을 상태에 붙이지 않음 + if kind == "mode_refine" and st == "running" and refine_phase in ("phase1", "phase2"): + done = prog.get("trials_done") + tot = prog.get("trials_total") or m.get("trials") + tag = "1차" if refine_phase == "phase1" else "2차" + if done is not None and tot: + m["leftover_note"] = f"{tag} TPE 진행 {done}/{tot}" + else: + m["leftover_note"] = f"{tag} TPE 진행 중" + m["can_continue"] = False + m["can_confirm"] = False + if refine_phase == "phase2": + try: + st_goal = int(m.get("study_trials") or 0) + except (TypeError, ValueError): + st_goal = 0 + if st_goal > 0 and done is not None: + m["n_complete"] = int(done) + m["study_trials"] = st_goal + return name = _result_study_name(m) if not name: m["can_continue"] = False @@ -139,7 +164,7 @@ def _attach_study_result_flags(m: Dict[str, Any]) -> None: m["leftover_note"] = fl.get("leftover_note") or "" st = str(m.get("status") or "").strip().lower() leftover_ok = st == "done" and int(m.get("leftover_trials") or 0) > 0 - seq = str(m.get("kind") or "") in ("seq", "seq4") + seq = str(m.get("kind") or "") in ("seq", "seq4", "mode_refine") m["can_continue"] = leftover_ok and not seq m["can_confirm"] = leftover_ok and not seq @@ -337,6 +362,34 @@ def _job_sort_ts(meta: Dict[str, Any], sort: str = "started") -> float: return st +def filter_redundant_refine1_jobs(jobs: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + """2차 refine import 가 있으면 동기간 1차 import 는 목록에서 숨김 (중간산출).""" + refine2_keys: set = set() + for j in jobs: + sn = str(j.get("study_name") or "") + if "refine2" in sn: + refine2_keys.add(( + str(j.get("strategy") or ""), + str(j.get("start") or "")[:10], + str(j.get("end") or "")[:10], + )) + if not refine2_keys: + return jobs + out: List[Dict[str, Any]] = [] + for j in jobs: + sn = str(j.get("study_name") or "") + if "refine1" in sn and str(j.get("kind") or "") == "import": + key = ( + str(j.get("strategy") or ""), + str(j.get("start") or "")[:10], + str(j.get("end") or "")[:10], + ) + if key in refine2_keys: + continue + out.append(j) + return out + + def list_jobs(limit: int = 30, sort: str = "started") -> List[Dict[str, Any]]: _ensure_dirs() out: List[Dict[str, Any]] = [] @@ -346,6 +399,7 @@ def list_jobs(limit: int = 30, sort: str = "started") -> List[Dict[str, Any]]: except Exception: continue out.sort(key=lambda m: _job_sort_ts(m, sort), reverse=True) + out = filter_redundant_refine1_jobs(out) return out[: max(1, int(limit))] @@ -353,6 +407,11 @@ def job_list_row(meta: Dict[str, Any]) -> Dict[str, Any]: """잡 목록 테이블용 슬림 행 — 후처리 JSON·브리핑·로그테일 제외.""" m = dict(meta or {}) _attach_study_result_flags(m) + if not isinstance(m.get("period_info"), dict): + try: + m["period_info"] = _build_period_info(m) + except Exception: + pass prog = m.get("progress") if isinstance(m.get("progress"), dict) else {} post = m.get("postprocess") if isinstance(m.get("postprocess"), dict) else {} return { @@ -378,15 +437,28 @@ def job_list_row(meta: Dict[str, Any]) -> Dict[str, Any]: "trials_done": prog.get("trials_done"), "trials_total": prog.get("trials_total"), "pct": prog.get("pct"), + "refine_phase": prog.get("refine_phase"), + "label": prog.get("label"), }, "postprocess": { "pct": post.get("pct"), "ready": post.get("ready"), }, "join_cmd": m.get("join_cmd") or "", + "join_cmd_ps": m.get("join_cmd_ps") or "", "web_cmd": m.get("web_cmd") or m.get("cmd") or "", + "web_cmd_full": m.get("web_cmd_full") or "", "join_study": m.get("join_study") or m.get("active_study_name") or m.get("study_name") or "", "join_hint": m.get("join_hint") or "", + "join_cmds_all": m.get("join_cmds_all") or [], + "seq_refine_cmds": m.get("seq_refine_cmds") or [], + "period_info": m.get("period_info") if isinstance(m.get("period_info"), dict) else None, + "kind": m.get("kind"), + "study_name": m.get("study_name") or m.get("active_study_name") or "", + "study_short": m.get("study_short") or _study_short_note( + str(m.get("study_name") or m.get("active_study_name") or "") + ), + "source": m.get("source"), } @@ -666,7 +738,7 @@ def _read_seq_active_sidecar(path: Optional[str]) -> Dict[str, str]: k, _, v = line.partition("=") k = k.strip().lower() v = v.strip() - if k in ("strategy", "study", "entry_mode", "sl_mode", "extra"): + if k in ("strategy", "study", "entry_mode", "sl_mode", "ob_mode", "extra", "refine_state_path"): out[k] = v except Exception: return {} @@ -838,6 +910,414 @@ def _ps_join_script(argv: List[str]) -> str: ) +def _refine_runner_argv( + meta: Dict[str, Any], + *, + strategy: str, + job_id: str, + entry_mode: Optional[str] = None, + sl_mode: Optional[str] = None, + ob_mode: Optional[str] = None, + skip_phase1: bool = False, + phase1_json: Optional[str] = None, + phase1_study: Optional[str] = None, + py_bin: str = ".venv/bin/python", +) -> List[str]: + """다른 PC — optuna_mode_refine_runner.py (1·2차 연쇄). 상대경로.""" + from kis_trader.backtest.optuna_common import ( + normalize_optuna_sort_by, + resolve_optuna_min_trades, + ) + + strat = str(strategy or "").strip().lower() + mode = str(meta.get("mode") or "tpe").strip() or "tpe" + start = str(meta.get("start") or "") + end = str(meta.get("end") or "") + trials = str(int(meta.get("trials") or 200)) + hist = str(meta.get("universe_history_source") or "kiwoom").strip() or "kiwoom" + sort_by = normalize_optuna_sort_by(meta.get("sort_by") or "score", web=True) + _mt = resolve_optuna_min_trades(start, end, strat) + argv = [ + py_bin, "-u", "kis_trader/backtest/optuna_mode_refine_runner.py", + "--job-id", str(job_id), + "--strategy", strat, + "--mode", mode, + "--start", start, + "--end", end, + "--trials", trials, + "--sort-by", sort_by, + "--min-trades", str(int(_mt["min_trades"])), + "--universe-history-source", hist, + ] + try: + st_goal = int(meta.get("study_trials") or 0) + except (TypeError, ValueError): + st_goal = 0 + if st_goal > 0: + argv.extend(["--study-trials", str(st_goal)]) + if strat == "tail" and entry_mode: + argv.extend(["--entry-mode", str(entry_mode)]) + if strat == "breakout": + argv.extend([ + "--sl-mode", str(sl_mode or "fixed"), + "--ob-mode", str(ob_mode or "off"), + ]) + sym = str(meta.get("symbol") or "").strip().upper() + if sym and strat == "us_momentum": + argv.extend(["--symbol", sym]) + for flag, key in ( + ("--candle-source", "candle_source"), + ("--tick-source", "tick_source"), + ("--ob-source", "ob_source"), + ): + val = str(meta.get(key) or "").strip() + if val: + argv.extend([flag, val]) + if skip_phase1: + if phase1_study: + argv.extend(["--skip-phase1", "--phase1-study", str(phase1_study)]) + elif phase1_json: + argv.extend(["--skip-phase1", "--phase1-json", str(phase1_json)]) + return argv + + +def _seq_env_export_lines(meta: Dict[str, Any]) -> List[str]: + """순차 bash 재현용 env (레포 루트 기준).""" + 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() and s.strip() not in ("seq", "all")] + 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"]) + pairs: List[Tuple[str, str]] = [ + ("START", str(meta.get("start") or "")), + ("END", str(meta.get("end") or "")), + ("TRIALS", str(int(meta.get("trials") or 200))), + ("MODE", str(meta.get("mode") or "tpe")), + ("SORT_BY", str(meta.get("sort_by") or "score")), + ("UNIVERSE_HISTORY_SOURCE", str(meta.get("universe_history_source") or "kiwoom")), + ("STRATEGIES", " ".join(picked)), + ("TAIL_OPTUNA_ENTRY_MODES", " ".join(tail_ems)), + ("BREAKOUT_OPTUNA_SL_MODES", " ".join(bo_sms)), + ("BREAKOUT_OPTUNA_OB_MODES", " ".join(bo_oms)), + ] + try: + st = int(meta.get("study_trials") or 0) + except (TypeError, ValueError): + st = 0 + if st > 0: + pairs.append(("STUDY_TRIALS", str(st))) + jid = str(meta.get("job_id") or "").strip() + if jid: + pairs.append(("OPTUNA_SEQ_JOB_ID", jid)) + for ek, mk in ( + ("CANDLE_SOURCE", "candle_source"), + ("TICK_SOURCE", "tick_source"), + ("OB_SOURCE", "ob_source"), + ): + v = str(meta.get(mk) or "").strip() + if v: + pairs.append((ek, v)) + lines: List[str] = [] + for k, v in pairs: + if v: + lines.append(f"export {k}={shlex.quote(v)}") + return lines + + +def _build_web_cmd_full(meta: Dict[str, Any]) -> str: + kind = str(meta.get("kind") or "") + if kind == "mode_refine": + strat = str(meta.get("strategy") or "").split(",")[0].strip().lower() + jid = str(meta.get("job_id") or "manual_refine") + tail_ems = list(meta.get("tail_entry_modes") or []) + bo_sms = list(meta.get("breakout_sl_modes") or ["fixed"]) + bo_oms = list(meta.get("breakout_ob_modes") or ["off"]) + em = tail_ems[0] if strat == "tail" and tail_ems else None + sm = bo_sms[0] if strat == "breakout" else None + om = bo_oms[0] if strat == "breakout" else None + argv = _refine_runner_argv( + meta, strategy=strat, job_id=jid, + entry_mode=em, sl_mode=sm, ob_mode=om, + py_bin=".venv/bin/python", + ) + return "# 레포 루트 (1·2차 단일)\n" + _quote_cmd(argv) + if kind not in ("seq", "seq4"): + web_argv = meta.get("cmd_argv") + if isinstance(web_argv, list) and web_argv: + return "# 레포 루트\n" + _quote_cmd([str(x) for x in web_argv]) + return str(meta.get("cmd") or "").strip() + env_lines = _seq_env_export_lines(meta) + body = "\n".join(env_lines) + "\nbash scripts/run_optuna_4strat_tpe_seq.sh" + return "# 레포 루트 (순차 1·2차 전체 — 이 VM과 동일 설정)\n" + body + + +def _parse_seq_phase1_studies_from_log(log_path: str) -> List[str]: + """마스터/refine 로그 순서대로 OPTUNA_PHASE1_STUDY= 수집.""" + if not log_path or not Path(log_path).is_file(): + return [] + try: + data = Path(log_path).read_bytes() + if len(data) > 800_000: + data = data[-800_000:] + text = data.decode("utf-8", errors="replace") + except Exception: + return [] + out: List[str] = [] + seen: set = set() + for m in re.finditer(r"OPTUNA_PHASE1_STUDY=(\S+)", text): + sy = str(m.group(1) or "").strip() + if sy and sy not in seen: + seen.add(sy) + out.append(sy) + return out + + +def _parse_seq_refine_states_from_log(log_path: str) -> List[Dict[str, Any]]: + """마스터 로그 STATE= 경로 순서 → refine_state 내용.""" + if not log_path or not Path(log_path).is_file(): + return [] + try: + data = Path(log_path).read_bytes() + if len(data) > 800_000: + data = data[-800_000:] + text = data.decode("utf-8", errors="replace") + except Exception: + return [] + out: List[Dict[str, Any]] = [] + seen: set = set() + for m in re.finditer(r"STATE=(\S+refine_state\.json)", text): + sp = str(m.group(1) or "").strip() + if not sp or sp in seen: + continue + seen.add(sp) + p = Path(sp) + if not p.is_file(): + p = ROOT / sp + row: Dict[str, Any] = {"state_path": str(p)} + if p.is_file(): + try: + st = json.loads(p.read_text(encoding="utf-8")) + if isinstance(st, dict): + row.update(st) + except Exception: + pass + out.append(row) + return out + + +def _resolve_step_phase1( + meta: Dict[str, Any], + *, + step_job_id: str, + step_i: int, + state_row: Optional[Dict[str, Any]] = None, + log_phase1_studies: Optional[List[str]] = None, + log_result_jsons: Optional[List[str]] = None, +) -> Dict[str, Any]: + """1차 study/JSON — DB·state·로그·job_id 조회.""" + from kis_trader.backtest.optuna_study_store import ( + load_phase1_study_for_job, + phase1_payload_ready, + ) + + st = state_row if isinstance(state_row, dict) else {} + p1_study = str(st.get("phase1_study") or "").strip() + p1_json = str(st.get("phase1_json") or "").strip() + idx = max(0, int(step_i) - 1) + log_studies = list(log_phase1_studies or []) + log_jsons = list(log_result_jsons or []) + if not p1_study and idx < len(log_studies): + p1_study = str(log_studies[idx] or "").strip() + if not p1_study and p1_json: + p1_study = str(_phase1_study_from_result_json(p1_json) or "").strip() + if not p1_study: + p1_study = str(load_phase1_study_for_job(step_job_id) or "").strip() + if not p1_json and idx < len(log_jsons): + p1_json = str(log_jsons[idx] or "").strip() + saved = list(meta.get("seq_refine_steps") or []) + if not p1_study and idx < len(saved): + p1_study = str((saved[idx] or {}).get("phase1_study") or "").strip() + db_ok = bool(p1_study and phase1_payload_ready(p1_study)) + file_ok = bool(p1_json and Path(p1_json).is_file()) + return { + "phase1_study": p1_study or None, + "phase1_json": p1_json or None, + "phase1_db": db_ok, + "done": db_ok or file_ok, + } + + +def _parse_seq_result_jsons_from_log(log_path: str) -> List[str]: + if not log_path or not Path(log_path).is_file(): + return [] + try: + data = Path(log_path).read_bytes() + if len(data) > 800_000: + data = data[-800_000:] + text = data.decode("utf-8", errors="replace") + except Exception: + return [] + out: List[str] = [] + for m in re.finditer(r"OPTUNA_RESULT_JSON=(\S+)", text): + p = str(m.group(1) or "").strip() + if not p: + continue + if out and out[-1] == p: + continue + out.append(p) + return out + + +def _phase1_study_from_result_json(json_path: str) -> Optional[str]: + """2차 결과 JSON → 1차 study_name (refine2→refine1 치환).""" + path = str(json_path or "").strip() + if not path or not Path(path).is_file(): + return None + try: + data = json.loads(Path(path).read_text(encoding="utf-8")) + except Exception: + return None + if not isinstance(data, dict): + return None + sn = str(data.get("optuna_study_name") or data.get("study_name") or "").strip() + if not sn: + return None + if "refine2" in sn: + return sn.replace("refine2", "refine1", 1) + if "refine1" in sn: + return sn + return None + + +def _step_refine_params(meta: Dict[str, Any], row: Dict[str, str]) -> Tuple[str, Optional[str], Optional[str], Optional[str]]: + strat = str(row.get("strategy") or "").strip().lower() + extra = str(row.get("extra") or "").strip() + em = sm = om = None + if strat == "tail": + em = extra or "align" + elif strat == "breakout": + 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") + return strat, em, sm, om + + +def _build_seq_refine_cmds(meta: Dict[str, Any]) -> List[Dict[str, Any]]: + """순차·단일 1·2차 — PC별 병렬용 refine runner 명령 목록.""" + kind = str(meta.get("kind") or "") + catalog = _seq_step_catalog(meta) + if kind == "mode_refine" and not catalog: + strat = str(meta.get("strategy") or "").split(",")[0].strip().lower() + if strat: + catalog = [{"strategy": strat, "extra": ""}] + if strat == "tail": + ems = list(meta.get("tail_entry_modes") or ["align"]) + catalog = [{"strategy": "tail", "extra": str(ems[0] if ems else "align")}] + elif strat == "breakout": + from kis_trader.backtest.optuna_breakout_tpe_space import breakout_tpe_study_extra + sms = list(meta.get("breakout_sl_modes") or ["fixed"]) + oms = list(meta.get("breakout_ob_modes") or ["off"]) + catalog = [{ + "strategy": "breakout", + "extra": breakout_tpe_study_extra(sms[0], oms[0]), + }] + if not catalog: + return [] + base_jid = str(meta.get("job_id") or "manual").replace(" ", "_") + log_path = str(meta.get("log_path") or "") + done_jsons = _parse_seq_result_jsons_from_log(log_path) + log_p1_studies = _parse_seq_phase1_studies_from_log(log_path) + state_rows = _parse_seq_refine_states_from_log(log_path) + rows: List[Dict[str, Any]] = [] + for i, row in enumerate(catalog, start=1): + strat, em, sm, om = _step_refine_params(meta, row) + extra = str(row.get("extra") or "") + lab_parts = [strat] + if extra: + lab_parts.append(extra) + label = "/".join(lab_parts) + slug = re.sub(r"[^a-z0-9_]+", "_", f"{strat}_{extra or 'base'}").strip("_")[:32] + step_jid = f"{base_jid}_{slug}_{i}" + state_row = state_rows[i - 1] if i - 1 < len(state_rows) else None + p1 = _resolve_step_phase1( + meta, + step_job_id=step_jid, + step_i=i, + state_row=state_row, + log_phase1_studies=log_p1_studies, + log_result_jsons=done_jsons, + ) + p1_study = str(p1.get("phase1_study") or "").strip() + p1_json = str(p1.get("phase1_json") or "").strip() + p1_db = bool(p1.get("phase1_db")) + p1_done = bool(p1.get("done")) + argv_full = _refine_runner_argv( + meta, strategy=strat, job_id=step_jid, + entry_mode=em, sl_mode=sm, ob_mode=om, + py_bin=".venv/bin/python", + ) + argv_ps = _refine_runner_argv( + meta, strategy=strat, job_id=step_jid, + entry_mode=em, sl_mode=sm, ob_mode=om, + py_bin="python", + ) + phase2_note = "" + cmd_p2 = "" + cmd_p2_ps = "" + if p1_study and p1_db: + phase2_note = f"1차 DB OK · --phase1-study {p1_study}" + elif p1_study: + phase2_note = f"1차 study · --phase1-study {p1_study} (DB payload 확인)" + elif p1_json and Path(p1_json).is_file(): + phase2_note = f"1차 JSON(로컬): {p1_json}" + else: + phase2_note = ( + "1차 완료 후 --phase1-study (MariaDB payload · DB_HOST=141 공유)" + ) + p1_study = "PHASE1_STUDY_NAME_HERE" + argv_p2 = _refine_runner_argv( + meta, strategy=strat, job_id=step_jid + "_p2only", + entry_mode=em, sl_mode=sm, ob_mode=om, + skip_phase1=True, + phase1_study=p1_study if p1_study else None, + phase1_json=p1_json if (not p1_study and p1_json) else None, + py_bin=".venv/bin/python", + ) + argv_p2_ps = _refine_runner_argv( + meta, strategy=strat, job_id=step_jid + "_p2only", + entry_mode=em, sl_mode=sm, ob_mode=om, + skip_phase1=True, + phase1_study=p1_study if p1_study else None, + phase1_json=p1_json if (not p1_study and p1_json) else None, + py_bin="python", + ) + cmd_p2 = _quote_cmd(argv_p2) + cmd_p2_ps = _ps_join_script(argv_p2_ps) + rows.append({ + "step": i, + "label": label, + "strategy": strat, + "extra": extra, + "job_id": step_jid, + "cmd": _quote_cmd(argv_full), + "cmd_ps": _ps_join_script(argv_ps), + "cmd_phase2": cmd_p2, + "cmd_phase2_ps": cmd_p2_ps, + "phase1_study": p1_study or None, + "phase1_json": p1_json or None, + "phase1_db": p1_db, + "phase2_note": phase2_note, + "done": p1_done, + }) + return rows + + def build_optuna_join_payload(meta: Dict[str, Any]) -> Dict[str, Any]: """웹 실행 명령 + 다른 PC에서 같은 study 에 붙는 python 명령.""" m = meta or {} @@ -864,15 +1344,15 @@ def build_optuna_join_payload(meta: Dict[str, Any]) -> Dict[str, Any]: web_cmd = _quote_cmd([str(x) for x in web_argv]) else: web_cmd = str(m.get("cmd") or "").strip() + web_cmd_full = _build_web_cmd_full(m) + seq_refine_cmds = _build_seq_refine_cmds(m) hints = [ - "레포 루트 · 웹과 같은 git 커밋 · MariaDB 141/kis_optuna.", - "PowerShell: .\\.venv\\Scripts\\python.exe 사용. --trials=이 PC 추가분(남은 횟수), --study-trials=스터디 총 목표.", - "목표가 이미 찼으면 --trials 0 (추가 연타 없음). 자잘한 VM 다수보다 Win PC 1대가 현실적.", + "레포 루트 · git 커밋 동일 · MariaDB(kis_optuna) 공유 필수.", + "병렬: 「전략별 1·2차」를 PC마다 1줄씩 — RAM 분산. study 이름은 runner가 PC마다 새로 붙임(각 PC 독립 1→2).", + "같은 study에 trial 추가: 아래 join(param_search). Optuna trial은 DB에 쌓임.", + "2차만: --phase1-study (MariaDB payload_json). 파일 scp 불필요 · DB_HOST=141.", + "순차 전체 재실행: web_cmd_full (env 포함 bash).", ] - if is_seq: - hints.append( - "순차 웹 bash 를 그대로 돌리면 새 study. 아래 python 만 복사." - ) join_cmd = "" join_cmd_ps = "" if strat and study: @@ -881,21 +1361,43 @@ def build_optuna_join_payload(meta: Dict[str, Any]) -> Dict[str, Any]: py_bin=".venv/bin/python", ) argv_ps = _join_argv_for_study( - m, strategy=strat, study=study, extra=extra, py_bin="python" + m, strategy=strat, study=study, extra=extra, py_bin="python", ) join_cmd = _quote_cmd(argv_sh) join_cmd_ps = _ps_join_script(argv_ps) - elif is_seq: - hints.append("현재 study 가 없으면 첫 전략 START 후 다시 여세요.") + elif is_seq or kind == "mode_refine": + hints.append("현재 study 없음 — 첫 스텝 START 후 join 갱신.") join_all: List[Dict[str, str]] = [] + refine_state = m.get("refine_state") if isinstance(m.get("refine_state"), dict) else {} + for sk, label in ( + ("phase1_study", "1차"), + ("phase2_study", "2차"), + ): + sy = str(refine_state.get(sk) or "").strip() + if not sy or not strat: + continue + a_sh = _join_argv_for_study( + m, strategy=strat, study=sy, extra=extra, + py_bin=".venv/bin/python", + ) + a_ps = _join_argv_for_study( + m, strategy=strat, study=sy, extra=extra, py_bin="python", + ) + join_all.append({ + "strategy": strat, + "extra": extra or label, + "study": sy, + "cmd": _quote_cmd(a_sh), + "cmd_ps": _ps_join_script(a_ps), + }) log_path = str(m.get("log_path") or "") - if is_seq and log_path: + if is_seq and log_path and not join_all: try: data = Path(log_path).read_bytes() if len(data) > 400_000: data = data[-400_000:] text = data.decode("utf-8", errors="replace") - seen = set() + seen: set = set() for hit in _SEQ_START_RE.finditer(text): st = str(hit.group("strat") or "").strip().lower() ex = str(hit.group("extra") or "").strip() @@ -908,7 +1410,7 @@ def build_optuna_join_payload(meta: Dict[str, Any]) -> Dict[str, Any]: py_bin=".venv/bin/python", ) a_ps = _join_argv_for_study( - m, strategy=st, study=sy, extra=ex, py_bin="python" + m, strategy=st, study=sy, extra=ex, py_bin="python", ) join_all.append({ "strategy": st, @@ -921,11 +1423,13 @@ def build_optuna_join_payload(meta: Dict[str, Any]) -> Dict[str, Any]: join_all = [] return { "web_cmd": web_cmd, + "web_cmd_full": web_cmd_full, "join_cmd": join_cmd, "join_cmd_ps": join_cmd_ps, "join_hint": "\n".join(hints), "join_study": study, "join_cmds_all": join_all, + "seq_refine_cmds": seq_refine_cmds, } @@ -945,6 +1449,423 @@ def _apply_seq_active(m: Dict[str, Any], info: Dict[str, str]) -> None: m["active_study_name"] = study +def _parse_study_name_from_log(log_path: str) -> str: + """refine/param 로그 CMD 줄에서 --study-name 추출.""" + p = Path(str(log_path or "")) + if not p.is_file(): + return "" + try: + head = p.read_text(encoding="utf-8", errors="replace")[:4000] + except Exception: + return "" + m = re.search(r"--study-name\s+(\S+)", head) + return str(m.group(1) or "").strip() if m else "" + + +def _resolve_refine_active_study(st: Dict[str, Any]) -> str: + """refine_state → 현재 단계 study (phase2_study 없으면 derive).""" + phase = str(st.get("phase") or "").strip().lower() + p1 = str(st.get("phase1_study") or "").strip() + p2 = str(st.get("phase2_study") or "").strip() + if phase == "phase2": + if p2: + return p2 + if "refine1" in p1: + return p1.replace("refine1", "refine2", 1) + p2_log = str(st.get("phase2_log") or "") + if p2_log: + sy = _parse_study_name_from_log(p2_log) + if sy: + return sy + return p1 + + +def _trial_to_result_row(trial: Any) -> Optional[Dict[str, Any]]: + """Optuna trial → results 행 (mode pool·밴드 근접용).""" + ua = dict(getattr(trial, "user_attrs", None) or {}) + if ua.get("total_pnl") is None: + return None + raw = ua.get("merged_json") or ua.get("params_json") or "{}" + try: + merged = json.loads(str(raw)) + except Exception: + merged = dict(getattr(trial, "params", None) or {}) + if not isinstance(merged, dict): + merged = dict(getattr(trial, "params", None) or {}) + params = dict(getattr(trial, "params", None) or {}) + try: + from kis_trader.backtest.optuna_common import optuna_score_fields_from_trial + score_fields = optuna_score_fields_from_trial(trial) + except Exception: + score_fields = {} + row: Dict[str, Any] = { + "params": params, + "merged_params": merged, + "total_trades": ua.get("total_trades"), + "win_rate": ua.get("win_rate"), + "total_pnl": ua.get("total_pnl"), + "pf": ua.get("pf"), + "mdd": ua.get("mdd"), + "optuna_trial_number": getattr(trial, "number", None), + **score_fields, + } + try: + from kis_trader.backtest.optuna_common import stability_fields_from_trial_attrs + row.update(stability_fields_from_trial_attrs(trial)) + except Exception: + pass + return row + + +def _live_mode_top3( + study_name: str, + *, + start: str, + end: str, + grid_keys: Optional[List[str]] = None, + n: int = 3, +) -> Optional[Dict[str, Any]]: + """진행 중 study — PnL 양수 pool 밴드 근접 mode Top3 + 일평균.""" + name = str(study_name or "").strip() + if not name: + return None + try: + import optuna + from kis_trader.backtest.optuna_common import resolve_optuna_storage_url + from kis_trader.backtest.optuna_mode_combo import build_results_mode_consensus_tier + + storage = resolve_optuna_storage_url() + study = optuna.load_study(study_name=name, storage=storage) + complete = optuna.trial.TrialState.COMPLETE + results: List[Dict[str, Any]] = [] + for tr in list(study.trials or []): + if getattr(tr, "state", None) != complete: + continue + row = _trial_to_result_row(tr) + if row is not None: + results.append(row) + if not results: + return None + data = {"start": start, "end": end, "results": results, "grid_keys": list(grid_keys or [])} + mode_rows, mode_meta = build_results_mode_consensus_tier( + results, + top_n=max(1, int(n)), + grid_keys=grid_keys, + data=data, + ) + top3: List[Dict[str, Any]] = [] + for i, row in enumerate(mode_rows[: max(1, int(n))], start=1): + _annotate_row_period_daily(row, start=start, end=end) + met = _row_metrics(row, label=f"mode #{i}", source="consensus", data=data) + if not met: + continue + for k in ("consensus_match_pct", "consensus_match_n", "consensus_match_of", "consensus_band_err"): + if row.get(k) is not None: + met[k] = row.get(k) + top3.append(met) + if not top3: + return None + ps = str(start or "").strip()[:10] + pe = str(end or "").strip()[:10] + if not (ps and pe): + ps, pe = _period_from_study(name) + return { + "mode_top3": top3, + "mode_pool_size": mode_meta.get("mode_pool_size"), + "mode_band_axes": mode_meta.get("band_axes"), + "period_range": _fmt_period_range(ps, pe), + } + except Exception: + return None + + +def _breakout_import_label(study: str, data: Dict[str, Any]) -> str: + """CLI import 잡 — refine1/2·sl_mode 구분 라벨.""" + sy = str(study or "") + if "refine2" in sy: + return "돌파·2차TPE" + if "refine1" in sy: + return "돌파·1차TPE" + sm = "" + if "_atr_" in sy or sy.endswith("_atr"): + sm = "atr" + elif "_fixed_" in sy: + sm = "fixed" + else: + rows = list(data.get("results_all") or data.get("results") or []) + p0 = (rows[0] or {}).get("params") or {} if rows else {} + sm = str(p0.get("sl_mode") or "").strip() + return f"돌파({sm})" if sm else "돌파" + + +def _study_short_note(study: str) -> str: + sy = str(study or "").strip() + if not sy: + return "" + if "refine2" in sy: + return "refine2" + if "refine1" in sy: + return "refine1" + if len(sy) <= 36: + return sy + return "…" + sy[-34:] + + +def _trial_to_learn_row(trial: Any) -> Dict[str, Any]: + ua = dict(getattr(trial, "user_attrs", None) or {}) + params: Dict[str, Any] = {} + raw = ua.get("params_json") + if raw: + try: + params = json.loads(str(raw)) + except Exception: + params = {} + return { + "optuna_trial_number": getattr(trial, "number", None), + "total_pnl": ua.get("total_pnl"), + "win_rate": ua.get("win_rate"), + "pf": ua.get("pf"), + "total_trades": ua.get("total_trades"), + "score": getattr(trial, "value", None), + "params": params, + } + + +def _annotate_row_period_daily(row: Dict[str, Any], *, start: str, end: str) -> None: + from kis_trader.utils.kr_trading_day import count_kr_trading_days + + try: + n_days = count_kr_trading_days(str(start or ""), str(end or "")) + except Exception: + n_days = 1 + n_days = max(1, int(n_days or 1)) + try: + pnl = float(row.get("total_pnl") or 0) + except (TypeError, ValueError): + pnl = 0.0 + row["n_period_trading_days"] = n_days + row["period_daily_avg_pnl"] = round(pnl / float(n_days), 2) + + +def _period_from_study(study_name: str) -> tuple: + """MariaDB optuna_study_result / payload_json 에서 study 백테 기간.""" + name = str(study_name or "").strip() + if not name: + return "", "" + try: + from kis_trader.backtest.optuna_study_store import load_payload_dict, load_row + + row = load_row(name) + if row: + s = str(row.get("start_date") or "").strip()[:10] + e = str(row.get("end_date") or "").strip()[:10] + if s and e: + return s, e + data = load_payload_dict(name) + if data: + s = str(data.get("start") or "").strip()[:10] + e = str(data.get("end") or "").strip()[:10] + if s and e: + return s, e + except Exception: + pass + return "", "" + + +def _fmt_period_range(start: str, end: str) -> str: + s = str(start or "").strip()[:10] + e = str(end or "").strip()[:10] + if s and e: + return f"{s}~{e}" + if s: + return s + return "—" + + +def _period_slot(label: str, start: str, end: str, study: str = "") -> Dict[str, str]: + s = str(start or "").strip()[:10] + e = str(end or "").strip()[:10] + return { + "label": label, + "start": s, + "end": e, + "range": _fmt_period_range(s, e), + "study": str(study or "").strip(), + } + + +def _infer_refine_study_pair(study_name: str) -> Tuple[str, str]: + """refine1/refine2 study 이름 쌍 추론 (import·완료 잡용).""" + sn = str(study_name or "").strip() + if not sn: + return "", "" + if "refine2" in sn: + p2 = sn + p1 = sn.replace("refine2", "refine1", 1) + return p1, p2 + if "refine1" in sn: + p1 = sn + p2 = sn.replace("refine1", "refine2", 1) + return p1, p2 + return "", "" + + +def _build_period_info(meta: Dict[str, Any]) -> Dict[str, Any]: + """잡(메인) · 1·2차 refine · 활성 study 백테 기간 — UI 강조용.""" + master_s = str(meta.get("start") or "").strip()[:10] + master_e = str(meta.get("end") or "").strip()[:10] + kind = str(meta.get("kind") or "") + rs = meta.get("refine_state") if isinstance(meta.get("refine_state"), dict) else {} + out: Dict[str, Any] = { + "master": _period_slot("잡(메인)", master_s, master_e), + "phase1": _period_slot("1차 TPE", "", "", ""), + "phase2": _period_slot("2차 TPE", "", "", ""), + "active": _period_slot("활성 study", "", "", ""), + "has_refine": False, + "same_all": True, + "mismatch": False, + "mismatch_notes": [], + } + if kind == "mode_refine" or rs: + out["has_refine"] = True + p1_study = str(rs.get("phase1_study") or "").strip() + p2_study = str(rs.get("phase2_study") or "").strip() + phase = str(rs.get("phase") or "").strip().lower() + if phase == "phase2" and not p2_study: + cand = _resolve_refine_active_study(rs) + if cand and cand != p1_study: + p2_study = cand + p1_s = str(rs.get("start") or master_s)[:10] + p1_e = str(rs.get("end") or master_e)[:10] + db_s, db_e = _period_from_study(p1_study) + if db_s and db_e: + p1_s, p1_e = db_s, db_e + out["phase1"] = _period_slot("1차 TPE", p1_s, p1_e, p1_study) + p2_s, p2_e = p1_s, p1_e + if p2_study: + db2_s, db2_e = _period_from_study(p2_study) + if db2_s and db2_e: + p2_s, p2_e = db2_s, db2_e + out["phase2"] = _period_slot("2차 TPE", p2_s, p2_e, p2_study) + act_study = str(meta.get("active_study_name") or _resolve_refine_active_study(rs)) + act_s, act_e = master_s, master_e + if act_study: + ds, de = _period_from_study(act_study) + if ds and de: + act_s, act_e = ds, de + elif phase == "phase2": + act_s, act_e = p2_s, p2_e + elif phase == "phase1": + act_s, act_e = p1_s, p1_e + out["active"] = _period_slot("활성 study", act_s, act_e, act_study) + else: + act_study = str(meta.get("active_study_name") or meta.get("study_name") or "").strip() + p1_study, p2_study = _infer_refine_study_pair(act_study) + if not p1_study and meta.get("result_json"): + p1_from_json = _phase1_study_from_result_json(str(meta.get("result_json") or "")) + if p1_from_json: + p1_study = str(p1_from_json).strip() + if "refine1" in p1_study: + p2_study = p1_study.replace("refine1", "refine2", 1) + if p2_study and "refine2" in act_study: + out["has_refine"] = True + p_s, p_e = master_s, master_e + ds, de = _period_from_study(act_study) + if ds and de: + p_s, p_e = ds, de + elif master_s and master_e: + p_s, p_e = master_s, master_e + out["phase1"] = _period_slot("1차 TPE", p_s, p_e, p1_study) + out["phase2"] = _period_slot("2차 TPE", p_s, p_e, p2_study or act_study) + out["active"] = _period_slot("2차 study", p_s, p_e, act_study) + elif act_study: + act_s, act_e = master_s, master_e + ds, de = _period_from_study(act_study) + if ds and de: + act_s, act_e = ds, de + out["active"] = _period_slot("study", act_s, act_e, act_study) + notes: List[str] = [] + if out["has_refine"]: + p1 = out["phase1"] + p2 = out["phase2"] + if p1.get("start") and (p1["start"], p1["end"]) != (master_s, master_e): + notes.append("1차≠잡") + if p2.get("study") and (p2["start"], p2["end"]) != (master_s, master_e): + notes.append("2차≠잡") + if p2.get("study") and p1.get("start") and (p2["start"], p2["end"]) != (p1["start"], p1["end"]): + notes.append("1차≠2차") + ranges = { + (master_s, master_e), + } + if out["phase1"].get("start"): + ranges.add((out["phase1"]["start"], out["phase1"]["end"])) + if out["phase2"].get("study") and out["phase2"].get("start"): + ranges.add((out["phase2"]["start"], out["phase2"]["end"])) + ranges = {r for r in ranges if r[0] and r[1]} + out["same_all"] = len(ranges) <= 1 + out["mismatch"] = bool(notes) or len(ranges) > 1 + out["mismatch_notes"] = notes + if out["has_refine"] and out["same_all"] and master_s and master_e: + out["refine_note"] = ( + "1·2차 TPE 모두 동일 백테 기간(거래일 합산 아님 · 1차=넓은 탐색 → 2차=밴드 축소 재탐색)" + ) + else: + out["refine_note"] = "" + return out + + +def _live_study_top3( + study_name: str, + *, + start: str, + end: str, + n: int = 3, + label_prefix: str = "learn", +) -> Optional[Dict[str, Any]]: + """진행 중 study — COMPLETE trial TopN (분포·일평균 미리보기).""" + name = str(study_name or "").strip() + if not name: + return None + try: + import optuna + from kis_trader.backtest.optuna_common import resolve_optuna_storage_url + from kis_trader.backtest.optuna_study_store import count_study_states + + storage = resolve_optuna_storage_url() + study = optuna.load_study(study_name=name, storage=storage) + complete = optuna.trial.TrialState.COMPLETE + ok = [ + t for t in list(study.trials or []) + if getattr(t, "state", None) == complete + and getattr(t, "value", None) is not None + ] + ok.sort(key=lambda t: float(t.value), reverse=True) + top3: List[Dict[str, Any]] = [] + for i, tr in enumerate(ok[: max(1, int(n))], start=1): + row = _trial_to_learn_row(tr) + _annotate_row_period_daily(row, start=start, end=end) + met = _row_metrics(row, label=f"{label_prefix} #{i}", source="learn") + if met: + top3.append(met) + n_c, n_r, n_f = count_study_states(study) + ps = str(start or "").strip()[:10] + pe = str(end or "").strip()[:10] + if not (ps and pe): + ps, pe = _period_from_study(name) + return { + "study_name": name, + "top3_learn": top3, + "n_complete": n_c, + "n_finished": n_f, + "n_running": n_r, + "period_start": ps, + "period_end": pe, + "period_range": _fmt_period_range(ps, pe), + } + except Exception: + return None + + def _study_progress(study_name: str, trials_total: int) -> Dict[str, Any]: """Optuna MariaDB study 기준 진행률 + best trial 실측 지표. @@ -1157,7 +2078,7 @@ def _row_metrics( for k in ( "stability_score", "n_losing_days", "n_active_days", "worst_day_pnl", "best_day_pnl", "daily_pnl_mean", "daily_pnl_std", - "daily_pnl", "period_daily_avg_pnl", "n_period_trading_days", + "daily_pnl", "period_daily_avg_pnl", "period_daily_avg_pct", "n_period_trading_days", ): if row.get(k) is not None: out[k] = row.get(k) @@ -1198,6 +2119,7 @@ def _summarize_result_data( from kis_trader.backtest.optuna_common import ( annotate_optuna_period_daily_avg, resolve_results_stable, + resolve_results_mode_consensus, ) from kis_trader.backtest.optuna_postprocess_topn import resolve_post_top_n @@ -1205,6 +2127,7 @@ def _summarize_result_data( top_n = resolve_post_top_n(10) gated = list(data.get("results_gated") or []) stable, stable_gates_resolved = resolve_results_stable(data, top_n=top_n) + mode_consensus, mode_consensus_meta = resolve_results_mode_consensus(data, top_n=top_n) allr = list(data.get("results") or data.get("results_all") or []) learn = allr[0] if allr else None gate0 = gated[0] if gated else None @@ -1267,6 +2190,26 @@ def _summarize_result_data( mode_row.update(_ob_whip_ui_from_params(mc.get("params") or {})) except Exception: pass + try: + _annotate_row_period_daily( + mode_row, + start=str(data.get("start") or ""), + end=str(data.get("end") or ""), + ) + try: + budget = float( + data.get("total_budget_krw") or data.get("total_budget") or 0, + ) + except (TypeError, ValueError): + budget = 0.0 + n_days = int(mode_row.get("n_period_trading_days") or 1) + pnl_mc = float(mode_row.get("total_pnl") or 0) + if budget > 0 and n_days > 0: + mode_row["period_daily_avg_pct"] = round( + pnl_mc / budget * 100.0 / float(n_days), 3, + ) + 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) @@ -1299,6 +2242,17 @@ def _summarize_result_data( m["rank"] = i top5_stable.append(m) + top5_consensus: List[Dict[str, Any]] = [] + for i, row in enumerate(mode_consensus[:top_n], start=1): + m = _row_metrics(row, label=f"mode #{i}", source="consensus", data=data) + if m: + m["rank"] = i + if row.get("consensus_match_pct") is not None: + m["consensus_match_pct"] = row.get("consensus_match_pct") + m["consensus_match_n"] = row.get("consensus_match_n") + m["consensus_match_of"] = row.get("consensus_match_of") + top5_consensus.append(m) + briefing = None bp = str(path).replace(".json", ".briefing.md") if Path(bp).is_file(): @@ -1362,6 +2316,7 @@ def _summarize_result_data( "min_trades_per_day": data.get("min_trades_per_day"), "n_gated": len(gated), "n_stable": len(stable), + "n_consensus": len(mode_consensus), "n_all": len(allr), "optuna_best_trial_number": data.get("optuna_best_trial_number"), "stable_gates": stable_gates_resolved or data.get("stable_gates"), @@ -1375,6 +2330,7 @@ def _summarize_result_data( "method": mc.get("method"), "top_n": mc.get("top_n"), "pool_size": mc.get("pool_size"), + "pool_kind": mc.get("pool_kind"), "vs_best": vs if vs else None, "has_params": bool(mc.get("params")), }, @@ -1389,6 +2345,8 @@ def _summarize_result_data( "top5_gated": top5, "top5_learn": top5_learn, "top5_stable": top5_stable, + "top5_consensus": top5_consensus, + "mode_consensus_meta": mode_consensus_meta or data.get("mode_consensus_meta"), "top5_mode": top5_mode, # 본 TPE 호가축 여부 · 사후8방 생략 안내 "tpe_includes_orderbook": ( @@ -1459,6 +2417,11 @@ def _pool_for_optuna_source(data: Dict[str, Any], src: str) -> List[Dict[str, An return list(pool or []) if s == "gated": return list(data.get("results_gated") or []) + if s in ("consensus", "mode_consensus", "results_mode", "mode_top"): + from kis_trader.backtest.optuna_common import resolve_results_mode_consensus + from kis_trader.backtest.optuna_postprocess_topn import resolve_post_top_n + pool, _ = resolve_results_mode_consensus(data, top_n=resolve_post_top_n(10)) + return list(pool or []) return list(data.get("results") or data.get("results_all") or []) @@ -1852,16 +2815,7 @@ def register_result_json_as_job( em = str(p0.get("entry_mode") or "").strip() label = f"꼬리({em})" if em else "꼬리" elif strat == "breakout": - sm = "" - if "_atr_" in study or study.endswith("_atr"): - sm = "atr" - elif "_fixed_" in study: - sm = "fixed" - else: - rows = list(data.get("results_all") or data.get("results") or []) - p0 = (rows[0] or {}).get("params") or {} if rows else {} - sm = str(p0.get("sl_mode") or "").strip() - label = f"돌파({sm})" if sm else "돌파" + label = _breakout_import_label(study, data) _labels_imp = { "momentum": "모멘텀", "us_momentum": "해외모멘텀", @@ -1952,6 +2906,72 @@ def _child_jobs_from_jsons(paths: List[str]) -> List[Dict[str, Any]]: return out +def _apply_refine_state_progress( + m: Dict[str, Any], + prog: Dict[str, Any], + state_path: Path, + *, + alive: bool, + seq_step: bool = False, +) -> Optional[str]: + """1·2차 refine_state.json → 진행률·활성 study·로그. seq_step=True 면 잡 status 는 건드리지 않음.""" + if not state_path.is_file(): + return None + active_log: Optional[str] = None + try: + st = json.loads(state_path.read_text(encoding="utf-8")) + m["refine_state"] = st + phase = str(st.get("phase") or "") + prog["refine_phase"] = phase + strat = str(st.get("strategy") or m.get("current_strategy") or "") + if strat: + m["current_strategy"] = strat + if phase == "phase1": + prog["label"] = f"{strat or 'Optuna'} · 1차 TPE(넓은 Grid)" + elif phase == "phase2": + prog["label"] = f"{strat or 'Optuna'} · 2차 TPE(밴드 축소)" + elif phase == "done": + prog["label"] = f"{strat or 'Optuna'} · 1·2차 완료" + active_study = _resolve_refine_active_study(st) if phase in ("phase1", "phase2") else str(st.get("phase1_study") or "") + if phase == "phase2" and not active_study: + active_study = str(st.get("phase2_study") or st.get("phase1_study") or "") + elif phase == "phase1": + active_study = str(st.get("phase1_study") or active_study or "") + phase_trials = int(m.get("trials") or 0) + if phase == "phase2" and st.get("phase2_trials"): + try: + phase_trials = int(st.get("phase2_trials") or phase_trials) + except (TypeError, ValueError): + pass + if active_study: + phase_prog = _study_progress(active_study, phase_trials) + for k in ( + "trials_done", "trials_total", "pct", "best_value", "best_trial", + "best_win_rate", "best_pnl", "best_pf", "best_mdd", "best_trades", + "study_ok", "error", + ): + if k in phase_prog and phase_prog.get(k) is not None: + prog[k] = phase_prog[k] + m["active_study_name"] = active_study + if st.get("result_json") and not seq_step: + m["result_json"] = st["result_json"] + p2_log = st.get("phase2_log") + p1_log = st.get("phase1_log") + if phase == "phase2" and p2_log and Path(str(p2_log)).is_file(): + active_log = str(p2_log) + elif p1_log and Path(str(p1_log)).is_file(): + active_log = str(p1_log) + if not alive and not seq_step: + if phase == "done": + m["status"] = "done" + elif phase == "error": + m["status"] = "error" + m["error"] = st.get("error") or m.get("error") + except Exception: + return active_log + return active_log + + def refresh_job_status(meta: Dict[str, Any]) -> Dict[str, Any]: """pid/로그/study 로 status·progress 갱신 후 저장.""" m = dict(meta) @@ -1986,17 +3006,28 @@ def refresh_job_status(meta: Dict[str, Any]) -> Dict[str, Any]: _apply_seq_active(m, side_info) elif log_info.get("study") or log_info.get("strategy"): _apply_seq_active(m, log_info) - cs = str(m.get("current_strategy") or "").strip().lower() - if cs: - lp = ROOT / "logs" / f"optuna_{cs}_tpe_latest.logpath" - try: - if lp.is_file(): - val = lp.read_text(encoding="utf-8").strip() - if val: - m["active_log_path"] = val - except Exception: - pass - if m.get("active_study_name"): + refine_sp = str(side_info.get("refine_state_path") or "").strip() + if refine_sp: + m["refine_state_path"] = refine_sp + rl = _apply_refine_state_progress( + m, prog, Path(refine_sp), alive=alive, seq_step=True, + ) + if rl: + active_log = rl + m["active_log_path"] = rl + if not refine_sp: + cs = str(m.get("current_strategy") or "").strip().lower() + if cs: + lp = ROOT / "logs" / f"optuna_{cs}_tpe_latest.logpath" + try: + if lp.is_file(): + val = lp.read_text(encoding="utf-8").strip() + if val: + m["active_log_path"] = val + active_log = val + except Exception: + pass + if not refine_sp and m.get("active_study_name"): prog = _study_progress( str(m["active_study_name"]), int(m.get("trials") or 0) ) @@ -2023,6 +3054,94 @@ def refresh_job_status(meta: Dict[str, Any]) -> Dict[str, Any]: m["status"] = "done" alive = False + if m.get("kind") == "mode_refine": + state_path = Path(str(m.get("refine_state_path") or "")) + if not state_path.is_file() and m.get("job_id"): + state_path = ROOT / "logs" / f"{m.get('job_id')}_refine_state.json" + rl = _apply_refine_state_progress( + m, prog, state_path, alive=alive, seq_step=False, + ) + if rl: + active_log = rl + m["period_info"] = _build_period_info(m) + if alive: + rs = m.get("refine_state") if isinstance(m.get("refine_state"), dict) else {} + phase = str(rs.get("phase") or prog.get("refine_phase") or "") + act = str(m.get("active_study_name") or _resolve_refine_active_study(rs)) + pi = m.get("period_info") if isinstance(m.get("period_info"), dict) else {} + p1_slot = pi.get("phase1") if isinstance(pi.get("phase1"), dict) else {} + p2_slot = pi.get("phase2") if isinstance(pi.get("phase2"), dict) else {} + start = str(m.get("start") or "") + end = str(m.get("end") or "") + live: Dict[str, Any] = {"refine_phase": phase} + if act and phase in ("phase1", "phase2"): + cur_start = p2_slot.get("start") if phase == "phase2" else p1_slot.get("start") or start + cur_end = p2_slot.get("end") if phase == "phase2" else p1_slot.get("end") or end + cur = _live_study_top3( + act, start=cur_start, end=cur_end, n=5, + label_prefix="2차" if phase == "phase2" else "1차", + ) + if cur: + live.update(cur) + if phase == "phase2": + p1s = str(rs.get("phase1_study") or "").strip() + if p1s: + p1live = _live_study_top3( + p1s, + start=p1_slot.get("start") or start, + end=p1_slot.get("end") or end, + n=5, + label_prefix="1차", + ) + if p1live: + live["phase1_top3"] = p1live.get("top3_learn") or [] + live["phase1_period_range"] = p1live.get("period_range") or "" + gkeys: List[str] = [] + try: + rj = m.get("result_json") + if rj and Path(str(rj)).is_file(): + jd = json.loads(Path(str(rj)).read_text(encoding="utf-8")) + gkeys = list(jd.get("grid_keys") or []) + except Exception: + gkeys = [] + if act and phase in ("phase1", "phase2"): + cur_start = p2_slot.get("start") if phase == "phase2" else p1_slot.get("start") or start + cur_end = p2_slot.get("end") if phase == "phase2" else p1_slot.get("end") or end + mode_live = _live_mode_top3( + act, start=cur_start, end=cur_end, grid_keys=gkeys or None, n=5, + ) + if mode_live: + live["mode_top3"] = mode_live.get("mode_top3") or [] + live["mode_pool_size"] = mode_live.get("mode_pool_size") + live["mode_period_range"] = mode_live.get("period_range") or "" + if live.get("top3_learn") or live.get("phase1_top3") or live.get("mode_top3"): + m["live_summary"] = live + else: + m.pop("live_summary", None) + elif alive and m.get("active_study_name"): + act = str(m.get("active_study_name") or "") + start = str(m.get("start") or "") + end = str(m.get("end") or "") + gkeys = [] + try: + rj = m.get("result_json") + if rj and Path(str(rj)).is_file(): + jd = json.loads(Path(str(rj)).read_text(encoding="utf-8")) + gkeys = list(jd.get("grid_keys") or []) + except Exception: + pass + learn = _live_study_top3(act, start=start, end=end, n=5, label_prefix="learn") + mode_live = _live_mode_top3(act, start=start, end=end, grid_keys=gkeys or None, n=5) + live2: Dict[str, Any] = {} + if learn: + live2.update(learn) + if mode_live: + live2["mode_top3"] = mode_live.get("mode_top3") or [] + live2["mode_pool_size"] = mode_live.get("mode_pool_size") + live2["mode_period_range"] = mode_live.get("period_range") or "" + if live2.get("top3_learn") or live2.get("mode_top3"): + m["live_summary"] = live2 + if alive: m["status"] = "running" m["finished_at"] = None @@ -2085,6 +3204,10 @@ def refresh_job_status(meta: Dict[str, Any]) -> Dict[str, Any]: except Exception: m["finished_ts"] = float(m.get("started_ts") or time.time()) + if m.get("status") in ("done", "error") and not alive: + # 진행 중 learn/mode Top3 스냅샷 — 완료 후에는 gated·mode Top3(결과 JSON)만 표시 + m.pop("live_summary", None) + if m.get("status") == "done" and not alive: # 순차: 1번 스터디 200/200 이어도 프로세스가 살아 있으면 아직 다음 전략 prog["pct"] = 100.0 @@ -2115,6 +3238,34 @@ def refresh_job_status(meta: Dict[str, Any]) -> Dict[str, Any]: sm = _summarize_result_json(m.get("result_json")) m["result_summary"] = sm or prev_sum _attach_study_result_flags(m) + if m.get("kind") == "import" and m.get("result_json"): + try: + rpath = Path(str(m["result_json"])) + if rpath.is_file(): + idata = json.loads(rpath.read_text(encoding="utf-8")) + study = str(idata.get("optuna_study_name") or m.get("study_name") or "") + if study: + m["study_name"] = study + m["study_short"] = _study_short_note(study) + strat = str(m.get("strategy") or idata.get("strategy") or "").lower() + if strat == "breakout": + m["label"] = _breakout_import_label(study, idata) + bp = Path(str(rpath).replace(".json", ".briefing.md")) + need_brief = not bp.is_file() + if not need_brief: + try: + need_brief = "최종 선택 후보" not in bp.read_text(encoding="utf-8") + except Exception: + need_brief = True + if need_brief: + from kis_trader.backtest.optuna_briefing import write_briefing_for_json + write_briefing_for_json(str(rpath)) + m["briefing_md"] = str(bp) + except Exception: + pass + m["study_short"] = m.get("study_short") or _study_short_note( + str(m.get("study_name") or m.get("active_study_name") or "") + ) if m.get("briefing_md") and Path(str(m["briefing_md"])).is_file(): try: m["briefing_preview"] = Path(str(m["briefing_md"])).read_text(encoding="utf-8")[:4000] @@ -2192,7 +3343,24 @@ def refresh_job_status(meta: Dict[str, Any]) -> Dict[str, Any]: post["hint"] = m.get("leftover_note") or "목표 미달 · 이어 돌리기 또는 확정" m["postprocess"] = post + m["period_info"] = _build_period_info(m) save_job(m) + try: + if str(m.get("kind") or "") in ("seq", "seq4", "mode_refine"): + m["seq_refine_steps"] = [ + { + "step": c.get("step"), + "label": c.get("label"), + "job_id": c.get("job_id"), + "phase1_study": c.get("phase1_study"), + "phase1_db": c.get("phase1_db"), + "done": c.get("done"), + } + for c in _build_seq_refine_cmds(m) + ] + save_job(m) + except Exception: + pass m.update(build_optuna_join_payload(m)) return m @@ -2211,7 +3379,7 @@ def any_optuna_python_running() -> Optional[Dict[str, Any]]: """웹 외 CLI nohup 도 상단바에 힌트용.""" try: r = subprocess.run( - ["pgrep", "-af", "param_search_optuna.py|run_optuna_4strat_tpe_seq.sh"], + ["pgrep", "-af", "param_search_optuna.py|optuna_mode_refine_runner.py|run_optuna_4strat_tpe_seq.sh"], capture_output=True, text=True, timeout=3, @@ -2220,7 +3388,7 @@ def any_optuna_python_running() -> Optional[Dict[str, Any]]: for ln in (r.stdout or "").splitlines(): if "extglob" in ln or "pgrep" in ln: continue - if "param_search_optuna.py" in ln or "run_optuna_4strat_tpe_seq.sh" in ln: + if "param_search_optuna.py" in ln or "optuna_mode_refine_runner.py" in ln or "run_optuna_4strat_tpe_seq.sh" in ln: lines.append(ln) if not lines: return None @@ -2319,6 +3487,7 @@ def start_optuna_job( from kis_trader.backtest.optuna_study_store import parse_study_trials_value st_goal = parse_study_trials_value(study_trials) reuse_study = str(study_name_override or "").strip() + refine_state_path: Optional[str] = None start = str(start or "").strip() end = str(end or "").strip() if not start or not end: @@ -2398,6 +3567,9 @@ def start_optuna_job( env["END"] = end env["TRIALS"] = str(trials) env["OPTUNA_SEQ_ACTIVE_FILE"] = str(seq_active) + env["OPTUNA_SEQ_JOB_ID"] = job_id + if sort_by: + env["SORT_BY"] = sort_by if st_goal > 0: env["STUDY_TRIALS"] = str(st_goal) env["KIS_OPTUNA_STUDY_TRIALS"] = str(st_goal) @@ -2424,11 +3596,10 @@ def start_optuna_job( if "breakout" in picked and 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 + ")" + label = "순차1·2차(" + _lab + ")" strat_field = ",".join(picked) else: strat = picked[0] - job_id = f"opt_{ts}_{strat[:4]}" if sym and strat == "us_momentum": study_name = ( f"usmom_{sym}_{mode}_{start.replace('-', '')}_{end.replace('-', '')}_{ts}" @@ -2454,44 +3625,92 @@ def start_optuna_job( study_name = f"{strat}_{mode}_{start.replace('-', '')}_{end.replace('-', '')}_{ts}" log_path = ROOT / "logs" / f"optuna_web_{strat}_{ts}.log" label = _labels.get(strat, strat) - cmd = [ - str(PY if PY.is_file() else "python3"), - "-u", - str(ROOT / "kis_trader" / "backtest" / "param_search_optuna.py"), - "--strategy", strat, - "--mode", mode, - "--start", start, - "--end", end, - "--trials", str(trials), - "--min_trades", str(min_trades), - "--min_win_rate", "0", - "--min_pf", "0", - "--orderbook-filter", (bo_oms[0] if strat == "breakout" else "off"), - "--no-progress", - "--study-name", study_name, - "--sort-by", sort_by, - "--universe-history-source", hist_src, - ] - if st_goal > 0: - cmd.extend(["--study-trials", str(st_goal)]) - env["KIS_OPTUNA_STUDY_TRIALS"] = str(st_goal) - env["PARAM_SEARCH_OPTUNA_STUDY_TRIALS"] = str(st_goal) - if strat == "tail": - cmd.extend(["--entry-mode", tail_ems[0]]) - if strat == "breakout": - cmd.extend(["--sl-mode", bo_sms[0]]) - if candle_source: - cmd.extend(["--candle-source", candle_source]) - env["CANDLE_SOURCE"] = candle_source - if tick_source: - cmd.extend(["--tick-source", tick_source]) - env["TICK_SOURCE"] = tick_source - if ob_source: - cmd.extend(["--ob-source", ob_source]) - env["OB_SOURCE"] = ob_source - if sym and strat == "us_momentum": - cmd.extend(["--symbol", sym]) - kind = "single" + + refine_state_path = None + if reuse_study: + # 이어 돌리기: 단일 study (1·2차 연쇄 아님) + job_id = f"opt_{ts}_{strat[:4]}" + cmd = [ + str(PY if PY.is_file() else "python3"), + "-u", + str(ROOT / "kis_trader" / "backtest" / "param_search_optuna.py"), + "--strategy", strat, + "--mode", mode, + "--start", start, + "--end", end, + "--trials", str(trials), + "--min_trades", str(min_trades), + "--min_win_rate", "0", + "--min_pf", "0", + "--orderbook-filter", (bo_oms[0] if strat == "breakout" else "off"), + "--no-progress", + "--study-name", study_name, + "--sort-by", sort_by, + "--universe-history-source", hist_src, + ] + if st_goal > 0: + cmd.extend(["--study-trials", str(st_goal)]) + env["KIS_OPTUNA_STUDY_TRIALS"] = str(st_goal) + env["PARAM_SEARCH_OPTUNA_STUDY_TRIALS"] = str(st_goal) + if strat == "tail": + cmd.extend(["--entry-mode", tail_ems[0]]) + if strat == "breakout": + cmd.extend(["--sl-mode", bo_sms[0]]) + if candle_source: + cmd.extend(["--candle-source", candle_source]) + env["CANDLE_SOURCE"] = candle_source + if tick_source: + cmd.extend(["--tick-source", tick_source]) + env["TICK_SOURCE"] = tick_source + if ob_source: + cmd.extend(["--ob-source", ob_source]) + env["OB_SOURCE"] = ob_source + if sym and strat == "us_momentum": + cmd.extend(["--symbol", sym]) + kind = "single" + else: + # 단일 전략 기본: 1차(넓은 Grid) → 2차(밴드 축소) 자동 연쇄 + job_id = f"opt_{ts}_refine_{strat[:4]}" + log_path = ROOT / "logs" / f"optuna_web_refine_{ts}.log" + study_name = ( + f"{strat}_{mode}_refine2_{start.replace('-', '')}_{end.replace('-', '')}_{ts}" + ) + label = f"1·2차TPE·{label}" + refine_state_path = str(ROOT / "logs" / f"{job_id}_refine_state.json") + cmd = [ + str(PY if PY.is_file() else "python3"), + "-u", + str(ROOT / "kis_trader" / "backtest" / "optuna_mode_refine_runner.py"), + "--job-id", job_id, + "--strategy", strat, + "--mode", mode, + "--start", start, + "--end", end, + "--trials", str(trials), + "--sort-by", sort_by, + "--min-trades", str(min_trades), + "--universe-history-source", hist_src, + ] + if strat == "tail": + cmd.extend(["--entry-mode", tail_ems[0]]) + if strat == "breakout": + cmd.extend(["--sl-mode", bo_sms[0], "--ob-mode", bo_oms[0]]) + if sym and strat == "us_momentum": + cmd.extend(["--symbol", sym]) + if candle_source: + cmd.extend(["--candle-source", candle_source]) + env["CANDLE_SOURCE"] = candle_source + if tick_source: + cmd.extend(["--tick-source", tick_source]) + env["TICK_SOURCE"] = tick_source + if ob_source: + cmd.extend(["--ob-source", ob_source]) + env["OB_SOURCE"] = ob_source + if st_goal > 0: + cmd.extend(["--study-trials", str(st_goal)]) + env["KIS_OPTUNA_STUDY_TRIALS"] = str(st_goal) + env["PARAM_SEARCH_OPTUNA_STUDY_TRIALS"] = str(st_goal) + kind = "mode_refine" strat_field = strat if reuse_study: @@ -2539,6 +3758,7 @@ def start_optuna_job( "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, + "refine_state_path": refine_state_path if kind == "mode_refine" else None, "log_path": str(log_path), "pid": int(proc.pid), "status": "running", @@ -2565,8 +3785,8 @@ def continue_optuna_job(job_id: str) -> Dict[str, Any]: meta = load_job(job_id) if not meta: raise FileNotFoundError(f"job not found: {job_id}") - if str(meta.get("kind") or "") in ("seq", "seq4"): - raise RuntimeError("순차 잡은 이어 돌리기 불가 — 전략별 보기 잡에서 하세요") + if str(meta.get("kind") or "") in ("seq", "seq4", "mode_refine"): + raise RuntimeError("순차·1·2차 잡은 이어 돌리기 불가 — 2차 study 또는 전략별 보기 잡에서 하세요") if str(meta.get("status") or "") != "done": raise RuntimeError("끝난 잡만 이어 돌리기 가능") name = _result_study_name(meta) @@ -2606,8 +3826,8 @@ def confirm_optuna_study(job_id: str) -> Dict[str, Any]: meta = load_job(job_id) if not meta: raise FileNotFoundError(f"job not found: {job_id}") - if str(meta.get("kind") or "") in ("seq", "seq4"): - raise RuntimeError("순차 잡은 확정 불가 — 전략별 보기 잡에서 하세요") + if str(meta.get("kind") or "") in ("seq", "seq4", "mode_refine"): + raise RuntimeError("순차·1·2차 잡은 확정 불가 — 전략별 보기 잡에서 하세요") if str(meta.get("status") or "") != "done": raise RuntimeError("끝난 잡만 확정 가능") name = _result_study_name(meta) diff --git a/kis_trader/backtest/results/optuna_breakout_fast_20260826_030807.briefing.md b/kis_trader/backtest/results/optuna_breakout_fast_20260826_030807.briefing.md new file mode 100644 index 0000000..1210ba3 --- /dev/null +++ b/kis_trader/backtest/results/optuna_breakout_fast_20260826_030807.briefing.md @@ -0,0 +1,46 @@ +# Optuna 브리핑 — 돌파 (fast) +기간: 2026-08-25 ~ 2026-08-25 (거래일≈1일) · trials 5/5 +탐색 게이트: WR≥0% PF≥0 · min_trades≥1 | 후보 게이트: WR≥40% PF≥1 · min_trades≥1 +- min_trades=1: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 5건 · 사후합격(results_gated) 2건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 58.0%** · **적용 가능도 42.0%** · 판정: **주의** — 적용 가능도 중간 — 다일(≥5일) 재검증·웹백테 후 소액만 + - 표본 장일: +40.0p — 거래일≈1일 — 단일 장 과적합 위험 최대 + - 거래 표본: +18.0p — 상위 후보 거래 3건 — 표본 부족 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +0.0p — 사후합격 2건 · stable 5건 + - PnL 고원: +0.0p — 고원 비율 50% (1건) +- 임계값 분포 표본: results_gated 상위 2건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `max_daily_chg` | 25.0 | 25.0 ~ 25.0 | 25.0 (100%) | +| `vol_mult` | 2.25 | 2.125 ~ 2.375 | 2.5 (50%) | +| `vol_window` | 1.0 | 1.0 ~ 1.0 | 1 (100%) | +| `min_turnover_1m_pct` | 0.1 | 0.1 ~ 0.1 | 0.1 (100%) | +| `prev_chg_min` | 0.5 | 0.5 ~ 0.5 | 0.5 (100%) | +| `prev_chg_max` | 8.0 | 8.0 ~ 8.0 | 8.0 (100%) | +| `min_price` | 3000.0 | 3000.0 ~ 3000.0 | 3000.0 (100%) | +| `tp_pct` | 12.0 | 12.0 ~ 12.0 | 12.0 (100%) | +| `sl_pct` | 4.0 | 4.0 ~ 4.0 | 4.0 (100%) | +| `sl_mode` | — | — | fixed (100%) | +| `atr_sl_mult` | 2.5 | 2.5 ~ 2.5 | 2.5 (100%) | +| `shoulder_min_high_pct` | 5.0 | 5.0 ~ 5.0 | 5.0 (100%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 이전 장에서는 +- 실매 동기간 청산 4건 · 승률 75% · 합산 PnL 35,844원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #1 · 거래 3건 · 승률 66.7% · PF 5.05 · PnL 13,725원 +- 표본 장이 1일뿐이라, ‘이전 장에 잘 먹힌 숫자’일 뿐 **추세가 바뀌면 바로 깨질 수 있습니다.** + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-26 03:08:09 (rule)_ + +## AI 코멘트 + +_(미생성: api_call_failed. 구글 키는 GEMINI_API_KEY 를 DB/환경에 넣으세요 — kis_trader/backtest/optuna_briefing.py 모듈 docstring 참고)_ diff --git a/kis_trader/backtest/results/optuna_breakout_tpe_20260823_223715.briefing.md b/kis_trader/backtest/results/optuna_breakout_tpe_20260823_223715.briefing.md new file mode 100644 index 0000000..f145d76 --- /dev/null +++ b/kis_trader/backtest/results/optuna_breakout_tpe_20260823_223715.briefing.md @@ -0,0 +1,50 @@ +# Optuna 브리핑 — 돌파 (tpe) +기간: 2026-08-18 ~ 2026-08-21 (거래일≈4일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥1 | 후보 게이트: WR≥40% PF≥1 · min_trades≥1 +- min_trades=1: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 136건 · 사후합격(results_gated) 17건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 66.0%** · **적용 가능도 34.0%** · 판정: **주의** — 적용 가능도 중간 — 다일(≥5일) 재검증·웹백테 후 소액만 + - 표본 장일: +16.0p — 거래일≈4일 — 다일 재검증 권장(≥5일) + - 거래 표본: +25.0p — 상위 후보 거래 1건 — 우연 승·과적합 가능 + - 승률·PF 이상치: +25.0p — 승률 100.0% + 거래 1건 · PF 9999.00 (소수 거래 폭증) + - 게이트 커버: +0.0p — 사후합격 17건 · stable 5건 + - PnL 고원: +0.0p — 고원 비율 24% (4건) +- 임계값 분포 표본: results_gated 상위 17건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `max_daily_chg` | 40.0 | 30.0 ~ 43.0 | 40.0 (24%) | +| `vol_mult` | 3.6 | 3.1 ~ 3.7 | 3.7 (24%) | +| `vol_window` | 14.0 | 14.0 ~ 15.0 | 15 (41%) | +| `min_turnover_1m_pct` | 0.2 | 0.15 ~ 0.3 | 0.15 (29%) | +| `prev_chg_min` | 0.9 | 0.9 ~ 1.0 | 0.9 (53%) | +| `prev_chg_max` | 9.5 | 7.5 ~ 11.5 | 9.5 (18%) | +| `min_price` | 4000.0 | 4000.0 ~ 4500.0 | 4000 (35%) | +| `tp_pct` | 5.5 | 5.0 ~ 9.0 | 5.0 (18%) | +| `sl_mode` | — | — | fixed (100%) | +| `trail_pct` | 3.0 | 2.3 ~ 3.7 | 2.3 (12%) | +| `trail_arm_pct` | 1.3 | 0.9 ~ 2.0 | 2.0 (12%) | +| `shoulder_min_high_pct` | 2.7 | 2.1 ~ 3.4 | 3.3 (12%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 이전 장에서는 +- 실매 동기간 청산 9건 · 승률 44% · 합산 PnL -2,360원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #46 · 거래 1건 · 승률 100.0% · PF 9999.00 · PnL 6,199원 +- 승률이 비정상적으로 높고 표본이 작습니다 → **해당 장에만 맞는 과적합** 신호. + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-23 22:37:22 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +### 이전 장에서는 +Optuna 백테스트는 4거래일간 1회 거래, 승률 100%, PF 9999.00으로 6,199원 수익을 보였습니다. 그러나 실제 매매에서는 9회 거래로 승률 44%, -2,360원의 손실을 기록했습니다. 이는 백테스트 과적합과 극심한 표본 부족, 그리고 실매와 백테스트 간 괴리(고스트 슬리피지 등)를 명확히 보여주는 결과입니다. + +### 앞으로 장에서는 +본 전략은 4거래일이라는 짧은 기간, 1회 거래라는 극심한 표본 부족으로 과적합 위험이 66.0%에 달해 '주의' 판정을 받았습니다. 따라서 이 결과를 바로 실매에 적용하는 것은 매우 위험합니다. 최소 5일 이상의 기간으로 재검증하고, 웹 백테스트 및 페이퍼/소액 모의 투자를 통해 실매와 백테스트 간의 괴리를 반드시 확인해야 합니다. 어떠한 종목 추천이나 확정 수익을 약속하지 않으며, 시장 상황 변화에 따른 손실 가능성을 인지하십시오. diff --git a/kis_trader/backtest/results/optuna_breakout_tpe_20260823_225201.briefing.md b/kis_trader/backtest/results/optuna_breakout_tpe_20260823_225201.briefing.md new file mode 100644 index 0000000..60631d3 --- /dev/null +++ b/kis_trader/backtest/results/optuna_breakout_tpe_20260823_225201.briefing.md @@ -0,0 +1,50 @@ +# Optuna 브리핑 — 돌파 (tpe) +기간: 2026-08-18 ~ 2026-08-21 (거래일≈4일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥1 | 후보 게이트: WR≥40% PF≥1 · min_trades≥1 +- min_trades=1: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 144건 · 사후합격(results_gated) 7건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 59.0%** · **적용 가능도 41.0%** · 판정: **주의** — 적용 가능도 중간 — 다일(≥5일) 재검증·웹백테 후 소액만 + - 표본 장일: +16.0p — 거래일≈4일 — 다일 재검증 권장(≥5일) + - 거래 표본: +18.0p — 상위 후보 거래 2건 — 표본 부족 + - 승률·PF 이상치: +25.0p — 승률 100.0% + 거래 2건 · PF 9999.00 (소수 거래 폭증) + - 게이트 커버: +0.0p — 사후합격 7건 · stable 10건 + - PnL 고원: +0.0p — 고원 비율 14% (1건) +- 임계값 분포 표본: results_gated 상위 7건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `max_daily_chg` | 25.0 | 22.5 ~ 29.0 | 25.0 (29%) | +| `vol_mult` | 3.7 | 3.35 ~ 4.2 | 3.7 (29%) | +| `vol_window` | 1.0 | 1.0 ~ 1.5 | 1 (71%) | +| `min_turnover_1m_pct` | 0.3 | 0.25 ~ 0.375 | 0.3 (29%) | +| `prev_chg_min` | 0.9 | 0.9 ~ 0.9 | 0.9 (71%) | +| `prev_chg_max` | 11.5 | 9.0 ~ 22.25 | 9.0 (29%) | +| `min_price` | 4000.0 | 3750.0 ~ 4500.0 | 4500 (43%) | +| `tp_pct` | 8.0 | 4.25 ~ 9.5 | 10.0 (29%) | +| `sl_mode` | — | — | atr (100%) | +| `trail_pct` | 3.3 | 0.45 ~ 4.65 | 0.0 (29%) | +| `trail_arm_pct` | 0.9 | 0.5 ~ 1.1 | 0.4 (29%) | +| `shoulder_min_high_pct` | 1.9 | 1.6 ~ 2.15 | 1.7 (14%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 이전 장에서는 +- 실매 동기간 청산 9건 · 승률 44% · 합산 PnL -2,360원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #93 · 거래 2건 · 승률 100.0% · PF 9999.00 · PnL 9,085원 +- 승률이 비정상적으로 높고 표본이 작습니다 → **해당 장에만 맞는 과적합** 신호. + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-23 22:52:08 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +**이전 장에서는** +이번 Optuna 백테스트(4일) 최적 전략(trial #93)은 단 2건의 거래로 100% 승률을 보였지만, 실제 매매에서는 9건의 거래로 44% 승률, -2,360원의 손실을 기록했습니다. 이는 **백테스트와 실매 간의 괴리**를 나타내며, 특히 짧은 기간과 적은 거래 수로 인한 **과적합 및 표본 부족** 위험이 명확합니다. + +**앞으로 장에서는** +단 4일 백테스트 결과는 **과적합 및 표본 부족** 위험이 커 즉시 실매 적용은 금물입니다. 최소 5일 이상의 재검증과 웹백테스트를 통해 **실전 적용 가능성을 충분히 확인**해야 합니다. 실매와 백테스트 간 **괴리(고스트퍼지 등)**는 언제든 발생할 수 있으니, `results_gated` 후보군으로 페이퍼/소액 모의 투자부터 시작, 손절 작동 여부를 우선 관찰하며 신중하게 접근하세요. diff --git a/kis_trader/backtest/results/optuna_breakout_tpe_20260824_020236.briefing.md b/kis_trader/backtest/results/optuna_breakout_tpe_20260824_020236.briefing.md new file mode 100644 index 0000000..f0f6611 --- /dev/null +++ b/kis_trader/backtest/results/optuna_breakout_tpe_20260824_020236.briefing.md @@ -0,0 +1,50 @@ +# Optuna 브리핑 — 돌파 (tpe) +기간: 2026-08-18 ~ 2026-08-21 (거래일≈4일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥1 | 후보 게이트: WR≥40% PF≥1 · min_trades≥1 +- min_trades=1: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 154건 · 사후합격(results_gated) 18건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 59.0%** · **적용 가능도 41.0%** · 판정: **주의** — 적용 가능도 중간 — 다일(≥5일) 재검증·웹백테 후 소액만 + - 표본 장일: +16.0p — 거래일≈4일 — 다일 재검증 권장(≥5일) + - 거래 표본: +18.0p — 상위 후보 거래 3건 — 표본 부족 + - 승률·PF 이상치: +25.0p — 승률 100.0% + 거래 3건 · PF 9999.00 (소수 거래 폭증) + - 게이트 커버: +0.0p — 사후합격 18건 · stable 10건 + - PnL 고원: +0.0p — 고원 비율 6% (1건) +- 임계값 분포 표본: results_gated 상위 18건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `max_daily_chg` | 48.5 | 47.25 ~ 50.75 | 48.0 (22%) | +| `vol_mult` | 4.0 | 3.55 ~ 4.625 | 4.4 (11%) | +| `vol_window` | 1.0 | 1.0 ~ 2.0 | 1 (67%) | +| `min_turnover_1m_pct` | 0.125 | 0.1 ~ 0.15 | 0.1 (33%) | +| `prev_chg_min` | 0.9 | 0.9 ~ 1.0 | 0.9 (56%) | +| `prev_chg_max` | 19.0 | 15.625 ~ 24.0 | 25.0 (11%) | +| `min_price` | 2500.0 | 2000.0 ~ 3500.0 | 2000 (22%) | +| `tp_pct` | 4.5 | 3.0 ~ 7.25 | 4.0 (11%) | +| `sl_mode` | — | — | fixed (100%) | +| `trail_pct` | 1.7 | 1.0 ~ 2.275 | 0.7 (11%) | +| `trail_arm_pct` | 1.05 | 0.8 ~ 2.0 | 0.8 (17%) | +| `shoulder_min_high_pct` | 3.3 | 3.05 ~ 3.9 | 3.3 (17%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 이전 장에서는 +- 실매 동기간 청산 9건 · 승률 44% · 합산 PnL -2,360원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #112 · 거래 3건 · 승률 100.0% · PF 9999.00 · PnL 13,269원 +- 승률이 비정상적으로 높고 표본이 작습니다 → **해당 장에만 맞는 과적합** 신호. + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-24 02:02:43 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +## 이전 장에서는 +백테스트 상위 결과(Optuna #112)는 짧은 4일간 3거래로 100% 승률과 높은 수익을 보였습니다. 하지만 같은 기간 실제 거래는 9건 중 44% 승률로 손실(-2,360원)을 기록하며 백테스트와 실거래 간의 큰 괴리가 확인됩니다. 이는 표본이 매우 적고 특정 장세에만 최적화된 **과적합** 신호로 판단됩니다. + +## 앞으로 장에서는 +이번 결과는 기간이 짧고 거래 횟수가 적어 과적합 위험(59%)이 높습니다. 절대 곧바로 실매에 적용하지 마시고, 최소 5일 이상의 긴 기간으로 **다일 재검증(웹백테스트)**해야 합니다. 실제 시장은 백테스트와 다르게 움직입니다 (고스트, 틱 경로 차이 등). 소액 모의투자로 손절매 기능이 정상 작동하는지 충분히 관찰 후 신중하게 적용해주세요. 확정 수익은 보장되지 않습니다. diff --git a/kis_trader/backtest/results/optuna_breakout_tpe_20260824_021717.briefing.md b/kis_trader/backtest/results/optuna_breakout_tpe_20260824_021717.briefing.md new file mode 100644 index 0000000..e0cb537 --- /dev/null +++ b/kis_trader/backtest/results/optuna_breakout_tpe_20260824_021717.briefing.md @@ -0,0 +1,50 @@ +# Optuna 브리핑 — 돌파 (tpe) +기간: 2026-08-18 ~ 2026-08-21 (거래일≈4일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥1 | 후보 게이트: WR≥40% PF≥1 · min_trades≥1 +- min_trades=1: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 138건 · 사후합격(results_gated) 26건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 59.0%** · **적용 가능도 41.0%** · 판정: **주의** — 적용 가능도 중간 — 다일(≥5일) 재검증·웹백테 후 소액만 + - 표본 장일: +16.0p — 거래일≈4일 — 다일 재검증 권장(≥5일) + - 거래 표본: +18.0p — 상위 후보 거래 2건 — 표본 부족 + - 승률·PF 이상치: +25.0p — 승률 100.0% + 거래 2건 · PF 9999.00 (소수 거래 폭증) + - 게이트 커버: +0.0p — 사후합격 26건 · stable 8건 + - PnL 고원: +0.0p — 고원 비율 4% (1건) +- 임계값 분포 표본: results_gated 상위 26건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `max_daily_chg` | 38.5 | 34.0 ~ 45.75 | 42.0 (12%) | +| `vol_mult` | 3.65 | 3.425 ~ 3.8 | 3.8 (23%) | +| `vol_window` | 15.0 | 14.0 ~ 15.0 | 15 (69%) | +| `min_turnover_1m_pct` | 0.175 | 0.1 ~ 0.25 | 0.05 (23%) | +| `prev_chg_min` | 1.0 | 0.9 ~ 1.0 | 1.0 (62%) | +| `prev_chg_max` | 25.75 | 22.5 ~ 28.375 | 28.5 (8%) | +| `min_price` | 4500.0 | 3625.0 ~ 5000.0 | 5000 (46%) | +| `tp_pct` | 5.25 | 1.5 ~ 7.75 | 1.5 (19%) | +| `sl_mode` | — | — | atr (100%) | +| `trail_pct` | 2.25 | 0.9 ~ 3.575 | 0.5 (12%) | +| `trail_arm_pct` | 1.8 | 1.175 ~ 2.775 | 2.2 (12%) | +| `shoulder_min_high_pct` | 2.8 | 2.125 ~ 3.325 | 3.1 (12%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 이전 장에서는 +- 실매 동기간 청산 9건 · 승률 44% · 합산 PnL -2,360원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #141 · 거래 2건 · 승률 100.0% · PF 9999.00 · PnL 12,449원 +- 승률이 비정상적으로 높고 표본이 작습니다 → **해당 장에만 맞는 과적합** 신호. + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-24 02:17:24 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +**이전 장에서는** +지난 4일간의 백테스트 최적 결과는 승률 100%와 높은 PF를 보였지만, 단 2건의 거래에서 비롯된 것입니다. 반면 실매는 9건 거래에 44% 승률, 손실을 기록하며 백테스트와 실매 간의 괴리(고스트퍼지, 유니버스 차이 등)가 컸습니다. 이는 표본 부족으로 인한 명확한 과적합 신호입니다. + +**앞으로 장에서는** +과적합 위험이 59%로 높으니, 이 결과를 바로 실매에 적용하는 것은 매우 위험합니다. 최소 5일 이상 장기간 재검증과 웹백테스트를 반드시 수행해야 합니다. 적용 시에는 `results_gated` 후보를 소액 모의투자 등으로 1~2일 관찰 후 신중하게 확대해야 합니다. 백테스트 결과가 미래 수익을 보장하지 않으며, 특정 종목 매수 추천이나 확정 수익 약속은 불가합니다. diff --git a/kis_trader/backtest/results/optuna_breakout_tpe_20260824_091243.briefing.md b/kis_trader/backtest/results/optuna_breakout_tpe_20260824_091243.briefing.md new file mode 100644 index 0000000..2dc83a9 --- /dev/null +++ b/kis_trader/backtest/results/optuna_breakout_tpe_20260824_091243.briefing.md @@ -0,0 +1,69 @@ +# Optuna 브리핑 — 돌파 (tpe) +기간: 2026-08-18 ~ 2026-08-24 (거래일≈7일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥1 | 후보 게이트: WR≥40% PF≥1 · min_trades≥1 +- min_trades=1: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 196건 · 사후합격(results_gated) 22건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 35.0%** · **적용 가능도 65.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈7일 — 표본 일수 상대적 양호 + - 거래 표본: +10.0p — 상위 후보 거래 5건 — 해석 시 주의 + - 승률·PF 이상치: +25.0p — 승률 100.0% + 거래 5건 · PF 9999.00 (소수 거래 폭증) + - 게이트 커버: +0.0p — 사후합격 22건 · stable 10건 + - PnL 고원: +0.0p — 고원 비율 5% (1건) +- 임계값 분포 표본: results_gated 상위 22건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `max_daily_chg` | 27.5 | 23.25 ~ 31.75 | 32.0 (9%) | +| `vol_mult` | 2.2 | 1.85 ~ 2.4 | 2.2 (23%) | +| `vol_window` | 1.0 | 1.0 ~ 2.0 | 1 (64%) | +| `min_turnover_1m_pct` | 0.3 | 0.1625 ~ 0.35 | 0.35 (23%) | +| `prev_chg_min` | 0.7 | 0.6 ~ 0.9 | 0.6 (27%) | +| `prev_chg_max` | 14.75 | 11.625 ~ 22.5 | 13.5 (9%) | +| `min_price` | 2750.0 | 2125.0 ~ 3375.0 | 3000 (23%) | +| `tp_pct` | 7.5 | 3.5 ~ 11.375 | 1.0 (18%) | +| `sl_mode` | — | — | fixed (100%) | +| `trail_pct` | 1.85 | 1.2 ~ 3.425 | 0.9 (9%) | +| `trail_arm_pct` | 4.1 | 3.425 ~ 5.0 | 5.0 (14%) | +| `shoulder_min_high_pct` | 3.85 | 2.8 ~ 4.175 | 2.8 (14%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 최종 선택 후보 (규칙 · DB 적용 전 확인) +- 아래는 **역할이 다른** 4종 후보입니다. mode Top10 1위 ≠ 익절 대표 · **DB 1순위는 사후합격(gated)** 입니다. +- mode pool: `positive` · scoring: `band_proximity_p25_p75` + +### 후보 4종 +- **① 사후합격 1위 (apply: gated)** (trial #181): PnL 22,110원 · WR 100.0% · PF 9999.00 · 거래 5건 · 안정점수 5462 · 익절1.0% 손절6.5% +- **② 안정 1위 (apply: stable)** (trial #181): PnL 22,110원 · WR 100.0% · PF 9999.00 · 거래 5건 · 안정점수 5462 · 익절1.0% 손절6.5% +- **③ mode_combo (apply: mode)** (trial 없음(조립)): PnL 23,613원 · WR 100.0% · PF 9999.00 · 거래 6건 · 안정점수 6897 · 익절1.0% 손절6.5% · tp 최빈 1.0% (3/20) +- **④ mode Top10 1위 (apply: consensus · 밴드 전형 trial)** (trial #39): PnL 4,171원 · WR 50.0% · PF 1.29 · 거래 4건 · 안정점수 -7800 · 근접 97.5% (22/27축 밴드안) · 익절12.5% 손절7.4% + +- mode pool tp_pct 밴드 p25~p75: **3.5~12%** · Top10 1위 tp=12.5% → 밴드 **밖**(다른 축 보정으로 근접% 높음) +- mode Top10 meta: pool 23건 · 축 27개 + +### 추천 (자동 · confirm 필수) +1. **실매 DB 적용 1순위 → ① 사후합격 1위** (`optunaApply gated`) — WR/PF·min_trades 사후 통과. +2. **변동성·손실일 줄이기 → ② 안정 1위** (`stable`) — PnL보다 일별 안정 우선 시. +3. **2차 narrow·대표 숫자 → ③ mode_combo** — 축별 최빈 조립 · trial 번호 없음 · 익절은 pool 최빈값 참고. +4. **④ mode Top10 1위** — 양수 pool 전형 trial(다축 밴드 근접). **익절 하나로 쓰지 말 것** · 표에서 「보기」→ consensus apply. + +## 이전 장에서는 +- 실매 동기간 청산 24건 · 승률 58% · 합산 PnL -20,053원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #181 · 거래 5건 · 승률 100.0% · PF 9999.00 · PnL 22,110원 +- 승률이 비정상적으로 높고 표본이 작습니다 → **해당 장에만 맞는 과적합** 신호. + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-30 12:59:02 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +**이전 장에서는** +실매에서는 -20,053원의 손실이 있었으나, Optuna 백테스트의 사후합격 1위(trial #181)는 5건의 거래로 PnL 22,110원, 100% 승률을 기록했습니다. 이는 백테스트와 실매 간의 괴리(고스트퍼지 현상)를 보여주며, 특히 적은 표본 거래 수와 비정상적 승률은 단기 시장에 극히 과적합되었을 가능성이 높다는 분명한 경고 신호입니다. + +**앞으로 장에서는** +현재 결과는 단 7일간의 짧은 기간과 적은 거래 수로 인한 과적합 및 표본 부족 위험이 매우 높습니다. 백테스트의 높은 승률이나 PnL은 실매에서 ‘고스트퍼지’와 같은 예측 불가능한 괴리로 이어질 수 있으므로, 어떤 종목도 확정 매수 추천이나 수익 보장을 드릴 수 없습니다. Optuna `사후합격` 후보라도 즉시 적용보다 추가적인 다일 검증 및 페이퍼/소액 모의투자로 손절 규칙 작동 여부를 먼저 확인 후 신중하게 접근해야 합니다. (mode Top10 1위는 익절 대표가 아님에 유의하세요.) diff --git a/kis_trader/backtest/results/optuna_breakout_tpe_20260824_092853.briefing.md b/kis_trader/backtest/results/optuna_breakout_tpe_20260824_092853.briefing.md new file mode 100644 index 0000000..70eca05 --- /dev/null +++ b/kis_trader/backtest/results/optuna_breakout_tpe_20260824_092853.briefing.md @@ -0,0 +1,52 @@ +# Optuna 브리핑 — 돌파 (tpe) +기간: 2026-08-18 ~ 2026-08-24 (거래일≈7일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥1 | 후보 게이트: WR≥40% PF≥1 · min_trades≥1 +- min_trades=1: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 130건 · 사후합격(results_gated) 27건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 50.0%** · **적용 가능도 50.0%** · 판정: **주의** — 적용 가능도 중간 — 다일(≥5일) 재검증·웹백테 후 소액만 + - 표본 장일: +0.0p — 거래일≈7일 — 표본 일수 상대적 양호 + - 거래 표본: +25.0p — 상위 후보 거래 1건 — 우연 승·과적합 가능 + - 승률·PF 이상치: +25.0p — 승률 100.0% + 거래 1건 · PF 9999.00 (소수 거래 폭증) + - 게이트 커버: +0.0p — 사후합격 27건 · stable 10건 + - PnL 고원: +0.0p — 고원 비율 4% (1건) +- 임계값 분포 표본: results_gated 상위 27건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `max_daily_chg` | 33.0 | 30.0 ~ 37.0 | 33.0 (11%) | +| `vol_mult` | 4.2 | 3.75 ~ 4.7 | 4.2 (11%) | +| `vol_window` | 3.0 | 1.0 ~ 3.5 | 1 (44%) | +| `min_turnover_1m_pct` | 0.15 | 0.1 ~ 0.3 | 0.1 (26%) | +| `prev_chg_min` | 0.3 | 0.2 ~ 0.5 | 0.2 (41%) | +| `prev_chg_max` | 23.5 | 18.75 ~ 27.5 | 25.5 (11%) | +| `min_price` | 2000.0 | 2000.0 ~ 3000.0 | 2000 (30%) | +| `tp_pct` | 3.5 | 1.0 ~ 6.5 | 1.0 (37%) | +| `sl_mode` | — | — | fixed (100%) | +| `trail_pct` | 4.8 | 4.25 ~ 5.65 | 5.9 (15%) | +| `trail_arm_pct` | 4.2 | 3.1 ~ 5.2 | 5.2 (11%) | +| `shoulder_min_high_pct` | 3.5 | 3.1 ~ 4.2 | 3.1 (11%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 이전 장에서는 +- 실매 동기간 청산 16건 · 승률 62% · 합산 PnL -6,882원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #192 · 거래 1건 · 승률 100.0% · PF 9999.00 · PnL 12,608원 +- 승률이 비정상적으로 높고 표본이 작습니다 → **해당 장에만 맞는 과적합** 신호. + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-24 09:29:01 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +## 이전 장에서는 + +지난 Optuna 백테스트는 7거래일의 짧은 기간 동안 진행되었으며, 최고 조합이 단 1건의 거래로 100% 승률과 높은 수익을 기록했습니다. 이는 과적합 및 표본 부족의 명확한 신호입니다. 실매에서는 백테스트와 다른 양상으로 손실이 발생했음도 반드시 인지해야 합니다. + +## 앞으로 장에서는 + +이러한 결과는 과적합 및 표본 부족으로 인한 우연한 승률일 가능성이 큽니다. 따라서 해당 조합을 실매에 즉시 적용하는 것은 금지합니다. 최소 5일 이상의 다일 재검증과 웹백테스트를 거쳐야 하며, 백테스트와 실매 간의 괴리(고스트퍼지, 슬리피지 등) 가능성을 항상 경고합니다. 특정 종목 매수 추천이나 확정 수익을 약속할 수 없으니, 소액 모의 투자로 충분히 관찰 후 신중하게 접근하세요. diff --git a/kis_trader/backtest/results/optuna_breakout_tpe_20260824_094416.briefing.md b/kis_trader/backtest/results/optuna_breakout_tpe_20260824_094416.briefing.md new file mode 100644 index 0000000..c27c7f5 --- /dev/null +++ b/kis_trader/backtest/results/optuna_breakout_tpe_20260824_094416.briefing.md @@ -0,0 +1,50 @@ +# Optuna 브리핑 — 돌파 (tpe) +기간: 2026-08-18 ~ 2026-08-24 (거래일≈7일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥1 | 후보 게이트: WR≥40% PF≥1 · min_trades≥1 +- min_trades=1: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 200건 · 사후합격(results_gated) 93건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 43.0%** · **적용 가능도 57.0%** · 판정: **주의** — 적용 가능도 중간 — 다일(≥5일) 재검증·웹백테 후 소액만 + - 표본 장일: +0.0p — 거래일≈7일 — 표본 일수 상대적 양호 + - 거래 표본: +18.0p — 상위 후보 거래 2건 — 표본 부족 + - 승률·PF 이상치: +25.0p — 승률 100.0% + 거래 2건 · PF 9999.00 (소수 거래 폭증) + - 게이트 커버: +0.0p — 사후합격 93건 · stable 10건 + - PnL 고원: +0.0p — 고원 비율 1% (1건) +- 임계값 분포 표본: results_gated 상위 30건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `max_daily_chg` | 24.0 | 22.0 ~ 26.75 | 26.0 (17%) | +| `vol_mult` | 3.2 | 2.725 ~ 3.5 | 3.5 (13%) | +| `vol_window` | 14.0 | 13.0 ~ 14.0 | 14 (43%) | +| `min_turnover_1m_pct` | 0.175 | 0.0625 ~ 0.2 | 0.05 (27%) | +| `prev_chg_min` | 0.9 | 0.9 ~ 1.0 | 0.9 (57%) | +| `prev_chg_max` | 15.5 | 12.5 ~ 25.0 | 13.0 (10%) | +| `min_price` | 4000.0 | 3500.0 ~ 5000.0 | 5000 (30%) | +| `tp_pct` | 13.25 | 10.75 ~ 15.5 | 13.5 (10%) | +| `sl_mode` | — | — | atr (100%) | +| `trail_pct` | 5.3 | 4.7 ~ 5.675 | 5.6 (13%) | +| `trail_arm_pct` | 3.8 | 2.95 ~ 4.425 | 3.8 (13%) | +| `shoulder_min_high_pct` | 4.25 | 3.325 ~ 5.7 | 5.9 (10%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 이전 장에서는 +- 실매 동기간 청산 18건 · 승률 61% · 합산 PnL -16,563원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #136 · 거래 2건 · 승률 100.0% · PF 9999.00 · PnL 33,422원 +- 승률이 비정상적으로 높고 표본이 작습니다 → **해당 장에만 맞는 과적합** 신호. + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-24 09:44:24 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +### 이전 장에서는 +이전 Optuna 백테스트에서 #136 조합이 2건 거래로 승률 100% 등 높은 성과를 보였습니다. 이는 짧은 기간, **표본 부족**으로 인한 **과적합** 신호가 매우 강합니다. 실제 매매에서는 18건 청산에도 합산 PnL이 마이너스였으며, 이는 **백테스트와 실매 간의 괴리**가 큼을 보여줍니다. + +### 앞으로 장에서는 +**과적합 위험**이 높으니, Optuna 상위 조합을 바로 실매에 적용 말고 **다일(5일 이상) 재검증**을 꼭 거치세요. 높은 승률보다 시장 변화 시 **손절매 작동**을 먼저 점검해야 합니다. **실매↔백테 괴리(고스트 슬리피지 등)**에 유의하여, 페이퍼나 소액으로 관찰 후 신중히 접근하세요. 확정 수익을 약속할 수 없습니다. diff --git a/kis_trader/backtest/results/optuna_breakout_tpe_20260824_100148.briefing.md b/kis_trader/backtest/results/optuna_breakout_tpe_20260824_100148.briefing.md new file mode 100644 index 0000000..df4fe22 --- /dev/null +++ b/kis_trader/backtest/results/optuna_breakout_tpe_20260824_100148.briefing.md @@ -0,0 +1,52 @@ +# Optuna 브리핑 — 돌파 (tpe) +기간: 2026-08-18 ~ 2026-08-24 (거래일≈7일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥1 | 후보 게이트: WR≥40% PF≥1 · min_trades≥1 +- min_trades=1: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 169건 · 사후합격(results_gated) 118건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 43.0%** · **적용 가능도 57.0%** · 판정: **주의** — 적용 가능도 중간 — 다일(≥5일) 재검증·웹백테 후 소액만 + - 표본 장일: +0.0p — 거래일≈7일 — 표본 일수 상대적 양호 + - 거래 표본: +18.0p — 상위 후보 거래 2건 — 표본 부족 + - 승률·PF 이상치: +25.0p — 승률 100.0% + 거래 2건 · PF 9999.00 (소수 거래 폭증) + - 게이트 커버: +0.0p — 사후합격 118건 · stable 10건 + - PnL 고원: +0.0p — 고원 비율 1% (1건) +- 임계값 분포 표본: results_gated 상위 30건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `max_daily_chg` | 44.0 | 36.0 ~ 48.0 | 36.0 (10%) | +| `vol_mult` | 4.8 | 4.525 ~ 4.9 | 5.0 (20%) | +| `vol_window` | 13.0 | 11.0 ~ 14.75 | 15 (27%) | +| `min_turnover_1m_pct` | 0.15 | 0.1 ~ 0.3 | 0.1 (20%) | +| `prev_chg_min` | 0.3 | 0.2 ~ 0.4 | 0.2 (47%) | +| `prev_chg_max` | 16.75 | 14.25 ~ 21.5 | 15.0 (10%) | +| `min_price` | 3500.0 | 3500.0 ~ 4000.0 | 3500 (43%) | +| `tp_pct` | 12.75 | 10.125 ~ 15.25 | 13.5 (10%) | +| `sl_mode` | — | — | atr (100%) | +| `trail_pct` | 3.35 | 2.175 ~ 4.525 | 3.1 (7%) | +| `trail_arm_pct` | 5.35 | 4.9 ~ 5.7 | 5.5 (10%) | +| `shoulder_min_high_pct` | 4.3 | 3.1 ~ 4.8 | 4.6 (10%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 이전 장에서는 +- 실매 동기간 청산 21건 · 승률 67% · 합산 PnL -11,887원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #93 · 거래 2건 · 승률 100.0% · PF 9999.00 · PnL 7,823원 +- 승률이 비정상적으로 높고 표본이 작습니다 → **해당 장에만 맞는 과적합** 신호. + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-24 10:01:57 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +## 이전 장에서는 + +실제 매매에서는 21건 거래에 67% 승률이었으나 합산 PnL은 -11,887원으로 마이너스였습니다. 이는 백테스트 Optuna 최고 결과(PnL 7,823원, 거래 2건, 승률 100%)와 크게 다른데, **실매와 백테스트 간의 필연적인 괴리(고스트퍼지, 슬리피지 등)를 명확히 보여줍니다.** 특히 Optuna 최고 결과는 단 2건의 거래로 100% 승률을 달성, **표본 부족과 과적합** 위험이 매우 높다는 경고등입니다. + +## 앞으로 장에서는 + +현재 전략은 과적합 위험 43%로 '주의' 단계이며, 적용 가능도는 57% 수준입니다. **이 Optuna 결과를 즉시 실매에 적용하는 것은 위험**합니다. 최소 5일 이상 장기간의 재검증(웹백테스트 등)을 반드시 거쳐야 합니다. 특히, 적은 거래 수에서 나오는 높은 승률과 PF는 특정 기간에만 우연히 맞아떨어진 **과적합일 가능성이 크므로 주의**해야 합니다. 시장이 변할 경우 백테스트 상의 손절 기준이나 트레일링 로직이 제대로 작동하지 않을 수 있으니, **항상 모의투자나 소액으로 먼저 관찰**하며 전략의 견고함을 확인해야 합니다. diff --git a/kis_trader/backtest/results/optuna_breakout_tpe_20260828_064009.briefing.md b/kis_trader/backtest/results/optuna_breakout_tpe_20260828_064009.briefing.md new file mode 100644 index 0000000..c9dc049 --- /dev/null +++ b/kis_trader/backtest/results/optuna_breakout_tpe_20260828_064009.briefing.md @@ -0,0 +1,49 @@ +# Optuna 브리핑 — 돌파 (tpe) +기간: 2026-08-18 ~ 2026-08-28 (거래일≈11일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥36 | 후보 게이트: WR≥40% PF≥1 · min_trades≥36 +- min_trades=36: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 95건 · 사후합격(results_gated) 13건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 0.0%** · **적용 가능도 100.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈11일 — 표본 일수 상대적 양호 + - 거래 표본: +0.0p — 상위 후보 거래 37건 — 상대적 양호 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +0.0p — 사후합격 13건 · stable 10건 + - PnL 고원: +0.0p — 고원 비율 8% (1건) +- 임계값 분포 표본: results_gated 상위 13건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `max_daily_chg` | 37.0 | 35.0 ~ 43.0 | 37.0 (15%) | +| `vol_mult` | 1.3 | 1.1 ~ 1.5 | 1.0 (23%) | +| `vol_window` | 13.0 | 12.0 ~ 14.0 | 13 (31%) | +| `min_turnover_1m_pct` | 0.4 | 0.15 ~ 0.4 | 0.4 (38%) | +| `prev_chg_min` | 0.3 | 0.3 ~ 0.4 | 0.3 (38%) | +| `prev_chg_max` | 17.5 | 15.5 ~ 22.0 | 17.0 (15%) | +| `min_price` | 3000.0 | 2000.0 ~ 3500.0 | 3500 (23%) | +| `tp_pct` | 23.0 | 19.5 ~ 23.5 | 23.5 (23%) | +| `sl_mode` | — | — | fixed (100%) | +| `trail_pct` | 1.7 | 1.1 ~ 3.0 | 3.0 (15%) | +| `trail_arm_pct` | 3.4 | 2.3 ~ 4.4 | 4.2 (8%) | +| `shoulder_min_high_pct` | 5.6 | 5.0 ~ 5.7 | 6.0 (23%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 이전 장에서는 +- 실매 동기간 청산 36건 · 승률 56% · 합산 PnL 22,027원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #56 · 거래 37건 · 승률 43.2% · PF 1.42 · PnL 40,308원 + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-28 06:40:27 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +**「이전 장에서는」** +Optuna 백테스트는 11일이라는 매우 짧은 기간(2026-08-18~28) 동안 진행되었고, 사후 합격 기준을 통과한 13개 조합을 확인했습니다. 최상위 백테스트는 PnL 4만원을 기록했으나, 실제 매매는 2만2천원 수준으로 큰 괴리를 보였습니다. 이는 고스트퍼지 현상이나 미체결 등 실매와 백테스트 간에 발생하는 고질적인 차이입니다. 과적합 위험은 휴리스틱상 낮게 평가되었지만, 11일의 짧은 기간과 36~37건의 적은 거래 표본은 여전히 과적합 및 표본 부족의 심각한 위험을 내포합니다. + +**「앞으로 장에서는」** +이 결과는 짧은 표본 기간으로 인한 과적합 위험과 실매-백테스트 괴리(고스트퍼지 등)가 높으므로, 당장 실매에 적용하는 것은 위험합니다. 반드시 최소 5거래일 이상 다일 재검증 및 웹백테스트를 통해 안정성을 확인해야 합니다. 시장 상황 변화 시에는 승률보다는 손절매 기준이 제대로 작동하는지 우선적으로 주시하며 리스크 관리에 집중하세요. 페이퍼/소액 매매로 충분히 관찰 후 신중하게 적용을 검토해야 하며, 특정 종목 매수 추천이나 확정 수익을 약속할 수 없습니다. diff --git a/kis_trader/backtest/results/optuna_breakout_tpe_20260828_071144.briefing.md b/kis_trader/backtest/results/optuna_breakout_tpe_20260828_071144.briefing.md new file mode 100644 index 0000000..89c1f9e --- /dev/null +++ b/kis_trader/backtest/results/optuna_breakout_tpe_20260828_071144.briefing.md @@ -0,0 +1,34 @@ +# Optuna 브리핑 — 돌파 (tpe) +기간: 2026-08-18 ~ 2026-08-28 (거래일≈11일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥36 | 후보 게이트: WR≥40% PF≥1 · min_trades≥36 +- min_trades=36: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 0건 · 사후합격(results_gated) 0건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 33.0%** · **적용 가능도 67.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈11일 — 표본 일수 상대적 양호 + - 거래 표본: +25.0p — 통과 후보 없음 — 적용 불가 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +8.0p — 학습·gated 모두 비어 있음 + - PnL 고원: +0.0p — 고원 판정 스킵 +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 이전 장에서는 +- 실매 동기간 청산 36건 · 승률 56% · 합산 PnL 22,027원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 이 기간 백테에서는 **유의미한 통과 조합이 거의 없었습니다.** 유니버스·틱·손절·시간창을 먼저 점검하세요 (돌파 0건이면 특히). + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 현재 사후합격 0건 → 파라미터 유지(현행 DB) + 유니버스/데이터 점검이 우선입니다. + +_생성: 2026-08-28 07:11:44 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +**이전 장에서는** +지난 11일간의 짧은 백테스트에서 유의미한 전략 조합이 0건 발견되었습니다. 실매에서 PnL이 있었음에도 백테스트가 0건인 것은 백테스트와 실매 간의 괴리(고스트 PnL, 유니버스/틱 경로 차이) 및 표본 부족의 강력한 경고입니다. + +**앞으로 장에서는** +현재 통과 조합이 전무하므로, 전략을 실매에 바로 적용해서는 안 됩니다. 11일은 과적합 위험이 높은 매우 짧은 기간이므로, 최소 5거래일 이상 다일 재검증과 웹 백테스트를 통해 견고함을 확보해야 합니다. 높은 승률보다 손절이 살아있는지 점검하며, 유니버스 및 데이터 점검이 우선입니다. diff --git a/kis_trader/backtest/results/optuna_breakout_tpe_20260828_074844.briefing.md b/kis_trader/backtest/results/optuna_breakout_tpe_20260828_074844.briefing.md new file mode 100644 index 0000000..bfb9f2d --- /dev/null +++ b/kis_trader/backtest/results/optuna_breakout_tpe_20260828_074844.briefing.md @@ -0,0 +1,49 @@ +# Optuna 브리핑 — 돌파 (tpe) +기간: 2026-08-18 ~ 2026-08-28 (거래일≈11일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥36 | 후보 게이트: WR≥40% PF≥1 · min_trades≥36 +- min_trades=36: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 132건 · 사후합격(results_gated) 15건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 0.0%** · **적용 가능도 100.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈11일 — 표본 일수 상대적 양호 + - 거래 표본: +0.0p — 상위 후보 거래 40건 — 상대적 양호 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +0.0p — 사후합격 15건 · stable 4건 + - PnL 고원: +0.0p — 고원 비율 7% (1건) +- 임계값 분포 표본: results_gated 상위 15건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `max_daily_chg` | 43.0 | 26.0 ~ 49.0 | 51.0 (20%) | +| `vol_mult` | 1.7 | 1.25 ~ 1.8 | 1.8 (27%) | +| `vol_window` | 14.0 | 13.0 ~ 14.0 | 14 (53%) | +| `min_turnover_1m_pct` | 0.15 | 0.1 ~ 0.325 | 0.1 (33%) | +| `prev_chg_min` | 0.3 | 0.2 ~ 0.35 | 0.2 (40%) | +| `prev_chg_max` | 27.0 | 21.75 ~ 29.0 | 29.5 (13%) | +| `min_price` | 4000.0 | 4000.0 ~ 4500.0 | 4000 (40%) | +| `tp_pct` | 12.0 | 9.75 ~ 16.75 | 7.5 (13%) | +| `sl_mode` | — | — | atr (100%) | +| `trail_pct` | 3.9 | 2.55 ~ 4.4 | 4.4 (13%) | +| `trail_arm_pct` | 1.5 | 0.85 ~ 2.55 | 1.1 (13%) | +| `shoulder_min_high_pct` | 1.7 | 1.6 ~ 2.1 | 1.6 (20%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 이전 장에서는 +- 실매 동기간 청산 36건 · 승률 56% · 합산 PnL 22,027원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #116 · 거래 40건 · 승률 52.5% · PF 1.35 · PnL 39,271원 + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-28 07:49:03 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +[이전 장에서는] +짧은 11일 백테스트에서 #116 조합(40거래, 승률 52.5%, PF 1.35)이 우수했으나, 동기간 실매는 36거래, 승률 56%로 PnL 22,027원이었습니다. 백테스트와 실매 간의 큰 괴리는 고스트퍼지, 유니버스, 틱 경로 등 다양한 차이로 인해 발생할 수 있음을 명심해야 합니다. + +[앞으로 장에서는] +휴리스틱상 과적합 위험은 낮으나, **겨우 11거래일의 짧은 표본 기간은 심각한 과적합 및 표본 부족 위험을 내포**합니다. Optuna 결과를 절대 즉시 실매에 적용하지 마십시오. 최소 5거래일 이상 다일 재검증과 웹백테스트 후, 소액 페이퍼/모의 거래로 **백테스트-실매 괴리(고스트퍼지 현상 등)를 반드시 확인**해야 합니다. `results_gated` 후보만 신중히 검토하고, 시장 변동 시 손절 기능에 우선하여 관찰하십시오. 특정 종목 추천 및 확정 수익 약속은 불가합니다. diff --git a/kis_trader/backtest/results/optuna_breakout_tpe_20260828_082431.briefing.md b/kis_trader/backtest/results/optuna_breakout_tpe_20260828_082431.briefing.md new file mode 100644 index 0000000..aaa0c7f --- /dev/null +++ b/kis_trader/backtest/results/optuna_breakout_tpe_20260828_082431.briefing.md @@ -0,0 +1,34 @@ +# Optuna 브리핑 — 돌파 (tpe) +기간: 2026-08-18 ~ 2026-08-28 (거래일≈11일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥36 | 후보 게이트: WR≥40% PF≥1 · min_trades≥36 +- min_trades=36: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 0건 · 사후합격(results_gated) 0건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 33.0%** · **적용 가능도 67.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈11일 — 표본 일수 상대적 양호 + - 거래 표본: +25.0p — 통과 후보 없음 — 적용 불가 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +8.0p — 학습·gated 모두 비어 있음 + - PnL 고원: +0.0p — 고원 판정 스킵 +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 이전 장에서는 +- 실매 동기간 청산 36건 · 승률 56% · 합산 PnL 22,027원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 이 기간 백테에서는 **유의미한 통과 조합이 거의 없었습니다.** 유니버스·틱·손절·시간창을 먼저 점검하세요 (돌파 0건이면 특히). + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 현재 사후합격 0건 → 파라미터 유지(현행 DB) + 유니버스/데이터 점검이 우선입니다. + +_생성: 2026-08-28 08:24:32 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +**「이전 장에서는」** +지난 11거래일간 실매는 36건 거래로 56% 승률, 22,027원 수익을 냈지만, 백테스트에서는 **어떤 유효한 전략도 발견하지 못했습니다.** 이는 **실매와 백테스트 간의 괴리(고스트 슬리피지 등)가 크며**, 단기 백테스트는 **과적합 및 표본 부족의 위험이 매우 큼**을 분명히 경고합니다. + +**「앞으로 장에서는」** +현재 사후 합격 전략이 0건이므로, **Optuna 베스트 결과를 즉시 실매 적용하는 것은 절대 금지**입니다. **과적합 및 표본 부족 위험이 높은** 현 상태에서는 유니버스/데이터 점검이 우선이며, 최소 **5거래일 이상 다일 재검증과 웹백테**를 통해 **실매↔백테 괴리**를 확인하고 **손절매가 정상 작동하는지** 면밀히 검토해야 합니다. diff --git a/kis_trader/backtest/results/optuna_breakout_tpe_20260829_215827.briefing.md b/kis_trader/backtest/results/optuna_breakout_tpe_20260829_215827.briefing.md new file mode 100644 index 0000000..cc004f2 --- /dev/null +++ b/kis_trader/backtest/results/optuna_breakout_tpe_20260829_215827.briefing.md @@ -0,0 +1,49 @@ +# Optuna 브리핑 — 돌파 (tpe) +기간: 2026-08-24 ~ 2026-08-28 (거래일≈5일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥10 | 후보 게이트: WR≥40% PF≥1 · min_trades≥10 +- min_trades=10: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 186건 · 사후합격(results_gated) 94건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 0.0%** · **적용 가능도 100.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈5일 — 표본 일수 상대적 양호 + - 거래 표본: +0.0p — 상위 후보 거래 14건 — 상대적 양호 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +0.0p — 사후합격 94건 · stable 10건 + - PnL 고원: +0.0p — 고원 비율 1% (1건) +- 임계값 분포 표본: results_gated 상위 30건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `max_daily_chg` | 46.5 | 42.0 ~ 50.0 | 53.0 (13%) | +| `vol_mult` | 2.75 | 1.65 ~ 3.6 | 3.6 (13%) | +| `vol_window` | 9.0 | 8.0 ~ 10.0 | 9 (27%) | +| `min_turnover_1m_pct` | 0.15 | 0.1125 ~ 0.325 | 0.15 (30%) | +| `prev_chg_min` | 0.6 | 0.325 ~ 0.675 | 0.6 (30%) | +| `prev_chg_max` | 18.5 | 10.625 ~ 24.875 | 18.5 (7%) | +| `min_price` | 4000.0 | 3125.0 ~ 5000.0 | 5000 (30%) | +| `tp_pct` | 9.75 | 6.625 ~ 13.625 | 11.5 (13%) | +| `sl_mode` | — | — | fixed (100%) | +| `trail_pct` | 2.55 | 1.85 ~ 3.625 | 4.5 (13%) | +| `trail_arm_pct` | 4.9 | 4.125 ~ 5.3 | 5.3 (13%) | +| `shoulder_min_high_pct` | 3.7 | 2.9 ~ 4.875 | 2.9 (10%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 이전 장에서는 +- 실매 동기간 청산 27건 · 승률 59% · 합산 PnL 24,386원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #191 · 거래 14건 · 승률 78.6% · PF 5.07 · PnL 83,775원 + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-29 21:58:39 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +### 이전 장에서는 +지난 5일 백테스트에서 Optuna는 우수한 돌파 전략 조합(최고 PnL 83,775원)을 찾았으나, 실제 매매 PnL은 24,386원으로 큰 괴리가 나타났습니다. 이는 고스트 슬리피지, 데이터 차이 등으로 늘 발생하는 백테-실매 괴리이며, 표본 부족 및 과적합 위험이 높음을 경고합니다. 짧은 5일 데이터로 얻은 결과는 극히 제한적입니다. + +### 앞으로 장에서는 +따라서 최적 파라미터를 바로 실매매에 적용하기보다, 다일 재검증 후 소액/모의 투자를 통해 1~2일간 실전 괴리를 관찰하세요. 시장 변화 시 손절 등 리스크 관리가 수익률보다 중요합니다. 특정 종목 추천이나 확정 수익은 불가하며, 모든 투자는 본인의 판단과 책임 하에 이루어져야 합니다. diff --git a/kis_trader/backtest/results/optuna_breakout_tpe_20260829_222020.briefing.md b/kis_trader/backtest/results/optuna_breakout_tpe_20260829_222020.briefing.md new file mode 100644 index 0000000..c97a8b4 --- /dev/null +++ b/kis_trader/backtest/results/optuna_breakout_tpe_20260829_222020.briefing.md @@ -0,0 +1,72 @@ +# Optuna 브리핑 — 돌파 (tpe) +기간: 2026-08-24 ~ 2026-08-28 (거래일≈5일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥10 | 후보 게이트: WR≥40% PF≥1 · min_trades≥10 +- min_trades=10: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 15건 · 사후합격(results_gated) 2건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 0.0%** · **적용 가능도 100.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈5일 — 표본 일수 상대적 양호 + - 거래 표본: +0.0p — 상위 후보 거래 19건 — 상대적 양호 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +0.0p — 사후합격 2건 · stable 2건 + - PnL 고원: +0.0p — 고원 비율 50% (1건) +- 임계값 분포 표본: results_gated 상위 2건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `max_daily_chg` | 43.5 | 39.25 ~ 47.75 | 35.0 (50%) | +| `vol_mult` | 1.4 | 1.3 ~ 1.5 | 1.2 (50%) | +| `vol_window` | 4.5 | 3.75 ~ 5.25 | 6 (50%) | +| `min_turnover_1m_pct` | 0.35 | 0.3 ~ 0.4 | 0.25 (50%) | +| `prev_chg_min` | 0.3 | 0.3 ~ 0.3 | 0.3 (100%) | +| `prev_chg_max` | 15.5 | 11.75 ~ 19.25 | 8.0 (50%) | +| `min_price` | 1500.0 | 1500.0 ~ 1500.0 | 1500 (100%) | +| `tp_pct` | 6.0 | 3.5 ~ 8.5 | 11.0 (50%) | +| `sl_mode` | — | — | fixed (100%) | +| `trail_pct` | 4.5 | 4.15 ~ 4.85 | 3.8 (50%) | +| `trail_arm_pct` | 0.9 | 0.55 ~ 1.25 | 0.2 (50%) | +| `shoulder_min_high_pct` | 4.95 | 4.925 ~ 4.975 | 4.9 (50%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 최종 선택 후보 (규칙 · DB 적용 전 확인) +- 아래는 **역할이 다른** 4종 후보입니다. mode Top10 1위 ≠ 익절 대표 · **DB 1순위는 사후합격(gated)** 입니다. +- mode pool: `positive` · scoring: `band_proximity_p25_p75` + +### 후보 4종 +- **① 사후합격 1위 (apply: gated)** (trial #197): PnL 15,857원 · WR 42.1% · PF 1.30 · 거래 19건 · 안정점수 -10882 · 익절11.0% 손절3.3% +- **② 안정 1위 (apply: stable)** (trial #178): PnL 10,394원 · WR 80.0% · PF 1.43 · 거래 15건 · 안정점수 -3346 · 익절1.0% 손절4.8% +- **③ mode_combo (apply: mode)** (trial 없음(조립)): PnL -41,571원 · WR 21.4% · PF 0.58 · 거래 14건 · 안정점수 -33601 · 익절11.0% 손절3.3% · tp 최빈 11.0% (1/4) +- **④ mode Top10 1위 (apply: consensus · 밴드 전형 trial)** (trial #194): PnL 13,617원 · WR 23.1% · PF 1.37 · 거래 13건 · 안정점수 -15997 · 근접 88.2% (20/30축 밴드안) · 익절9.5% 손절1.4% + +- mode pool tp_pct 밴드 p25~p75: **7.375~12.5%** · Top10 1위 tp=9.5% → 밴드 **안** +- mode Top10 meta: pool 4건 · 축 30개 + +### 추천 (자동 · confirm 필수) +1. **실매 DB 적용 1순위 → ① 사후합격 1위** (`optunaApply gated`) — WR/PF·min_trades 사후 통과. +2. **변동성·손실일 줄이기 → ② 안정 1위** (`stable`) — PnL보다 일별 안정 우선 시. +3. **2차 narrow·대표 숫자 → ③ mode_combo** — 축별 최빈 조립 · trial 번호 없음 · 익절은 pool 최빈값 참고. +4. **④ mode Top10 1위** — 양수 pool 전형 trial(다축 밴드 근접). **익절 하나로 쓰지 말 것** · 표에서 「보기」→ consensus apply. + +## 이전 장에서는 +- 실매 동기간 청산 27건 · 승률 59% · 합산 PnL 24,386원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #197 · 거래 19건 · 승률 42.1% · PF 1.30 · PnL 15,857원 + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-30 00:56:04 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +## 이전 장에서는 + +Optuna 백테스트는 5거래일이라는 짧은 기간 동안 사후합격(gated) 기준을 통과한 전략이 2건에 불과하며, 그 중 1위(trial #197)는 승률 42.1%, PnL 15,857원을 기록했습니다. 하지만 동일 기간 실매 청산은 승률 59%, PnL 24,386원으로, 백테스트 결과와 실제 거래 간에 상당한 괴리가 있었음을 확인했습니다. 이는 실매 환경의 다양한 요인(고스트퍼지, 유니버스, 틱 경로 등)이 백테스트와 다르게 작용했음을 시사합니다. + +## 앞으로 장에서는 + +**5거래일이라는 짧은 백테스트 기간은 표본 부족과 과적합의 위험이 매우 높습니다.** Optuna 휴리스틱상 위험이 낮다고 판단되었더라도, 이는 제한적인 지표일 뿐 실제 시장 적용 가능성을 과신해서는 안 됩니다. + +따라서, **단기 Optuna 최적 결과를 바로 실매에 적용하는 것은 위험합니다.** 사후합격(gated) 후보군 내에서도 `① 사후합격 1위` 전략을 포함하여 최소 5거래일 이상의 충분한 다일(多日) 재검증과 웹백테스트를 거쳐야 합니다. 또한, PnL이 마이너스인 `③ mode_combo`나 승률이 낮은 `④ mode Top10 1위`를 익절 대표로 단정하여 활용하는 것은 지양해야 합니다. 백테스트 결과는 미래 수익을 보장하지 않으며, 실매 시 백테스트 PnL과 큰 괴리가 발생할 수 있음을 항상 경고합니다. 초기에는 페이퍼/소액 거래로 시장 적합성을 면밀히 관찰하고, 시장 변동성 증가 시 손절 규칙이 제대로 작동하는지 우선적으로 점검해야 합니다. diff --git a/kis_trader/backtest/results/optuna_breakout_tpe_20260829_224248.briefing.md b/kis_trader/backtest/results/optuna_breakout_tpe_20260829_224248.briefing.md new file mode 100644 index 0000000..9289ddc --- /dev/null +++ b/kis_trader/backtest/results/optuna_breakout_tpe_20260829_224248.briefing.md @@ -0,0 +1,49 @@ +# Optuna 브리핑 — 돌파 (tpe) +기간: 2026-08-24 ~ 2026-08-28 (거래일≈5일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥10 | 후보 게이트: WR≥40% PF≥1 · min_trades≥10 +- min_trades=10: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 188건 · 사후합격(results_gated) 84건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 0.0%** · **적용 가능도 100.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈5일 — 표본 일수 상대적 양호 + - 거래 표본: +0.0p — 상위 후보 거래 14건 — 상대적 양호 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +0.0p — 사후합격 84건 · stable 10건 + - PnL 고원: +0.0p — 고원 비율 1% (1건) +- 임계값 분포 표본: results_gated 상위 30건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `max_daily_chg` | 23.5 | 22.0 ~ 30.5 | 21.0 (17%) | +| `vol_mult` | 1.7 | 1.4 ~ 1.975 | 1.7 (17%) | +| `vol_window` | 11.0 | 9.25 ~ 13.0 | 10 (17%) | +| `min_turnover_1m_pct` | 0.15 | 0.1 ~ 0.2 | 0.1 (23%) | +| `prev_chg_min` | 0.75 | 0.6 ~ 0.8 | 0.6 (27%) | +| `prev_chg_max` | 10.75 | 8.25 ~ 12.5 | 7.5 (13%) | +| `min_price` | 2500.0 | 1500.0 ~ 3500.0 | 3500 (27%) | +| `tp_pct` | 10.75 | 6.0 ~ 14.125 | 5.5 (10%) | +| `sl_mode` | — | — | atr (100%) | +| `trail_pct` | 4.55 | 3.725 ~ 5.1 | 5.3 (10%) | +| `trail_arm_pct` | 4.95 | 4.1 ~ 5.5 | 6.0 (10%) | +| `shoulder_min_high_pct` | 4.65 | 3.65 ~ 5.5 | 5.5 (13%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 이전 장에서는 +- 실매 동기간 청산 27건 · 승률 59% · 합산 PnL 24,386원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #67 · 거래 14건 · 승률 57.1% · PF 2.58 · PnL 56,940원 + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-29 22:43:00 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +**이전 장에서는** +지난 5거래일(2026-08-24~28) Optuna 백테스트는 84개 돌파 전략 후보를 발굴했으며, 1위 후보는 14건 거래에서 승률 57.1%, PF 2.58로 PnL 56,940원을 기록했습니다. 하지만 실제 매매는 PnL 24,386원으로, 백테스트와 실매 간 고스트, 유니버스 등 필연적 괴리가 있었음을 확인했습니다. + +**앞으로 장에서는** +이처럼 짧은 5일간의 백테스트는 특정 장세에 과적합되었을 위험이 매우 높고, 14건의 거래는 표본 부족의 가능성이 큽니다. 휴리스틱상 위험이 낮더라도 실매와 백테스트 괴리는 언제든 발생할 수 있습니다. 따라서 Optuna 최적 조합을 바로 실매에 적용하지 말고, 반드시 다일(≥5거래일) 재검증 및 웹백테스트 후 페이퍼/소액으로 충분히 관찰하며 리스크 관리(손절)에 집중하십시오. 과거 결과는 미래 수익을 보장하지 않습니다. diff --git a/kis_trader/backtest/results/optuna_breakout_tpe_20260829_230347.briefing.md b/kis_trader/backtest/results/optuna_breakout_tpe_20260829_230347.briefing.md new file mode 100644 index 0000000..c98f9b2 --- /dev/null +++ b/kis_trader/backtest/results/optuna_breakout_tpe_20260829_230347.briefing.md @@ -0,0 +1,34 @@ +# Optuna 브리핑 — 돌파 (tpe) +기간: 2026-08-24 ~ 2026-08-28 (거래일≈5일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥10 | 후보 게이트: WR≥40% PF≥1 · min_trades≥10 +- min_trades=10: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 0건 · 사후합격(results_gated) 0건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 33.0%** · **적용 가능도 67.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈5일 — 표본 일수 상대적 양호 + - 거래 표본: +25.0p — 통과 후보 없음 — 적용 불가 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +8.0p — 학습·gated 모두 비어 있음 + - PnL 고원: +0.0p — 고원 판정 스킵 +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 이전 장에서는 +- 실매 동기간 청산 27건 · 승률 59% · 합산 PnL 24,386원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 이 기간 백테에서는 **유의미한 통과 조합이 거의 없었습니다.** 유니버스·틱·손절·시간창을 먼저 점검하세요 (돌파 0건이면 특히). + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 현재 사후합격 0건 → 파라미터 유지(현행 DB) + 유니버스/데이터 점검이 우선입니다. + +_생성: 2026-08-29 23:03:47 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +**「이전 장에서는」** +지난 5거래일 Optuna 백테스트는 유효 조합을 0건 발견했습니다. 실매는 소폭 수익이었지만, 백테스트가 실패한 점은 '백테스트-실매 괴리(고스트 PnL)'와 최적화의 표본 부족을 명확히 보여줍니다. 짧은 기간 백테스트는 과적합 위험이 매우 높으니 유의해야 합니다. + +**「앞으로 장에서는」** +사후합격 조합이 0건이므로 어떤 파라미터도 실매에 즉시 적용 불가능합니다. 과적합과 표본 부족 위험이 매우 높기 때문입니다. 유니버스, 틱 데이터, 손절 기준, 시간창 등을 먼저 면밀히 점검하고, 최소 5거래일 이상 다일 백테스트로 전략 견고성을 재확인해야 합니다. 승률보다 '손절 기준'이 현재 시장에 적합한지 우선 점검하십시오. diff --git a/kis_trader/backtest/results/optuna_breakout_tpe_20260830_001144.briefing.md b/kis_trader/backtest/results/optuna_breakout_tpe_20260830_001144.briefing.md new file mode 100644 index 0000000..2cc6463 --- /dev/null +++ b/kis_trader/backtest/results/optuna_breakout_tpe_20260830_001144.briefing.md @@ -0,0 +1,64 @@ +# Optuna 브리핑 — 돌파 (tpe) +기간: 2026-08-18 ~ 2026-08-28 (거래일≈11일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥18 | 후보 게이트: WR≥40% PF≥1 · min_trades≥18 +- min_trades=18: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 127건 · 사후합격(results_gated) 63건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 0.0%** · **적용 가능도 100.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈11일 — 표본 일수 상대적 양호 + - 거래 표본: +0.0p — 상위 후보 거래 19건 — 상대적 양호 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +0.0p — 사후합격 63건 · stable 10건 + - PnL 고원: +0.0p — 고원 비율 2% (1건) +- 임계값 분포 표본: results_gated 상위 30건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `max_daily_chg` | 27.0 | 23.25 ~ 30.75 | 27.0 (13%) | +| `vol_mult` | 4.3 | 4.125 ~ 4.5 | 4.2 (17%) | +| `vol_window` | 12.0 | 11.0 ~ 14.0 | 11 (23%) | +| `min_turnover_1m_pct` | 0.25 | 0.1 ~ 0.3 | 0.25 (20%) | +| `prev_chg_min` | 0.5 | 0.4 ~ 0.6 | 0.6 (37%) | +| `prev_chg_max` | 22.5 | 19.625 ~ 24.875 | 18.0 (10%) | +| `min_price` | 3000.0 | 2000.0 ~ 4000.0 | 4500 (17%) | +| `tp_pct` | 5.5 | 4.5 ~ 7.5 | 7.5 (13%) | +| `sl_mode` | — | — | fixed (100%) | +| `trail_pct` | 4.2 | 3.625 ~ 4.9 | 4.7 (7%) | +| `trail_arm_pct` | 2.3 | 1.625 ~ 3.325 | 1.1 (7%) | +| `shoulder_min_high_pct` | 4.6 | 3.5 ~ 5.1 | 4.9 (10%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 최종 선택 후보 (규칙 · DB 적용 전 확인) +- 아래는 **역할이 다른** 4종 후보입니다. mode Top10 1위 ≠ 익절 대표 · **DB 1순위는 사후합격(gated)** 입니다. +- mode pool: `positive` · scoring: `band_proximity_p25_p75` + +### 후보 4종 +- **① 사후합격 1위 (apply: gated)** (trial #38): PnL 122,553원 · WR 63.2% · PF 4.84 · 거래 19건 · 안정점수 8673 · 익절6.0% 손절4.2% +- **② 안정 1위 (apply: stable)** (trial #147): PnL 76,230원 · WR 68.4% · PF 2.64 · 거래 19건 · 안정점수 13438 · 익절5.0% 손절4.0% +- **③ mode_combo (apply: mode)** (trial 없음(조립)): PnL 20,553원 · WR 50.0% · PF 1.25 · 거래 22건 · 안정점수 -3724 · 익절5.0% 손절3.6% · tp 최빈 5.0% (7/68) +- **④ mode Top10 1위 (apply: consensus · 밴드 전형 trial)** (trial #72): PnL 68,682원 · WR 65.4% · PF 2.45 · 거래 26건 · 안정점수 -388 · 근접 98.4% (25/27축 밴드안) · 익절2.5% 손절4.6% + +- mode pool tp_pct 밴드 p25~p75: **4.5~9.625%** · Top10 1위 tp=2.5% → 밴드 **밖**(다른 축 보정으로 근접% 높음) +- mode Top10 meta: pool 68건 · 축 27개 + +### 추천 (자동 · confirm 필수) +1. **실매 DB 적용 1순위 → ① 사후합격 1위** (`optunaApply gated`) — WR/PF·min_trades 사후 통과. +2. **변동성·손실일 줄이기 → ② 안정 1위** (`stable`) — PnL보다 일별 안정 우선 시. +3. **2차 narrow·대표 숫자 → ③ mode_combo** — 축별 최빈 조립 · trial 번호 없음 · 익절은 pool 최빈값 참고. +4. **④ mode Top10 1위** — 양수 pool 전형 trial(다축 밴드 근접). **익절 하나로 쓰지 말 것** · 표에서 「보기」→ consensus apply. + +## 이전 장에서는 +- 실매 동기간 청산 36건 · 승률 56% · 합산 PnL 22,027원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #38 · 거래 19건 · 승률 63.2% · PF 4.84 · PnL 122,553원 + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-30 00:52:09 (rule)_ + +## AI 코멘트 + +_(미생성: api_call_failed. 구글 키는 GEMINI_API_KEY 를 DB/환경에 넣으세요 — kis_trader/backtest/optuna_briefing.py 모듈 docstring 참고)_ diff --git a/kis_trader/backtest/results/optuna_breakout_tpe_20260830_004932.briefing.md b/kis_trader/backtest/results/optuna_breakout_tpe_20260830_004932.briefing.md new file mode 100644 index 0000000..3f5f60a --- /dev/null +++ b/kis_trader/backtest/results/optuna_breakout_tpe_20260830_004932.briefing.md @@ -0,0 +1,68 @@ +# Optuna 브리핑 — 돌파 (tpe) +기간: 2026-08-18 ~ 2026-08-28 (거래일≈11일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥18 | 후보 게이트: WR≥40% PF≥1 · min_trades≥18 +- min_trades=18: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 161건 · 사후합격(results_gated) 88건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 0.0%** · **적용 가능도 100.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈11일 — 표본 일수 상대적 양호 + - 거래 표본: +0.0p — 상위 후보 거래 26건 — 상대적 양호 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +0.0p — 사후합격 88건 · stable 10건 + - PnL 고원: +0.0p — 고원 비율 1% (1건) +- 임계값 분포 표본: results_gated 상위 30건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `max_daily_chg` | 44.5 | 39.5 ~ 49.5 | 54.0 (10%) | +| `vol_mult` | 3.2 | 1.975 ~ 3.5 | 3.5 (17%) | +| `vol_window` | 10.0 | 6.25 ~ 13.75 | 14 (17%) | +| `min_turnover_1m_pct` | 0.275 | 0.2 ~ 0.3 | 0.3 (33%) | +| `prev_chg_min` | 0.3 | 0.3 ~ 0.4 | 0.3 (37%) | +| `prev_chg_max` | 18.5 | 13.0 ~ 24.875 | 13.0 (10%) | +| `min_price` | 4000.0 | 2125.0 ~ 5000.0 | 5000 (30%) | +| `tp_pct` | 11.5 | 8.5 ~ 12.875 | 12.0 (17%) | +| `sl_mode` | — | — | fixed (100%) | +| `trail_pct` | 4.4 | 4.0 ~ 5.375 | 4.2 (10%) | +| `trail_arm_pct` | 2.75 | 1.6 ~ 3.65 | 3.4 (10%) | +| `shoulder_min_high_pct` | 3.75 | 2.275 ~ 4.875 | 3.9 (10%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 최종 선택 후보 (규칙 · DB 적용 전 확인) +- 아래는 **역할이 다른** 4종 후보입니다. mode Top10 1위 ≠ 익절 대표 · **DB 1순위는 사후합격(gated)** 입니다. +- mode pool: `positive` · scoring: `band_proximity_p25_p75` + +### 후보 4종 +- **① 사후합격 1위 (apply: gated)** (trial #101): PnL 94,086원 · WR 57.7% · PF 3.08 · 거래 26건 · 안정점수 6254 · 익절21.5% 손절1.2% +- **② 안정 1위 (apply: stable)** (trial #14): PnL 64,170원 · WR 51.9% · PF 1.77 · 거래 27건 · 안정점수 10116 · 익절13.0% 손절3.5% +- **③ mode_combo (apply: mode)** (trial 없음(조립)): PnL 26,391원 · WR 60.6% · PF 1.49 · 거래 33건 · 안정점수 -5033 · 익절13.0% 손절1.2% · tp 최빈 13.0% (9/105) +- **④ mode Top10 1위 (apply: consensus · 밴드 전형 trial)** (trial #94): PnL 12,637원 · WR 33.3% · PF 1.10 · 거래 42건 · 안정점수 -9421 · 근접 97.9% (24/27축 밴드안) · 익절13.0% 손절1.3% + +- mode pool tp_pct 밴드 p25~p75: **8.5~16.5%** · Top10 1위 tp=13% → 밴드 **안** +- mode Top10 meta: pool 105건 · 축 27개 + +### 추천 (자동 · confirm 필수) +1. **실매 DB 적용 1순위 → ① 사후합격 1위** (`optunaApply gated`) — WR/PF·min_trades 사후 통과. +2. **변동성·손실일 줄이기 → ② 안정 1위** (`stable`) — PnL보다 일별 안정 우선 시. +3. **2차 narrow·대표 숫자 → ③ mode_combo** — 축별 최빈 조립 · trial 번호 없음 · 익절은 pool 최빈값 참고. +4. **④ mode Top10 1위** — 양수 pool 전형 trial(다축 밴드 근접). **익절 하나로 쓰지 말 것** · 표에서 「보기」→ consensus apply. + +## 이전 장에서는 +- 실매 동기간 청산 36건 · 승률 56% · 합산 PnL 22,027원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #101 · 거래 26건 · 승률 57.7% · PF 3.08 · PnL 94,086원 + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-30 00:56:06 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +**이전 장에서는** +실매에서 36건 거래, 승률 56%로 22,027원 PnL을 기록했습니다. 백테스트와 실매 간 PnL 괴리는 정상입니다. Optuna 사후합격 1위(#101)는 백테스트에서 26건 거래, 승률 57.7%, PF 3.08로 94,086원의 PnL을 보였습니다. + +**앞으로 장에서는** +11일 백테스트 기간은 짧아 과적합 및 표본 부족 위험이 상존합니다. `mode Top10 1위`를 익절 대표로 단정 마세요. `DB 1순위`는 사후합격 조합입니다. 백테스트 결과는 실매와 고스트 퍼지 등으로 괴리가 크니, `사후합격 1위`라도 최소 5거래일 이상 다일 재검증 및 소액/페이퍼 트레이딩 후 확대하세요. 승률보다 `손절` 작동 확인이 중요하며, 특정 종목 추천이나 확정 수익은 약속할 수 없습니다. diff --git a/kis_trader/backtest/results/optuna_breakout_tpe_20260830_043651.briefing.md b/kis_trader/backtest/results/optuna_breakout_tpe_20260830_043651.briefing.md new file mode 100644 index 0000000..da71dec --- /dev/null +++ b/kis_trader/backtest/results/optuna_breakout_tpe_20260830_043651.briefing.md @@ -0,0 +1,68 @@ +# Optuna 브리핑 — 돌파 (tpe) +기간: 2026-08-24 ~ 2026-08-28 (거래일≈5일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥10 | 후보 게이트: WR≥40% PF≥1 · min_trades≥10 +- min_trades=10: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 151건 · 사후합격(results_gated) 102건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 0.0%** · **적용 가능도 100.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈5일 — 표본 일수 상대적 양호 + - 거래 표본: +0.0p — 상위 후보 거래 11건 — 상대적 양호 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +0.0p — 사후합격 102건 · stable 10건 + - PnL 고원: +0.0p — 고원 비율 1% (1건) +- 임계값 분포 표본: results_gated 상위 30건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `max_daily_chg` | 41.5 | 34.25 ~ 47.0 | 47.0 (13%) | +| `vol_mult` | 4.55 | 4.15 ~ 4.8 | 4.9 (17%) | +| `vol_window` | 14.0 | 11.25 ~ 14.75 | 15 (27%) | +| `min_turnover_1m_pct` | 0.325 | 0.25 ~ 0.45 | 0.45 (27%) | +| `prev_chg_min` | 0.4 | 0.3 ~ 0.5 | 0.4 (37%) | +| `prev_chg_max` | 21.5 | 18.0 ~ 25.75 | 27.5 (10%) | +| `min_price` | 3750.0 | 2500.0 ~ 4500.0 | 4500 (23%) | +| `tp_pct` | 6.0 | 4.5 ~ 7.875 | 6.0 (17%) | +| `sl_mode` | — | — | fixed (100%) | +| `trail_pct` | 0.55 | 0.125 ~ 1.05 | 0.0 (13%) | +| `trail_arm_pct` | 4.95 | 4.625 ~ 5.375 | 5.3 (10%) | +| `shoulder_min_high_pct` | 4.55 | 3.65 ~ 5.175 | 5.1 (10%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 최종 선택 후보 (규칙 · DB 적용 전 확인) +- 아래는 **역할이 다른** 4종 후보입니다. mode Top10 1위 ≠ 익절 대표 · **DB 1순위는 사후합격(gated)** 입니다. +- mode pool: `positive` · scoring: `band_proximity_p25_p75` + +### 후보 4종 +- **① 사후합격 1위 (apply: gated)** (trial #55): PnL 77,192원 · WR 72.7% · PF 21.73 · 거래 11건 · 안정점수 2852 · 익절12.0% 손절4.2% +- **② 안정 1위 (apply: stable)** (trial #117): PnL 74,968원 · WR 84.6% · PF 12.02 · 거래 13건 · 안정점수 12308 · 익절2.5% 손절6.1% +- **③ mode_combo (apply: mode)** (trial 없음(조립)): PnL 36,900원 · WR 52.6% · PF 1.68 · 거래 19건 · 안정점수 -928 · 익절6.0% 손절6.1% · tp 최빈 6.0% (10/105) +- **④ mode Top10 1위 (apply: consensus · 밴드 전형 trial)** (trial #73): PnL 71,400원 · WR 68.8% · PF 3.06 · 거래 16건 · 안정점수 1554 · 근접 96.5% (22/27축 밴드안) · 익절7.0% 손절5.8% + +- mode pool tp_pct 밴드 p25~p75: **4.5~9.5%** · Top10 1위 tp=7% → 밴드 **안** +- mode Top10 meta: pool 105건 · 축 27개 + +### 추천 (자동 · confirm 필수) +1. **실매 DB 적용 1순위 → ① 사후합격 1위** (`optunaApply gated`) — WR/PF·min_trades 사후 통과. +2. **변동성·손실일 줄이기 → ② 안정 1위** (`stable`) — PnL보다 일별 안정 우선 시. +3. **2차 narrow·대표 숫자 → ③ mode_combo** — 축별 최빈 조립 · trial 번호 없음 · 익절은 pool 최빈값 참고. +4. **④ mode Top10 1위** — 양수 pool 전형 trial(다축 밴드 근접). **익절 하나로 쓰지 말 것** · 표에서 「보기」→ consensus apply. + +## 이전 장에서는 +- 실매 동기간 청산 27건 · 승률 59% · 합산 PnL 24,386원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #55 · 거래 11건 · 승률 72.7% · PF 21.73 · PnL 77,192원 + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-30 04:37:03 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +**이전 장에서는** +지난 5거래일(2026-08-24~28)의 짧은 Optuna TPE 백테스트에서 사후합격(gated) 1위는 PnL 77,192원, 승률 72.7%로 우수한 결과였습니다. 하지만 같은 기간 실매 청산은 PnL 24,386원, 승률 59%로 큰 차이를 보였습니다. 이는 실매와 백테스트 간 유니버스, 틱 경로, 고스트퍼지 등 불가피한 괴리가 항상 존재함을 명확히 보여줍니다. + +**앞으로 장에서는** +단 5거래일의 결과는 과적합 위험이 상존하며 표본 부족으로 우연적 승률일 가능성이 높습니다. 백테스트의 높은 PnL이나 승률이 실제 시장에서 재현될 것이라 단정할 수 없습니다. Optuna 결과를 바로 실매에 적용하기보다, 반드시 `results_gated` 후보군 중에서도 페이퍼/소액 거래로 최소 1~2일간 관찰하며 손절 라인이 유효한지 면밀히 확인 후 적용 규모를 확대해야 합니다. 특정 종목 매수 추천이나 확정 수익 약속은 불가합니다. diff --git a/kis_trader/backtest/results/optuna_breakout_tpe_20260830_045843.briefing.md b/kis_trader/backtest/results/optuna_breakout_tpe_20260830_045843.briefing.md new file mode 100644 index 0000000..22544a3 --- /dev/null +++ b/kis_trader/backtest/results/optuna_breakout_tpe_20260830_045843.briefing.md @@ -0,0 +1,70 @@ +# Optuna 브리핑 — 돌파 (tpe) +기간: 2026-08-24 ~ 2026-08-28 (거래일≈5일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥10 | 후보 게이트: WR≥40% PF≥1 · min_trades≥10 +- min_trades=10: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 167건 · 사후합격(results_gated) 87건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 0.0%** · **적용 가능도 100.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈5일 — 표본 일수 상대적 양호 + - 거래 표본: +0.0p — 상위 후보 거래 10건 — 상대적 양호 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +0.0p — 사후합격 87건 · stable 10건 + - PnL 고원: +0.0p — 고원 비율 1% (1건) +- 임계값 분포 표본: results_gated 상위 30건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `max_daily_chg` | 51.0 | 46.75 ~ 53.0 | 51.0 (20%) | +| `vol_mult` | 4.15 | 3.825 ~ 4.625 | 5.0 (13%) | +| `vol_window` | 11.0 | 9.25 ~ 12.0 | 10 (20%) | +| `min_turnover_1m_pct` | 0.15 | 0.15 ~ 0.2 | 0.15 (37%) | +| `prev_chg_min` | 0.3 | 0.2 ~ 0.475 | 0.2 (37%) | +| `prev_chg_max` | 8.0 | 6.0 ~ 12.0 | 7.0 (13%) | +| `min_price` | 4000.0 | 3125.0 ~ 4500.0 | 5000 (23%) | +| `tp_pct` | 14.75 | 10.625 ~ 20.375 | 23.0 (13%) | +| `sl_mode` | — | — | fixed (100%) | +| `trail_pct` | 2.5 | 1.825 ~ 3.15 | 3.0 (10%) | +| `trail_arm_pct` | 2.25 | 1.8 ~ 3.1 | 3.1 (10%) | +| `shoulder_min_high_pct` | 5.55 | 5.025 ~ 5.8 | 6.0 (20%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 최종 선택 후보 (규칙 · DB 적용 전 확인) +- 아래는 **역할이 다른** 4종 후보입니다. mode Top10 1위 ≠ 익절 대표 · **DB 1순위는 사후합격(gated)** 입니다. +- mode pool: `positive` · scoring: `band_proximity_p25_p75` + +### 후보 4종 +- **① 사후합격 1위 (apply: gated)** (trial #183): PnL 100,802원 · WR 80.0% · PF 17.34 · 거래 10건 · 안정점수 7013 · 익절9.5% 손절6.3% +- **② 안정 1위 (apply: stable)** (trial #168): PnL 66,770원 · WR 48.3% · PF 1.75 · 거래 29건 · 안정점수 7842 · 익절6.5% 손절5.1% +- **③ mode_combo (apply: mode)** (trial 없음(조립)): PnL 43,396원 · WR 61.5% · PF 1.82 · 거래 13건 · 안정점수 -5244 · 익절23.0% 손절5.0% · tp 최빈 23.0% (6/107) +- **④ mode Top10 1위 (apply: consensus · 밴드 전형 trial)** (trial #33): PnL 68,733원 · WR 65.0% · PF 4.61 · 거래 20건 · 안정점수 -9410 · 근접 99.8% (26/27축 밴드안) · 익절12.0% 손절3.8% + +- mode pool tp_pct 밴드 p25~p75: **9.75~20%** · Top10 1위 tp=12% → 밴드 **안** +- mode Top10 meta: pool 107건 · 축 27개 + +### 추천 (자동 · confirm 필수) +1. **실매 DB 적용 1순위 → ① 사후합격 1위** (`optunaApply gated`) — WR/PF·min_trades 사후 통과. +2. **변동성·손실일 줄이기 → ② 안정 1위** (`stable`) — PnL보다 일별 안정 우선 시. +3. **2차 narrow·대표 숫자 → ③ mode_combo** — 축별 최빈 조립 · trial 번호 없음 · 익절은 pool 최빈값 참고. +4. **④ mode Top10 1위** — 양수 pool 전형 trial(다축 밴드 근접). **익절 하나로 쓰지 말 것** · 표에서 「보기」→ consensus apply. + +## 이전 장에서는 +- 실매 동기간 청산 27건 · 승률 59% · 합산 PnL 24,386원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #183 · 거래 10건 · 승률 80.0% · PF 17.34 · PnL 100,802원 + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-30 04:58:54 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +## 이전 장에서는 + +지난주 실제 매매에서는 총 27건 청산, 승률 59%로 24,386원의 수익을 기록했습니다. 이는 백테스트 최고 결과와 차이가 컸지만, 실매와 백테스트 간의 '고스트 퍼지' 같은 괴리는 정상적입니다. 특히, Optuna 사후합격 1위는 짧은 기간(5일) 10건 거래로 80% 승률, 10만 802원이라는 높은 PnL을 보였습니다. 이 결과는 표본 부족에 따른 과적합 가능성이 있으니 주의가 필요합니다. + +## 앞으로 장에서는 + +단기 백테스트 결과(특히 `사후합격 1위`처럼 표본이 적은 경우)는 과적합 및 표본 부족 위험이 큽니다. `mode Top10 1위` 등 어떤 후보도 확정적 수익을 보장하지 않습니다. 실매 적용 전 반드시 페이퍼/소액으로 최소 1~2일 관찰하며, 백테스트와 실제 시장 간의 괴리('고스트 퍼지' 등)를 직접 확인해야 합니다. 시장 상황 변화 시 '손절 규칙'이 제대로 작동하는지 주시하고, 안정성이 검증된 `results_gated` 후보군 내에서 신중하게 접근하시길 바랍니다. diff --git a/kis_trader/backtest/results/optuna_breakout_tpe_20260830_051859.briefing.md b/kis_trader/backtest/results/optuna_breakout_tpe_20260830_051859.briefing.md new file mode 100644 index 0000000..b323cfb --- /dev/null +++ b/kis_trader/backtest/results/optuna_breakout_tpe_20260830_051859.briefing.md @@ -0,0 +1,51 @@ +# Optuna 브리핑 — 돌파 (tpe) +기간: 2026-08-24 ~ 2026-08-28 (거래일≈5일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥10 | 후보 게이트: WR≥40% PF≥1 · min_trades≥10 +- min_trades=10: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 0건 · 사후합격(results_gated) 0건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 33.0%** · **적용 가능도 67.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈5일 — 표본 일수 상대적 양호 + - 거래 표본: +25.0p — 통과 후보 없음 — 적용 불가 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +8.0p — 학습·gated 모두 비어 있음 + - PnL 고원: +0.0p — 고원 판정 스킵 +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 최종 선택 후보 (규칙 · DB 적용 전 확인) +- 아래는 **역할이 다른** 4종 후보입니다. mode Top10 1위 ≠ 익절 대표 · **DB 1순위는 사후합격(gated)** 입니다. +- mode pool: `positive` · scoring: `band_proximity_p25_p75` + +### 후보 4종 +- **① 사후합격 1위 (apply: gated)**: 없음 +- **② 안정 1위 (apply: stable)**: 없음 +- **③ mode_combo (apply: mode)**: 없음 (미산출·구 JSON) +- **④ mode Top10 1위 (apply: consensus · 밴드 전형 trial)**: 없음 +- mode Top10 meta: pool 0건 · 축 0개 + +### 추천 (자동 · confirm 필수) +1. **사후합격 0건 → DB 적용 비권장.** 현행 DB 유지 + 유니버스·틱·기간 재검증. +- ⚠ 안정 게이트 0건 → 안정 Top은 **점수순 폴백** (안정 게이트 0건 → 학습풀 안정점수순 TopN (마이너스 PnL 포함 · 상대비교)) + +## 이전 장에서는 +- 실매 동기간 청산 27건 · 승률 59% · 합산 PnL 24,386원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 이 기간 백테에서는 **유의미한 통과 조합이 거의 없었습니다.** 유니버스·틱·손절·시간창을 먼저 점검하세요 (돌파 0건이면 특히). + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 현재 사후합격 0건 → 파라미터 유지(현행 DB) + 유니버스/데이터 점검이 우선입니다. + +_생성: 2026-08-30 05:18:59 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +## 이전 장에서는 + +지난 5거래일(2026-08-24~28) Optuna 백테스트에서 **유효한 전략 조합이 전혀 도출되지 않았습니다.** 실매 수익이 있었더라도, 이는 백테스트와 **과적합·표본 부족, 실매 괴리(고스트퍼지 등)** 등으로 다를 수 있음을 명심해야 합니다. **DB 1순위는 사후합격(gated)이며 mode Top10 1위는 익절 대표가 아닙니다.** + +## 앞으로 장에서는 + +단 5일간의 결과는 **과적합 위험이 높습니다.** 현재 사후합격(gated)된 전략이 없으므로, **현행 DB를 유지하며 유니버스·데이터 재점검**이 우선입니다. **Optuna best를 바로 실매에 적용하지 말고, 다일(≥5거래일) 교차 검증 후 `results_gated` 후보만 검토하세요.** **확정 수익 약속 및 특정 종목 매수 추천은 금지됩니다.** 손절이 살아있는지 확인하는 것이 핵심입니다. diff --git a/kis_trader/backtest/results/optuna_breakout_tpe_20260830_053932.briefing.md b/kis_trader/backtest/results/optuna_breakout_tpe_20260830_053932.briefing.md new file mode 100644 index 0000000..cbbbdf7 --- /dev/null +++ b/kis_trader/backtest/results/optuna_breakout_tpe_20260830_053932.briefing.md @@ -0,0 +1,49 @@ +# Optuna 브리핑 — 돌파 (tpe) +기간: 2026-08-24 ~ 2026-08-28 (거래일≈5일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥10 | 후보 게이트: WR≥40% PF≥1 · min_trades≥10 +- min_trades=10: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 0건 · 사후합격(results_gated) 0건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 33.0%** · **적용 가능도 67.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈5일 — 표본 일수 상대적 양호 + - 거래 표본: +25.0p — 통과 후보 없음 — 적용 불가 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +8.0p — 학습·gated 모두 비어 있음 + - PnL 고원: +0.0p — 고원 판정 스킵 +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 최종 선택 후보 (규칙 · DB 적용 전 확인) +- 아래는 **역할이 다른** 4종 후보입니다. mode Top10 1위 ≠ 익절 대표 · **DB 1순위는 사후합격(gated)** 입니다. +- mode pool: `positive` · scoring: `band_proximity_p25_p75` + +### 후보 4종 +- **① 사후합격 1위 (apply: gated)**: 없음 +- **② 안정 1위 (apply: stable)**: 없음 +- **③ mode_combo (apply: mode)**: 없음 (미산출·구 JSON) +- **④ mode Top10 1위 (apply: consensus · 밴드 전형 trial)**: 없음 +- mode Top10 meta: pool 0건 · 축 0개 + +### 추천 (자동 · confirm 필수) +1. **사후합격 0건 → DB 적용 비권장.** 현행 DB 유지 + 유니버스·틱·기간 재검증. +- ⚠ 안정 게이트 0건 → 안정 Top은 **점수순 폴백** (안정 게이트 0건 → 학습풀 안정점수순 TopN (마이너스 PnL 포함 · 상대비교)) + +## 이전 장에서는 +- 실매 동기간 청산 27건 · 승률 59% · 합산 PnL 24,386원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 이 기간 백테에서는 **유의미한 통과 조합이 거의 없었습니다.** 유니버스·틱·손절·시간창을 먼저 점검하세요 (돌파 0건이면 특히). + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 현재 사후합격 0건 → 파라미터 유지(현행 DB) + 유니버스/데이터 점검이 우선입니다. + +_생성: 2026-08-30 05:39:32 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +## 이전 장에서는 +지난 실매는 소폭 수익이었으나, 이번 Optuna 백테스트는 **유효한 전략 조합을 전혀 찾지 못했습니다.** 이는 **표본 부족과 과적합** 위험을 경고하며, 백테스트와 실매 간 **괴리(고스트퍼지 등)를 항상 유념**해야 합니다. 모델 유니버스·틱·손절 등 파라미터 점검이 시급합니다. + +## 앞으로 장에서는 +**사후합격(gated) 0건**으로 현재는 어떠한 조합도 실매 적용을 **절대 금지**합니다. `mode Top10 1위`는 익절 대표가 아닌 단순 후보일 뿐, **DB 1순위는 사후합격된 것**이어야 합니다. **과적합 위험이 낮더라도 표본 부족으로 백테-실매 괴리는 언제든 발생**할 수 있습니다. 확정 수익 약속은 불가하며, 유니버스·데이터 재점검이 최우선입니다. diff --git a/kis_trader/backtest/results/optuna_breakout_tpe_20260830_060128.briefing.md b/kis_trader/backtest/results/optuna_breakout_tpe_20260830_060128.briefing.md new file mode 100644 index 0000000..4c20cbd --- /dev/null +++ b/kis_trader/backtest/results/optuna_breakout_tpe_20260830_060128.briefing.md @@ -0,0 +1,68 @@ +# Optuna 브리핑 — 돌파 (tpe) +기간: 2026-08-24 ~ 2026-08-28 (거래일≈5일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥10 | 후보 게이트: WR≥40% PF≥1 · min_trades≥10 +- min_trades=10: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 166건 · 사후합격(results_gated) 88건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 0.0%** · **적용 가능도 100.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈5일 — 표본 일수 상대적 양호 + - 거래 표본: +0.0p — 상위 후보 거래 13건 — 상대적 양호 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +0.0p — 사후합격 88건 · stable 10건 + - PnL 고원: +0.0p — 고원 비율 1% (1건) +- 임계값 분포 표본: results_gated 상위 30건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `max_daily_chg` | 36.0 | 31.25 ~ 40.0 | 36.0 (13%) | +| `vol_mult` | 4.35 | 4.1 ~ 4.7 | 4.9 (10%) | +| `vol_window` | 5.5 | 4.0 ~ 7.75 | 8 (20%) | +| `min_turnover_1m_pct` | 0.45 | 0.35 ~ 0.45 | 0.45 (37%) | +| `prev_chg_min` | 0.3 | 0.3 ~ 0.4 | 0.3 (37%) | +| `prev_chg_max` | 14.75 | 11.125 ~ 19.375 | 11.5 (7%) | +| `min_price` | 4000.0 | 3500.0 ~ 4375.0 | 4000 (33%) | +| `tp_pct` | 9.25 | 4.625 ~ 11.5 | 11.5 (17%) | +| `sl_mode` | — | — | atr (100%) | +| `trail_pct` | 5.1 | 4.525 ~ 5.275 | 5.2 (17%) | +| `trail_arm_pct` | 3.2 | 1.825 ~ 4.675 | 3.1 (7%) | +| `shoulder_min_high_pct` | 2.25 | 1.825 ~ 3.3 | 2.2 (13%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 최종 선택 후보 (규칙 · DB 적용 전 확인) +- 아래는 **역할이 다른** 4종 후보입니다. mode Top10 1위 ≠ 익절 대표 · **DB 1순위는 사후합격(gated)** 입니다. +- mode pool: `positive` · scoring: `band_proximity_p25_p75` + +### 후보 4종 +- **① 사후합격 1위 (apply: gated)** (trial #72): PnL 69,576원 · WR 69.2% · PF 5.14 · 거래 13건 · 안정점수 3523 · 익절9.0% +- **② 안정 1위 (apply: stable)** (trial #195): PnL 37,005원 · WR 63.6% · PF 4.89 · 거래 11건 · 안정점수 8940 · 익절3.0% +- **③ mode_combo (apply: mode)** (trial 없음(조립)): PnL 44,809원 · WR 71.4% · PF 2.62 · 거래 21건 · 안정점수 3747 · 익절11.5% · tp 최빈 11.5% (7/109) +- **④ mode Top10 1위 (apply: consensus · 밴드 전형 trial)** (trial #2): PnL 25,921원 · WR 63.2% · PF 1.82 · 거래 19건 · 안정점수 2264 · 근접 98.0% (24/27축 밴드안) · 익절12.5% + +- mode pool tp_pct 밴드 p25~p75: **6.5~13.5%** · Top10 1위 tp=12.5% → 밴드 **안** +- mode Top10 meta: pool 109건 · 축 27개 + +### 추천 (자동 · confirm 필수) +1. **실매 DB 적용 1순위 → ① 사후합격 1위** (`optunaApply gated`) — WR/PF·min_trades 사후 통과. +2. **변동성·손실일 줄이기 → ② 안정 1위** (`stable`) — PnL보다 일별 안정 우선 시. +3. **2차 narrow·대표 숫자 → ③ mode_combo** — 축별 최빈 조립 · trial 번호 없음 · 익절은 pool 최빈값 참고. +4. **④ mode Top10 1위** — 양수 pool 전형 trial(다축 밴드 근접). **익절 하나로 쓰지 말 것** · 표에서 「보기」→ consensus apply. + +## 이전 장에서는 +- 실매 동기간 청산 27건 · 승률 59% · 합산 PnL 24,386원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #72 · 거래 13건 · 승률 69.2% · PF 5.14 · PnL 69,576원 + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-30 06:01:39 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +## 이전 장에서는 +이번 Optuna TPE 백테스트는 5거래일(2026-08-24 ~ 2026-08-28)간 진행되었고, 200회 탐색 중 88개의 유효 후보가 사후합격했습니다. 그중 ‘사후합격 1위’(trial #72)는 PnL 69,576원, 승률 69.2%로 우수했지만, **실매 결과(PnL 24,386원, 승률 59%)와는 큰 차이가 있었습니다.** 이는 백테스트와 실매 간의 **유니버스, 틱 경로, 호가 공백(고스트퍼지) 등 괴리**로 인해 늘 발생할 수 있는 현상입니다. + +## 앞으로 장에서는 +이번 결과는 **단 5거래일의 짧은 표본**으로, **과적합 및 표본 부족 위험**이 상대적으로 낮다는 휴리스틱 판단에도 불구하고, 실제 시장 환경 변화 시 **백테스트와 실매 간 괴리**는 언제든 발생할 수 있습니다. `mode Top10 1위`를 익절 대표로 단정하거나, `DB 1순위`를 맹신하여 바로 실매에 적용하는 것은 위험합니다. 항상 `사후합격` 후보 중에서도 **다일 재검증**과 **페이퍼/소액 모의 투자**를 통해 1~2일간 관찰 후 적용 여부를 신중히 검토하세요. 시장 변동성이 커질 때는 승률보다 **손절 기능이 정상 작동하는지**를 우선 확인하는 리스크 관리가 중요합니다. 특정 종목 추천이나 확정 수익은 보장하지 않습니다. diff --git a/kis_trader/backtest/results/optuna_breakout_tpe_20260830_062238.briefing.md b/kis_trader/backtest/results/optuna_breakout_tpe_20260830_062238.briefing.md new file mode 100644 index 0000000..e80cdb4 --- /dev/null +++ b/kis_trader/backtest/results/optuna_breakout_tpe_20260830_062238.briefing.md @@ -0,0 +1,68 @@ +# Optuna 브리핑 — 돌파 (tpe) +기간: 2026-08-24 ~ 2026-08-28 (거래일≈5일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥10 | 후보 게이트: WR≥40% PF≥1 · min_trades≥10 +- min_trades=10: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 165건 · 사후합격(results_gated) 54건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 0.0%** · **적용 가능도 100.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈5일 — 표본 일수 상대적 양호 + - 거래 표본: +0.0p — 상위 후보 거래 16건 — 상대적 양호 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +0.0p — 사후합격 54건 · stable 10건 + - PnL 고원: +0.0p — 고원 비율 2% (1건) +- 임계값 분포 표본: results_gated 상위 30건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `max_daily_chg` | 31.5 | 25.25 ~ 43.25 | 31.0 (10%) | +| `vol_mult` | 2.7 | 2.0 ~ 3.85 | 2.0 (13%) | +| `vol_window` | 11.0 | 8.0 ~ 11.75 | 11 (27%) | +| `min_turnover_1m_pct` | 0.4 | 0.2625 ~ 0.45 | 0.4 (23%) | +| `prev_chg_min` | 0.65 | 0.5 ~ 0.8 | 0.5 (23%) | +| `prev_chg_max` | 9.75 | 6.625 ~ 12.875 | 10.0 (10%) | +| `min_price` | 4000.0 | 3500.0 ~ 4500.0 | 3500 (27%) | +| `tp_pct` | 11.0 | 6.625 ~ 12.0 | 12.0 (13%) | +| `sl_mode` | — | — | atr (100%) | +| `trail_pct` | 5.1 | 4.5 ~ 5.475 | 5.3 (10%) | +| `trail_arm_pct` | 4.35 | 2.875 ~ 5.45 | 5.5 (10%) | +| `shoulder_min_high_pct` | 1.9 | 1.65 ~ 2.675 | 1.8 (17%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 최종 선택 후보 (규칙 · DB 적용 전 확인) +- 아래는 **역할이 다른** 4종 후보입니다. mode Top10 1위 ≠ 익절 대표 · **DB 1순위는 사후합격(gated)** 입니다. +- mode pool: `positive` · scoring: `band_proximity_p25_p75` + +### 후보 4종 +- **① 사후합격 1위 (apply: gated)** (trial #98): PnL 46,202원 · WR 43.8% · PF 2.30 · 거래 16건 · 안정점수 -3675 · 익절7.0% +- **② 안정 1위 (apply: stable)** (trial #193): PnL 46,836원 · WR 60.9% · PF 2.08 · 거래 23건 · 안정점수 6112 · 익절2.0% +- **③ mode_combo (apply: mode)** (trial 없음(조립)): PnL 31,593원 · WR 33.3% · PF 1.44 · 거래 24건 · 안정점수 -17331 · 익절12.0% · tp 최빈 12.0% (7/85) +- **④ mode Top10 1위 (apply: consensus · 밴드 전형 trial)** (trial #57): PnL 41,867원 · WR 45.0% · PF 2.01 · 거래 20건 · 안정점수 -18773 · 근접 97.1% (20/27축 밴드안) · 익절6.5% + +- mode pool tp_pct 밴드 p25~p75: **7~13%** · Top10 1위 tp=6.5% → 밴드 **밖**(다른 축 보정으로 근접% 높음) +- mode Top10 meta: pool 85건 · 축 27개 + +### 추천 (자동 · confirm 필수) +1. **실매 DB 적용 1순위 → ① 사후합격 1위** (`optunaApply gated`) — WR/PF·min_trades 사후 통과. +2. **변동성·손실일 줄이기 → ② 안정 1위** (`stable`) — PnL보다 일별 안정 우선 시. +3. **2차 narrow·대표 숫자 → ③ mode_combo** — 축별 최빈 조립 · trial 번호 없음 · 익절은 pool 최빈값 참고. +4. **④ mode Top10 1위** — 양수 pool 전형 trial(다축 밴드 근접). **익절 하나로 쓰지 말 것** · 표에서 「보기」→ consensus apply. + +## 이전 장에서는 +- 실매 동기간 청산 27건 · 승률 59% · 합산 PnL 24,386원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #98 · 거래 16건 · 승률 43.8% · PF 2.30 · PnL 46,202원 + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-30 06:22:49 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +## 이전 장에서는 +지난 5거래일(2026-08-24~28) 돌파 전략 Optuna 백테스트 결과, **사후합격 1위(#98)**는 PnL 46,202원, 승률 43.8%를 기록했습니다. 하지만 **동일 기간 실제 운용은 PnL 24,386원, 승률 59%**로, 백테스트와 큰 차이를 보였습니다. 이는 데이터 유니버스, 틱 경로 등 실제와 백테스트 간의 고유한 괴리(고스트퍼지) 때문에 자연스러운 현상입니다. 5일이라는 **매우 짧은 표본 기간**으로 과적합 및 표본 부족 위험이 상존합니다. + +## 앞으로 장에서는 +Optuna가 '과적합 위험 상대적으로 낮음'으로 판단했으나, 이는 휴리스틱 지표일 뿐 **5일 표본의 한계로 과적합 및 표본 부족 위험은 여전히 높습니다.** **DB 1위 후보는 단지 과거 데이터에서 최적의 성능을 보인 조합일 뿐, 특정 종목 매수를 추천하거나 확정 수익을 약속하는 것이 아닙니다.** 실매 적용 전 반드시 **페이퍼/소액 모의 거래로 최소 1~2일 이상 관찰**하며, 백테스트와의 괴리 및 **손절(SL) 기능이 제대로 작동하는지 우선적으로 확인**해야 합니다. 시장이 변동하면 수익률보다 위험 관리 기준이 먼저 깨질 수 있음을 명심하십시오. diff --git a/kis_trader/backtest/results/optuna_breakout_tpe_20260830_064647.briefing.md b/kis_trader/backtest/results/optuna_breakout_tpe_20260830_064647.briefing.md new file mode 100644 index 0000000..18ac5fd --- /dev/null +++ b/kis_trader/backtest/results/optuna_breakout_tpe_20260830_064647.briefing.md @@ -0,0 +1,68 @@ +# Optuna 브리핑 — 돌파 (tpe) +기간: 2026-08-24 ~ 2026-08-28 (거래일≈5일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥10 | 후보 게이트: WR≥40% PF≥1 · min_trades≥10 +- min_trades=10: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 48건 · 사후합격(results_gated) 15건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 0.0%** · **적용 가능도 100.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈5일 — 표본 일수 상대적 양호 + - 거래 표본: +0.0p — 상위 후보 거래 10건 — 상대적 양호 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +0.0p — 사후합격 15건 · stable 4건 + - PnL 고원: +0.0p — 고원 비율 7% (1건) +- 임계값 분포 표본: results_gated 상위 15건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `max_daily_chg` | 45.0 | 41.5 ~ 47.0 | 45.0 (20%) | +| `vol_mult` | 1.6 | 1.35 ~ 1.8 | 1.6 (20%) | +| `vol_window` | 12.0 | 9.5 ~ 14.0 | 15 (20%) | +| `min_turnover_1m_pct` | 0.4 | 0.35 ~ 0.425 | 0.4 (40%) | +| `prev_chg_min` | 0.2 | 0.2 ~ 0.25 | 0.2 (73%) | +| `prev_chg_max` | 10.5 | 8.25 ~ 13.75 | 7.5 (13%) | +| `min_price` | 3500.0 | 2500.0 ~ 4250.0 | 2000 (27%) | +| `tp_pct` | 14.5 | 10.0 ~ 21.5 | 7.0 (13%) | +| `sl_mode` | — | — | atr (100%) | +| `trail_pct` | 2.0 | 1.2 ~ 2.35 | 1.2 (27%) | +| `trail_arm_pct` | 1.6 | 0.95 ~ 2.2 | 2.2 (13%) | +| `shoulder_min_high_pct` | 5.4 | 5.1 ~ 5.5 | 5.4 (27%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 최종 선택 후보 (규칙 · DB 적용 전 확인) +- 아래는 **역할이 다른** 4종 후보입니다. mode Top10 1위 ≠ 익절 대표 · **DB 1순위는 사후합격(gated)** 입니다. +- mode pool: `positive` · scoring: `band_proximity_p25_p75` + +### 후보 4종 +- **① 사후합격 1위 (apply: gated)** (trial #90): PnL 33,665원 · WR 40.0% · PF 3.07 · 거래 10건 · 안정점수 -10125 · 익절12.0% +- **② 안정 1위 (apply: stable)** (trial #178): PnL 12,982원 · WR 40.0% · PF 1.71 · 거래 10건 · 안정점수 -1700 · 익절24.5% +- **③ mode_combo (apply: mode)** (trial 없음(조립)): PnL 8,701원 · WR 45.0% · PF 1.30 · 거래 20건 · 안정점수 -6956 · 익절12.0% · tp 최빈 12.0% (2/26) +- **④ mode Top10 1위 (apply: consensus · 밴드 전형 trial)** (trial #102): PnL 18,213원 · WR 26.7% · PF 1.46 · 거래 15건 · 안정점수 -15014 · 근접 96.2% (20/30축 밴드안) · 익절15.0% + +- mode pool tp_pct 밴드 p25~p75: **8.75~20.875%** · Top10 1위 tp=15% → 밴드 **안** +- mode Top10 meta: pool 26건 · 축 30개 + +### 추천 (자동 · confirm 필수) +1. **실매 DB 적용 1순위 → ① 사후합격 1위** (`optunaApply gated`) — WR/PF·min_trades 사후 통과. +2. **변동성·손실일 줄이기 → ② 안정 1위** (`stable`) — PnL보다 일별 안정 우선 시. +3. **2차 narrow·대표 숫자 → ③ mode_combo** — 축별 최빈 조립 · trial 번호 없음 · 익절은 pool 최빈값 참고. +4. **④ mode Top10 1위** — 양수 pool 전형 trial(다축 밴드 근접). **익절 하나로 쓰지 말 것** · 표에서 「보기」→ consensus apply. + +## 이전 장에서는 +- 실매 동기간 청산 27건 · 승률 59% · 합산 PnL 24,386원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #90 · 거래 10건 · 승률 40.0% · PF 3.07 · PnL 33,665원 + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-30 06:47:02 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +**이전 장에서는** +지난주(2026-08-24~28) 5거래일간의 돌파 전략 Optuna TPE 백테스트에서, 사후합격(gated) 조건 통과 후 1위 전략은 PnL 33,665원, 승률 40.0%, PF 3.07을 기록했습니다. 동 기간 실매 청산 PnL 24,386원과는 차이가 있으며, 이는 백테스트와 실제 매매 환경의 괴리 때문임을 인지해야 합니다. + +**앞으로 장에서는** +단 5일간의 백테스트는 `과적합 위험`과 `표본 부족` 가능성이 높습니다. 백테스트 결과는 `고스트·유니버스·틱 경로 차이` 등으로 `실매와 괴리`될 수 있으니, `사후합격 1위`라도 즉시 실매 적용은 금물입니다. `mode Top10 1위`를 익절 대표로 단정하지 마시고, DB 적용 1순위는 `사후합격 1위`입니다. 실매 전 다일 검증 및 페이퍼/소액 모의 거래로 충분히 관찰 후 확대하세요. 변동성 장세에서는 손절 기준이 유효한지 항상 확인하는 것이 중요하며, 특정 종목 추천이나 확정 수익은 보장하지 않습니다. diff --git a/kis_trader/backtest/results/optuna_breakout_tpe_20260830_070939.briefing.md b/kis_trader/backtest/results/optuna_breakout_tpe_20260830_070939.briefing.md new file mode 100644 index 0000000..bfa7c22 --- /dev/null +++ b/kis_trader/backtest/results/optuna_breakout_tpe_20260830_070939.briefing.md @@ -0,0 +1,70 @@ +# Optuna 브리핑 — 돌파 (tpe) +기간: 2026-08-24 ~ 2026-08-28 (거래일≈5일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥10 | 후보 게이트: WR≥40% PF≥1 · min_trades≥10 +- min_trades=10: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 37건 · 사후합격(results_gated) 14건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 0.0%** · **적용 가능도 100.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈5일 — 표본 일수 상대적 양호 + - 거래 표본: +0.0p — 상위 후보 거래 10건 — 상대적 양호 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +0.0p — 사후합격 14건 · stable 2건 + - PnL 고원: +0.0p — 고원 비율 7% (1건) +- 임계값 분포 표본: results_gated 상위 14건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `max_daily_chg` | 27.0 | 24.25 ~ 39.25 | 25.0 (14%) | +| `vol_mult` | 1.3 | 1.125 ~ 1.725 | 1.2 (21%) | +| `vol_window` | 5.5 | 4.0 ~ 7.0 | 7 (21%) | +| `min_turnover_1m_pct` | 0.4 | 0.35 ~ 0.45 | 0.45 (29%) | +| `prev_chg_min` | 0.25 | 0.2 ~ 0.4 | 0.2 (50%) | +| `prev_chg_max` | 23.75 | 21.375 ~ 26.0 | 24.0 (14%) | +| `min_price` | 2000.0 | 1000.0 ~ 2500.0 | 1000 (43%) | +| `tp_pct` | 12.5 | 11.625 ~ 15.5 | 12.0 (14%) | +| `sl_mode` | — | — | atr (100%) | +| `trail_pct` | 1.75 | 1.2 ~ 2.175 | 2.1 (14%) | +| `trail_arm_pct` | 3.3 | 0.75 ~ 4.05 | 3.3 (14%) | +| `shoulder_min_high_pct` | 1.8 | 1.525 ~ 4.225 | 1.8 (21%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 최종 선택 후보 (규칙 · DB 적용 전 확인) +- 아래는 **역할이 다른** 4종 후보입니다. mode Top10 1위 ≠ 익절 대표 · **DB 1순위는 사후합격(gated)** 입니다. +- mode pool: `positive` · scoring: `band_proximity_p25_p75` + +### 후보 4종 +- **① 사후합격 1위 (apply: gated)** (trial #169): PnL 27,467원 · WR 60.0% · PF 2.41 · 거래 10건 · 안정점수 -5874 · 익절20.5% +- **② 안정 1위 (apply: stable)** (trial #157): PnL 17,019원 · WR 55.0% · PF 1.42 · 거래 20건 · 안정점수 -483 · 익절12.0% +- **③ mode_combo (apply: mode)** (trial 없음(조립)): PnL 3,712원 · WR 25.0% · PF 1.06 · 거래 24건 · 안정점수 -18256 · 익절12.0% · tp 최빈 12.0% (2/19) +- **④ mode Top10 1위 (apply: consensus · 밴드 전형 trial)** (trial #159): PnL 8,050원 · WR 52.0% · PF 1.20 · 거래 25건 · 안정점수 -7055 · 근접 97.5% (25/30축 밴드안) · 익절13.5% + +- mode pool tp_pct 밴드 p25~p75: **11.75~19%** · Top10 1위 tp=13.5% → 밴드 **안** +- mode Top10 meta: pool 19건 · 축 30개 + +### 추천 (자동 · confirm 필수) +1. **실매 DB 적용 1순위 → ① 사후합격 1위** (`optunaApply gated`) — WR/PF·min_trades 사후 통과. +2. **변동성·손실일 줄이기 → ② 안정 1위** (`stable`) — PnL보다 일별 안정 우선 시. +3. **2차 narrow·대표 숫자 → ③ mode_combo** — 축별 최빈 조립 · trial 번호 없음 · 익절은 pool 최빈값 참고. +4. **④ mode Top10 1위** — 양수 pool 전형 trial(다축 밴드 근접). **익절 하나로 쓰지 말 것** · 표에서 「보기」→ consensus apply. + +## 이전 장에서는 +- 실매 동기간 청산 27건 · 승률 59% · 합산 PnL 24,386원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #169 · 거래 10건 · 승률 60.0% · PF 2.41 · PnL 27,467원 + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-30 07:09:55 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +## 이전 장에서는 + +5일간의 짧은 돌파 전략 Optuna TPE 백테스트 결과, 200회 시행 중 **사후합격(gated)**된 14개 조합을 확인했습니다. 백테스트 상 사후합격 1위는 PnL 27,467원, 승률 60%를 기록했습니다. 실제 거래에서는 PnL 24,386원, 승률 59%로 유사한 흐름이었으나, **백테스트와 실매 간 괴리는 늘 존재함**을 다시 한번 상기합니다. + +## 앞으로 장에서는 + +이 짧은 5일 백테스트 결과만으로 **과적합 및 표본 부족 위험**이 상존함을 명심해야 합니다. 아무리 휴리스틱이 낮다고 해도, 최소 거래 10건은 우연한 승률일 수 있습니다. `mode Top10 1위`를 익절 대표로 단정하지 마시고, 오직 **사후합격(gated)된 조합**만 고려하세요. 실매에서는 **고스트 퍼지, 유동성, 슬리피지** 등으로 백테스트와 반드시 괴리가 발생합니다. 특정 종목 매수 추천이나 확정 수익은 불가하며, 반드시 페이퍼/소액 거래로 손절 작동 여부를 확인하며 점진적으로 적용하세요. diff --git a/kis_trader/backtest/results/optuna_breakout_tpe_20260830_125447.briefing.md b/kis_trader/backtest/results/optuna_breakout_tpe_20260830_125447.briefing.md new file mode 100644 index 0000000..0d76873 --- /dev/null +++ b/kis_trader/backtest/results/optuna_breakout_tpe_20260830_125447.briefing.md @@ -0,0 +1,70 @@ +# Optuna 브리핑 — 돌파 (tpe) +기간: 2026-08-24 ~ 2026-08-28 (거래일≈5일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥10 | 후보 게이트: WR≥40% PF≥1 · min_trades≥10 +- min_trades=10: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 163건 · 사후합격(results_gated) 102건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 0.0%** · **적용 가능도 100.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈5일 — 표본 일수 상대적 양호 + - 거래 표본: +0.0p — 상위 후보 거래 10건 — 상대적 양호 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +0.0p — 사후합격 102건 · stable 10건 + - PnL 고원: +0.0p — 고원 비율 1% (1건) +- 임계값 분포 표본: results_gated 상위 30건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `max_daily_chg` | 24.0 | 22.0 ~ 27.75 | 24.0 (17%) | +| `vol_mult` | 3.8 | 3.5 ~ 4.2 | 3.8 (17%) | +| `vol_window` | 8.0 | 7.0 ~ 10.75 | 8 (20%) | +| `min_turnover_1m_pct` | 0.25 | 0.2 ~ 0.3 | 0.2 (27%) | +| `prev_chg_min` | 0.4 | 0.3 ~ 0.5 | 0.5 (23%) | +| `prev_chg_max` | 23.5 | 18.75 ~ 26.5 | 23.5 (10%) | +| `min_price` | 3000.0 | 2500.0 ~ 4000.0 | 3000 (20%) | +| `tp_pct` | 10.5 | 8.125 ~ 14.375 | 10.5 (10%) | +| `sl_mode` | — | — | fixed (100%) | +| `trail_pct` | 4.95 | 4.2 ~ 5.65 | 4.2 (10%) | +| `trail_arm_pct` | 2.4 | 2.125 ~ 3.2 | 2.4 (10%) | +| `shoulder_min_high_pct` | 1.75 | 1.325 ~ 2.275 | 1.3 (13%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 최종 선택 후보 (규칙 · DB 적용 전 확인) +- 아래는 **역할이 다른** 4종 후보입니다. mode Top10 1위 ≠ 익절 대표 · **DB 1순위는 사후합격(gated)** 입니다. +- mode pool: `positive` · scoring: `band_proximity_p25_p75` + +### 후보 4종 +- **① 사후합격 1위 (apply: gated)** (trial #163): PnL 103,102원 · WR 60.0% · PF 7.25 · 거래 10건 · 안정점수 13426 · 익절18.5% 손절3.4% +- **② 안정 1위 (apply: stable)** (trial #163): PnL 103,102원 · WR 60.0% · PF 7.25 · 거래 10건 · 안정점수 13426 · 익절18.5% 손절3.4% +- **③ mode_combo (apply: mode)** (trial 없음(조립)): PnL 90,379원 · WR 68.2% · PF 3.37 · 거래 22건 · 안정점수 -14317 · 익절10.5% 손절2.3% · tp 최빈 10.5% (10/110) +- **④ mode Top10 1위 (apply: consensus · 밴드 전형 trial)** (trial #87): PnL 85,877원 · WR 78.6% · PF 6.61 · 거래 14건 · 안정점수 -2361 · 근접 94.4% (24/27축 밴드안) · 익절9.0% 손절2.7% + +- mode pool tp_pct 밴드 p25~p75: **7.125~13.75%** · Top10 1위 tp=9% → 밴드 **안** +- mode Top10 meta: pool 110건 · 축 27개 + +### 추천 (자동 · confirm 필수) +1. **실매 DB 적용 1순위 → ① 사후합격 1위** (`optunaApply gated`) — WR/PF·min_trades 사후 통과. +2. **변동성·손실일 줄이기 → ② 안정 1위** (`stable`) — PnL보다 일별 안정 우선 시. +3. **2차 narrow·대표 숫자 → ③ mode_combo** — 축별 최빈 조립 · trial 번호 없음 · 익절은 pool 최빈값 참고. +4. **④ mode Top10 1위** — 양수 pool 전형 trial(다축 밴드 근접). **익절 하나로 쓰지 말 것** · 표에서 「보기」→ consensus apply. + +## 이전 장에서는 +- 실매 동기간 청산 27건 · 승률 59% · 합산 PnL 24,386원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #163 · 거래 10건 · 승률 60.0% · PF 7.25 · PnL 103,102원 + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-30 12:54:58 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +## 이전 장에서는 + +지난 5거래일(2026-08-24~28) Optuna TPE 백테스트에서, 사후합격 1위(#163)는 PnL 10.3만원, 승률 60%, 10건 거래를 기록했습니다. 하지만 동기간 실매는 PnL 2.4만원, 27건으로, 백테스트와 **큰 괴리**를 보였습니다. + +## 앞으로 장에서는 + +이번 결과는 5일이라는 **짧은 표본 기간**과 **10건의 적은 거래량**으로 **과적합 및 표본 부족 위험**이 큽니다. **실매↔백테 괴리(고스트퍼지 등)**가 확인된 만큼, 파라미터를 **즉시 적용하는 것은 금물**입니다. 반드시 추가 검증(다일 백테스트, 모의/소액) 후 신중히 접근해야 합니다. 특히 `mode Top10 1위`를 익절 대표로 단정 마세요(DB 1순위는 `사후합격`입니다). 불확실한 시장에서 확정 수익은 없으며, 손절 기준 유지에 집중할 것입니다. diff --git a/kis_trader/backtest/results/optuna_breakout_tpe_20260830_131518.briefing.md b/kis_trader/backtest/results/optuna_breakout_tpe_20260830_131518.briefing.md new file mode 100644 index 0000000..9c79b3e --- /dev/null +++ b/kis_trader/backtest/results/optuna_breakout_tpe_20260830_131518.briefing.md @@ -0,0 +1,68 @@ +# Optuna 브리핑 — 돌파 (tpe) +기간: 2026-08-24 ~ 2026-08-28 (거래일≈5일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥10 | 후보 게이트: WR≥40% PF≥1 · min_trades≥10 +- min_trades=10: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 167건 · 사후합격(results_gated) 108건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 0.0%** · **적용 가능도 100.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈5일 — 표본 일수 상대적 양호 + - 거래 표본: +0.0p — 상위 후보 거래 11건 — 상대적 양호 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +0.0p — 사후합격 108건 · stable 10건 + - PnL 고원: +0.0p — 고원 비율 1% (1건) +- 임계값 분포 표본: results_gated 상위 30건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `max_daily_chg` | 28.0 | 22.25 ~ 38.5 | 20.0 (10%) | +| `vol_mult` | 4.25 | 3.825 ~ 4.5 | 4.5 (13%) | +| `vol_window` | 8.5 | 7.0 ~ 11.0 | 7 (27%) | +| `min_turnover_1m_pct` | 0.3 | 0.2 ~ 0.35 | 0.3 (23%) | +| `prev_chg_min` | 0.4 | 0.3 ~ 0.5 | 0.3 (27%) | +| `prev_chg_max` | 21.0 | 14.0 ~ 24.375 | 26.0 (13%) | +| `min_price` | 2000.0 | 1500.0 ~ 2375.0 | 2000 (30%) | +| `tp_pct` | 19.0 | 15.125 ~ 21.375 | 16.0 (13%) | +| `sl_mode` | — | — | fixed (100%) | +| `trail_pct` | 5.1 | 4.7 ~ 5.6 | 4.7 (10%) | +| `trail_arm_pct` | 0.9 | 0.4 ~ 1.575 | 0.2 (13%) | +| `shoulder_min_high_pct` | 2.65 | 1.9 ~ 3.425 | 1.9 (17%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 최종 선택 후보 (규칙 · DB 적용 전 확인) +- 아래는 **역할이 다른** 4종 후보입니다. mode Top10 1위 ≠ 익절 대표 · **DB 1순위는 사후합격(gated)** 입니다. +- mode pool: `positive` · scoring: `band_proximity_p25_p75` + +### 후보 4종 +- **① 사후합격 1위 (apply: gated)** (trial #187): PnL 133,569원 · WR 72.7% · PF 14.61 · 거래 11건 · 안정점수 17483 · 익절15.0% 손절7.2% +- **② 안정 1위 (apply: stable)** (trial #133): PnL 95,026원 · WR 53.9% · PF 2.45 · 거래 13건 · 안정점수 23359 · 익절12.0% 손절5.4% +- **③ mode_combo (apply: mode)** (trial 없음(조립)): PnL -17,292원 · WR 31.6% · PF 0.78 · 거래 19건 · 안정점수 -14962 · 익절19.5% 손절1.9% · tp 최빈 19.5% (9/116) +- **④ mode Top10 1위 (apply: consensus · 밴드 전형 trial)** (trial #113): PnL 51,500원 · WR 68.8% · PF 2.48 · 거래 16건 · 안정점수 1757 · 근접 97.8% (24/27축 밴드안) · 익절18.5% 손절4.6% + +- mode pool tp_pct 밴드 p25~p75: **12.875~21%** · Top10 1위 tp=18.5% → 밴드 **안** +- mode Top10 meta: pool 116건 · 축 27개 + +### 추천 (자동 · confirm 필수) +1. **실매 DB 적용 1순위 → ① 사후합격 1위** (`optunaApply gated`) — WR/PF·min_trades 사후 통과. +2. **변동성·손실일 줄이기 → ② 안정 1위** (`stable`) — PnL보다 일별 안정 우선 시. +3. **2차 narrow·대표 숫자 → ③ mode_combo** — 축별 최빈 조립 · trial 번호 없음 · 익절은 pool 최빈값 참고. +4. **④ mode Top10 1위** — 양수 pool 전형 trial(다축 밴드 근접). **익절 하나로 쓰지 말 것** · 표에서 「보기」→ consensus apply. + +## 이전 장에서는 +- 실매 동기간 청산 27건 · 승률 59% · 합산 PnL 24,386원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #187 · 거래 11건 · 승률 72.7% · PF 14.61 · PnL 133,569원 + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-30 13:15:29 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +「이전 장에서는」 +지난 5거래일간 실매는 27건 거래, 승률 59%로 약 2.4만원 PnL을 기록했습니다. 백테스트 '사후합격 1위'는 11건 거래, 승률 72.7%, PnL 13.3만원으로 양호했으나, 실매와는 차이가 있었습니다. + +「앞으로 장에서는」 +이번 Optuna 결과는 단 5일간의 짧은 기간과 10여 건의 적은 거래량으로 **과적합 및 표본 부족 위험이 매우 높습니다.** '과적합 위험 낮음' 판정은 휴리스틱일 뿐, 실제 시장에서는 고스트퍼지 등 백테스트와 실매 간 상당한 괴리가 발생할 수 있음을 명심해야 합니다. 특정 조합을 실매에 즉시 적용하기보다, 최소 5거래일 이상 재검증과 웹백테스트, 소액 모의투자로 안정성을 충분히 확인하세요. 'mode Top10 1위'를 익절 대표로 오인하지 마세요. 변동성 장세에서는 승률보다 **손절 원칙 준수**가 훨씬 중요합니다. 확정 수익 보장이나 특정 종목 매수 추천은 불가합니다. diff --git a/kis_trader/backtest/results/optuna_breakout_tpe_20260830_133751.briefing.md b/kis_trader/backtest/results/optuna_breakout_tpe_20260830_133751.briefing.md new file mode 100644 index 0000000..f9d0135 --- /dev/null +++ b/kis_trader/backtest/results/optuna_breakout_tpe_20260830_133751.briefing.md @@ -0,0 +1,68 @@ +# Optuna 브리핑 — 돌파 (tpe) +기간: 2026-08-24 ~ 2026-08-28 (거래일≈5일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥10 | 후보 게이트: WR≥40% PF≥1 · min_trades≥10 +- min_trades=10: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 43건 · 사후합격(results_gated) 12건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 0.0%** · **적용 가능도 100.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈5일 — 표본 일수 상대적 양호 + - 거래 표본: +0.0p — 상위 후보 거래 10건 — 상대적 양호 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +0.0p — 사후합격 12건 · stable 6건 + - PnL 고원: +0.0p — 고원 비율 8% (1건) +- 임계값 분포 표본: results_gated 상위 12건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `max_daily_chg` | 28.5 | 26.5 ~ 29.5 | 29.0 (25%) | +| `vol_mult` | 1.65 | 1.575 ~ 2.225 | 1.6 (25%) | +| `vol_window` | 14.0 | 13.75 ~ 14.25 | 14 (50%) | +| `min_turnover_1m_pct` | 0.2 | 0.1375 ~ 0.3125 | 0.1 (25%) | +| `prev_chg_min` | 0.2 | 0.2 ~ 0.3 | 0.2 (67%) | +| `prev_chg_max` | 20.0 | 14.5 ~ 23.375 | 20.0 (17%) | +| `min_price` | 1750.0 | 1000.0 ~ 2000.0 | 2000 (33%) | +| `tp_pct` | 14.75 | 13.5 ~ 18.25 | 14.0 (17%) | +| `sl_mode` | — | — | fixed (100%) | +| `trail_pct` | 5.75 | 5.425 ~ 5.925 | 6.0 (25%) | +| `trail_arm_pct` | 4.5 | 4.05 ~ 4.825 | 4.5 (17%) | +| `shoulder_min_high_pct` | 5.25 | 4.075 ~ 5.675 | 5.5 (17%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 최종 선택 후보 (규칙 · DB 적용 전 확인) +- 아래는 **역할이 다른** 4종 후보입니다. mode Top10 1위 ≠ 익절 대표 · **DB 1순위는 사후합격(gated)** 입니다. +- mode pool: `positive` · scoring: `band_proximity_p25_p75` + +### 후보 4종 +- **① 사후합격 1위 (apply: gated)** (trial #131): PnL 65,204원 · WR 40.0% · PF 3.91 · 거래 10건 · 안정점수 -20405 · 익절14.0% 손절2.6% +- **② 안정 1위 (apply: stable)** (trial #122): PnL 20,494원 · WR 45.5% · PF 1.82 · 거래 11건 · 안정점수 -137 · 익절12.0% 손절4.9% +- **③ mode_combo (apply: mode)** (trial 없음(조립)): PnL 3,743원 · WR 53.9% · PF 1.10 · 거래 13건 · 안정점수 -5711 · 익절14.0% 손절2.9% · tp 최빈 14.0% (3/23) +- **④ mode Top10 1위 (apply: consensus · 밴드 전형 trial)** (trial #166): PnL 1,233원 · WR 30.8% · PF 1.03 · 거래 13건 · 안정점수 -8593 · 근접 95.1% (25/30축 밴드안) · 익절14.0% 손절2.0% + +- mode pool tp_pct 밴드 p25~p75: **12.5~18%** · Top10 1위 tp=14% → 밴드 **안** +- mode Top10 meta: pool 23건 · 축 30개 + +### 추천 (자동 · confirm 필수) +1. **실매 DB 적용 1순위 → ① 사후합격 1위** (`optunaApply gated`) — WR/PF·min_trades 사후 통과. +2. **변동성·손실일 줄이기 → ② 안정 1위** (`stable`) — PnL보다 일별 안정 우선 시. +3. **2차 narrow·대표 숫자 → ③ mode_combo** — 축별 최빈 조립 · trial 번호 없음 · 익절은 pool 최빈값 참고. +4. **④ mode Top10 1위** — 양수 pool 전형 trial(다축 밴드 근접). **익절 하나로 쓰지 말 것** · 표에서 「보기」→ consensus apply. + +## 이전 장에서는 +- 실매 동기간 청산 27건 · 승률 59% · 합산 PnL 24,386원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #131 · 거래 10건 · 승률 40.0% · PF 3.91 · PnL 65,204원 + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-30 13:38:03 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +## 이전 장에서는 +지난 5거래일 백테스트에서 사후합격 1위 전략은 PnL 6.5만원, 승률 40%를 보였습니다. 하지만 동기간 실매는 PnL 2.4만원, 승률 59%로 큰 괴리를 보였습니다. 이는 실매와 백테스트 간의 유니버스, 틱 경로 등 자연스러운 차이(고스트 PnL) 때문입니다. + +## 앞으로 장에서는 +이번 5일간의 짧은 백테스트 기간과 최소 거래 건수(10건)는 **과적합 및 표본 부족 위험**을 내포합니다. 휴리스틱상 '상대적으로 낮음' 판단에도 불구하고, **실매와 백테스트의 괴리**를 항상 경고합니다. `mode Top10 1위`는 익절 대표가 아니며, DB 1순위는 사후합격(gated) 후보입니다. 어떤 '최적' 조합도 미래 수익을 확정하지 않으니, 실전 적용 전 반드시 다일 검증, 소액 모의투자로 **손절 기준의 유효성**을 면밀히 확인 후 신중히 접근하세요. diff --git a/kis_trader/backtest/results/optuna_breakout_tpe_20260830_135705.briefing.md b/kis_trader/backtest/results/optuna_breakout_tpe_20260830_135705.briefing.md new file mode 100644 index 0000000..d40638d --- /dev/null +++ b/kis_trader/backtest/results/optuna_breakout_tpe_20260830_135705.briefing.md @@ -0,0 +1,49 @@ +# Optuna 브리핑 — 돌파 (tpe) +기간: 2026-08-24 ~ 2026-08-28 (거래일≈5일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥10 | 후보 게이트: WR≥40% PF≥1 · min_trades≥10 +- min_trades=10: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 0건 · 사후합격(results_gated) 0건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 33.0%** · **적용 가능도 67.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈5일 — 표본 일수 상대적 양호 + - 거래 표본: +25.0p — 통과 후보 없음 — 적용 불가 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +8.0p — 학습·gated 모두 비어 있음 + - PnL 고원: +0.0p — 고원 판정 스킵 +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 최종 선택 후보 (규칙 · DB 적용 전 확인) +- 아래는 **역할이 다른** 4종 후보입니다. mode Top10 1위 ≠ 익절 대표 · **DB 1순위는 사후합격(gated)** 입니다. +- mode pool: `positive` · scoring: `band_proximity_p25_p75` + +### 후보 4종 +- **① 사후합격 1위 (apply: gated)**: 없음 +- **② 안정 1위 (apply: stable)**: 없음 +- **③ mode_combo (apply: mode)**: 없음 (미산출·구 JSON) +- **④ mode Top10 1위 (apply: consensus · 밴드 전형 trial)**: 없음 +- mode Top10 meta: pool 0건 · 축 0개 + +### 추천 (자동 · confirm 필수) +1. **사후합격 0건 → DB 적용 비권장.** 현행 DB 유지 + 유니버스·틱·기간 재검증. +- ⚠ 안정 게이트 0건 → 안정 Top은 **점수순 폴백** (안정 게이트 0건 → 학습풀 안정점수순 TopN (마이너스 PnL 포함 · 상대비교)) + +## 이전 장에서는 +- 실매 동기간 청산 27건 · 승률 59% · 합산 PnL 24,386원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 이 기간 백테에서는 **유의미한 통과 조합이 거의 없었습니다.** 유니버스·틱·손절·시간창을 먼저 점검하세요 (돌파 0건이면 특히). + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 현재 사후합격 0건 → 파라미터 유지(현행 DB) + 유니버스/데이터 점검이 우선입니다. + +_생성: 2026-08-30 13:57:05 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +## 이전 장에서는 +실매에서는 5일간 27건의 청산, 승률 59%, 합산 PnL 24,386원의 성과를 보였으나, 백테스트에서는 **유의미한 통과 조합이 전무했습니다.** 최소 거래 건수(min_trades=10)와 승률/PF 조건을 충족한 조합이 단 1건도 없어, 해당 기간 전략의 유효성이 확인되지 않았습니다. 과적합 위험은 '상대적으로 낮음'으로 평가되었으나, 이는 통과 후보 자체가 없어 휴리스틱 적용이 제한적이었음을 의미합니다. + +## 앞으로 장에서는 +현재 사후합격(gated)된 전략이 전무하므로, **절대 실매 적용을 재고해야 합니다.** 단 5일간의 짧은 백테스트 기간은 과적합 위험이 매우 높으며, 미흡한 표본으로 얻은 우연한 결과일 가능성이 큽니다. 또한 백테스트 결과가 실매 환경과 괴리될 수 있음을 항상 인지해야 합니다(고스트퍼지 등). Mode Top10 1위 등 탐색 결과는 단순 참고용이며, 최종 실매 적용은 **사후합격(gated)된 유일한 후보**에 한정되어야 합니다. 현행 DB를 유지하고, 유니버스, 틱 데이터, 또는 손절/시간창 설정 등 전략의 기본 요소를 재점검해야 합니다. 특정 종목 추천이나 확정 수익은 보장할 수 없습니다. diff --git a/kis_trader/backtest/results/optuna_breakout_tpe_20260830_141708.briefing.md b/kis_trader/backtest/results/optuna_breakout_tpe_20260830_141708.briefing.md new file mode 100644 index 0000000..c0d7576 --- /dev/null +++ b/kis_trader/backtest/results/optuna_breakout_tpe_20260830_141708.briefing.md @@ -0,0 +1,70 @@ +# Optuna 브리핑 — 돌파 (tpe) +기간: 2026-08-24 ~ 2026-08-28 (거래일≈5일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥10 | 후보 게이트: WR≥40% PF≥1 · min_trades≥10 +- min_trades=10: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 144건 · 사후합격(results_gated) 88건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 0.0%** · **적용 가능도 100.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈5일 — 표본 일수 상대적 양호 + - 거래 표본: +0.0p — 상위 후보 거래 13건 — 상대적 양호 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +0.0p — 사후합격 88건 · stable 10건 + - PnL 고원: +0.0p — 고원 비율 1% (1건) +- 임계값 분포 표본: results_gated 상위 30건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `max_daily_chg` | 29.5 | 25.25 ~ 33.25 | 31.0 (17%) | +| `vol_mult` | 3.95 | 3.6 ~ 4.1 | 4.1 (20%) | +| `vol_window` | 7.5 | 7.0 ~ 9.0 | 7 (27%) | +| `min_turnover_1m_pct` | 0.15 | 0.1 ~ 0.2875 | 0.1 (27%) | +| `prev_chg_min` | 0.5 | 0.4 ~ 0.5 | 0.5 (43%) | +| `prev_chg_max` | 22.75 | 19.625 ~ 27.875 | 29.0 (10%) | +| `min_price` | 4000.0 | 3000.0 ~ 4500.0 | 5000 (23%) | +| `tp_pct` | 8.5 | 5.625 ~ 11.0 | 8.5 (13%) | +| `sl_mode` | — | — | atr (100%) | +| `trail_pct` | 2.05 | 1.25 ~ 2.825 | 2.2 (10%) | +| `trail_arm_pct` | 4.9 | 4.0 ~ 5.4 | 4.9 (10%) | +| `shoulder_min_high_pct` | 4.9 | 3.35 ~ 5.575 | 5.8 (10%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 최종 선택 후보 (규칙 · DB 적용 전 확인) +- 아래는 **역할이 다른** 4종 후보입니다. mode Top10 1위 ≠ 익절 대표 · **DB 1순위는 사후합격(gated)** 입니다. +- mode pool: `positive` · scoring: `band_proximity_p25_p75` + +### 후보 4종 +- **① 사후합격 1위 (apply: gated)** (trial #147): PnL 84,651원 · WR 69.2% · PF 5.98 · 거래 13건 · 안정점수 -3338 · 익절9.0% +- **② 안정 1위 (apply: stable)** (trial #57): PnL 37,638원 · WR 70.0% · PF 3.95 · 거래 10건 · 안정점수 11740 · 익절17.5% +- **③ mode_combo (apply: mode)** (trial 없음(조립)): PnL 80,163원 · WR 60.0% · PF 3.76 · 거래 15건 · 안정점수 -6381 · 익절11.5% · tp 최빈 11.5% (7/99) +- **④ mode Top10 1위 (apply: consensus · 밴드 전형 trial)** (trial #94): PnL 57,616원 · WR 66.7% · PF 2.80 · 거래 15건 · 안정점수 -5301 · 근접 97.3% (22/27축 밴드안) · 익절7.0% + +- mode pool tp_pct 밴드 p25~p75: **4.75~12.25%** · Top10 1위 tp=7% → 밴드 **안** +- mode Top10 meta: pool 99건 · 축 27개 + +### 추천 (자동 · confirm 필수) +1. **실매 DB 적용 1순위 → ① 사후합격 1위** (`optunaApply gated`) — WR/PF·min_trades 사후 통과. +2. **변동성·손실일 줄이기 → ② 안정 1위** (`stable`) — PnL보다 일별 안정 우선 시. +3. **2차 narrow·대표 숫자 → ③ mode_combo** — 축별 최빈 조립 · trial 번호 없음 · 익절은 pool 최빈값 참고. +4. **④ mode Top10 1위** — 양수 pool 전형 trial(다축 밴드 근접). **익절 하나로 쓰지 말 것** · 표에서 「보기」→ consensus apply. + +## 이전 장에서는 +- 실매 동기간 청산 27건 · 승률 59% · 합산 PnL 24,386원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #147 · 거래 13건 · 승률 69.2% · PF 5.98 · PnL 84,651원 + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-30 14:17:19 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +## 이전 장에서는 + +지난 5거래일 Optuna 결과, '사후합격 1위'는 백테스트에서 PnL 84,651원, 승률 69.2%를 기록했습니다. 그러나 실매에서는 PnL 24,386원, 승률 59%로, 백테스트 대비 큰 괴리를 보였습니다. 이는 유니버스, 틱 경로, 고스트 트레이드 등 다양한 요인으로 인한 것이며, 백테스트 결과가 실매를 그대로 반영하지 않는다는 점을 명확히 경고합니다. DB 1순위는 사후합격된 트라이얼이며, 익절 대표값이 아님을 강조합니다. + +## 앞으로 장에서는 + +이번 결과는 5거래일 최적화로 **과적합 및 표본 부족 위험**이 상존합니다. 백테스트 대비 **실매 괴리(고스트퍼지 등)**는 언제든 발생할 수 있으며, 'mode Top10 1위'는 대표 익절 값이 아님을 명심하세요. 어떤 종목도 추천하지 않으며 확정 수익을 약속하지 않습니다. 전략 적용 시에는 최소 5거래일 이상 **재검증**하고, 페이퍼/소액으로 **손절 라인 작동 여부**를 면밀히 관찰하며 신중하게 접근해야 합니다. diff --git a/kis_trader/backtest/results/optuna_breakout_tpe_20260830_143624.briefing.md b/kis_trader/backtest/results/optuna_breakout_tpe_20260830_143624.briefing.md new file mode 100644 index 0000000..333fb7c --- /dev/null +++ b/kis_trader/backtest/results/optuna_breakout_tpe_20260830_143624.briefing.md @@ -0,0 +1,70 @@ +# Optuna 브리핑 — 돌파 (tpe) +기간: 2026-08-24 ~ 2026-08-28 (거래일≈5일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥10 | 후보 게이트: WR≥40% PF≥1 · min_trades≥10 +- min_trades=10: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 186건 · 사후합격(results_gated) 105건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 0.0%** · **적용 가능도 100.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈5일 — 표본 일수 상대적 양호 + - 거래 표본: +0.0p — 상위 후보 거래 19건 — 상대적 양호 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +0.0p — 사후합격 105건 · stable 10건 + - PnL 고원: +0.0p — 고원 비율 1% (1건) +- 임계값 분포 표본: results_gated 상위 30건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `max_daily_chg` | 30.5 | 25.5 ~ 39.0 | 21.0 (10%) | +| `vol_mult` | 1.8 | 1.6 ~ 2.175 | 1.6 (13%) | +| `vol_window` | 11.0 | 7.25 ~ 14.0 | 14 (17%) | +| `min_turnover_1m_pct` | 0.3 | 0.2 ~ 0.35 | 0.35 (27%) | +| `prev_chg_min` | 1.0 | 0.9 ~ 1.0 | 1.0 (70%) | +| `prev_chg_max` | 21.0 | 12.5 ~ 23.0 | 21.0 (10%) | +| `min_price` | 4000.0 | 3500.0 ~ 5000.0 | 5000 (30%) | +| `tp_pct` | 17.5 | 13.125 ~ 20.75 | 18.5 (13%) | +| `sl_mode` | — | — | atr (100%) | +| `trail_pct` | 4.95 | 4.5 ~ 5.575 | 4.5 (10%) | +| `trail_arm_pct` | 2.5 | 0.75 ~ 4.4 | 2.2 (10%) | +| `shoulder_min_high_pct` | 2.0 | 1.575 ~ 2.275 | 2.0 (17%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 최종 선택 후보 (규칙 · DB 적용 전 확인) +- 아래는 **역할이 다른** 4종 후보입니다. mode Top10 1위 ≠ 익절 대표 · **DB 1순위는 사후합격(gated)** 입니다. +- mode pool: `positive` · scoring: `band_proximity_p25_p75` + +### 후보 4종 +- **① 사후합격 1위 (apply: gated)** (trial #138): PnL 54,885원 · WR 57.9% · PF 2.38 · 거래 19건 · 안정점수 -7002 · 익절22.5% +- **② 안정 1위 (apply: stable)** (trial #185): PnL 25,375원 · WR 46.7% · PF 1.51 · 거래 15건 · 안정점수 2555 · 익절21.0% +- **③ mode_combo (apply: mode)** (trial 없음(조립)): PnL 25,606원 · WR 61.1% · PF 1.65 · 거래 18건 · 안정점수 -8396 · 익절19.0% · tp 최빈 19.0% (8/115) +- **④ mode Top10 1위 (apply: consensus · 밴드 전형 trial)** (trial #96): PnL 22,994원 · WR 58.8% · PF 1.54 · 거래 17건 · 안정점수 -8381 · 근접 98.1% (23/27축 밴드안) · 익절22.5% + +- mode pool tp_pct 밴드 p25~p75: **12~20.25%** · Top10 1위 tp=22.5% → 밴드 **밖**(다른 축 보정으로 근접% 높음) +- mode Top10 meta: pool 115건 · 축 27개 + +### 추천 (자동 · confirm 필수) +1. **실매 DB 적용 1순위 → ① 사후합격 1위** (`optunaApply gated`) — WR/PF·min_trades 사후 통과. +2. **변동성·손실일 줄이기 → ② 안정 1위** (`stable`) — PnL보다 일별 안정 우선 시. +3. **2차 narrow·대표 숫자 → ③ mode_combo** — 축별 최빈 조립 · trial 번호 없음 · 익절은 pool 최빈값 참고. +4. **④ mode Top10 1위** — 양수 pool 전형 trial(다축 밴드 근접). **익절 하나로 쓰지 말 것** · 표에서 「보기」→ consensus apply. + +## 이전 장에서는 +- 실매 동기간 청산 27건 · 승률 59% · 합산 PnL 24,386원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #138 · 거래 19건 · 승률 57.9% · PF 2.38 · PnL 54,885원 + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-30 14:36:34 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +## 이전 장에서는 + +지난 5거래일 실매 운용에서 총 27건, 승률 59%, PnL 24,386원을 기록했습니다. 이는 Optuna 백테스트의 사후합격 1위(trial #138: 거래 19건, PnL 54,885원)와 PnL 괴리가 컸음을 확인했습니다. 실매와 백테스트 간 괴리는 늘 있으니 이점 유념해야 합니다. + +## 앞으로 장에서는 + +이번 5일 백테스트는 표본 부족과 과적합 위험이 높습니다. 휴리스틱상 낮아 보여도 실매↔백테스트 괴리(고스트퍼지 등)는 항상 상존합니다. Optuna `gated` 1순위 모델이라도 즉시 실매 적용은 위험합니다. 반드시 다일 재검증과 페이퍼/소액 운용으로 1~2일간 손절 로직 작동을 면밀히 관찰 후 확대하세요. `mode Top10 1위`는 익절 대표가 아니며, 확정 수익을 약속할 수 없습니다. diff --git a/kis_trader/backtest/results/optuna_breakout_tpe_20260830_145903.briefing.md b/kis_trader/backtest/results/optuna_breakout_tpe_20260830_145903.briefing.md new file mode 100644 index 0000000..a1fc718 --- /dev/null +++ b/kis_trader/backtest/results/optuna_breakout_tpe_20260830_145903.briefing.md @@ -0,0 +1,68 @@ +# Optuna 브리핑 — 돌파 (tpe) +기간: 2026-08-24 ~ 2026-08-28 (거래일≈5일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥10 | 후보 게이트: WR≥40% PF≥1 · min_trades≥10 +- min_trades=10: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 58건 · 사후합격(results_gated) 20건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 0.0%** · **적용 가능도 100.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈5일 — 표본 일수 상대적 양호 + - 거래 표본: +0.0p — 상위 후보 거래 28건 — 상대적 양호 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +0.0p — 사후합격 20건 · stable 9건 + - PnL 고원: +0.0p — 고원 비율 5% (1건) +- 임계값 분포 표본: results_gated 상위 20건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `max_daily_chg` | 25.0 | 22.75 ~ 33.25 | 24.0 (15%) | +| `vol_mult` | 1.4 | 1.1 ~ 1.7 | 1.0 (20%) | +| `vol_window` | 6.5 | 3.0 ~ 10.5 | 3 (20%) | +| `min_turnover_1m_pct` | 0.25 | 0.2 ~ 0.35 | 0.25 (20%) | +| `prev_chg_min` | 0.2 | 0.2 ~ 0.3 | 0.2 (70%) | +| `prev_chg_max` | 14.0 | 10.0 ~ 15.0 | 14.5 (15%) | +| `min_price` | 2000.0 | 1500.0 ~ 3000.0 | 1500 (35%) | +| `tp_pct` | 7.25 | 5.0 ~ 13.125 | 5.0 (15%) | +| `sl_mode` | — | — | atr (100%) | +| `trail_pct` | 3.7 | 2.125 ~ 4.6 | 4.5 (15%) | +| `trail_arm_pct` | 2.35 | 1.6 ~ 2.825 | 1.9 (10%) | +| `shoulder_min_high_pct` | 2.15 | 1.475 ~ 2.4 | 2.4 (20%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 최종 선택 후보 (규칙 · DB 적용 전 확인) +- 아래는 **역할이 다른** 4종 후보입니다. mode Top10 1위 ≠ 익절 대표 · **DB 1순위는 사후합격(gated)** 입니다. +- mode pool: `positive` · scoring: `band_proximity_p25_p75` + +### 후보 4종 +- **① 사후합격 1위 (apply: gated)** (trial #171): PnL 39,400원 · WR 64.3% · PF 1.98 · 거래 28건 · 안정점수 -4479 · 익절7.5% +- **② 안정 1위 (apply: stable)** (trial #119): PnL 18,653원 · WR 54.5% · PF 1.70 · 거래 11건 · 안정점수 -1681 · 익절5.0% +- **③ mode_combo (apply: mode)** (trial 없음(조립)): PnL -9,784원 · WR 30.0% · PF 0.76 · 거래 30건 · 안정점수 -7724 · 익절5.0% · tp 최빈 5.0% (3/28) +- **④ mode Top10 1위 (apply: consensus · 밴드 전형 trial)** (trial #92): PnL 5,434원 · WR 43.8% · PF 1.12 · 거래 16건 · 안정점수 -2724 · 근접 98.5% (26/30축 밴드안) · 익절4.0% + +- mode pool tp_pct 밴드 p25~p75: **5~13%** · Top10 1위 tp=4% → 밴드 **밖**(다른 축 보정으로 근접% 높음) +- mode Top10 meta: pool 28건 · 축 30개 + +### 추천 (자동 · confirm 필수) +1. **실매 DB 적용 1순위 → ① 사후합격 1위** (`optunaApply gated`) — WR/PF·min_trades 사후 통과. +2. **변동성·손실일 줄이기 → ② 안정 1위** (`stable`) — PnL보다 일별 안정 우선 시. +3. **2차 narrow·대표 숫자 → ③ mode_combo** — 축별 최빈 조립 · trial 번호 없음 · 익절은 pool 최빈값 참고. +4. **④ mode Top10 1위** — 양수 pool 전형 trial(다축 밴드 근접). **익절 하나로 쓰지 말 것** · 표에서 「보기」→ consensus apply. + +## 이전 장에서는 +- 실매 동기간 청산 27건 · 승률 59% · 합산 PnL 24,386원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #171 · 거래 28건 · 승률 64.3% · PF 1.98 · PnL 39,400원 + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-30 14:59:17 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +**이전 장에서는** +5일간의 짧은 백테스트에서 `사후합격 1위` 조합이 PnL 39,400원(승률 64.3%, PF 1.98)으로 가장 좋은 성과를 기록했습니다. 그러나 실매 동기간 청산 데이터(PnL 24,386원, 승률 59%)와는 차이가 있었습니다. 이는 백테스트와 실매 간의 고스트 오더, 유니버스, 틱 경로 등 다양한 요인으로 인한 괴리 현상입니다. + +**앞으로 장에서는** +5거래일이라는 짧은 기간은 과적합 및 표본 부족 위험이 매우 높습니다. Optuna 휴리스틱상 위험이 낮다고 나와도 실매 적용 시에는 반드시 다일 재검증과 웹백테스트, 페이퍼/소액 트레이딩을 통해 면밀히 관찰해야 합니다. `mode Top10 1위`는 익절 대표가 아니며, 실매 적용을 고려할 DB 1순위는 `사후합격`입니다. 시장은 항상 변하므로 백테스트 결과가 내일의 확정 수익을 보장하지 않으니 손절 관리와 함께 신중하게 접근하세요. diff --git a/kis_trader/backtest/results/optuna_breakout_tpe_20260830_152138.briefing.md b/kis_trader/backtest/results/optuna_breakout_tpe_20260830_152138.briefing.md new file mode 100644 index 0000000..17d8d3a --- /dev/null +++ b/kis_trader/backtest/results/optuna_breakout_tpe_20260830_152138.briefing.md @@ -0,0 +1,70 @@ +# Optuna 브리핑 — 돌파 (tpe) +기간: 2026-08-24 ~ 2026-08-28 (거래일≈5일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥10 | 후보 게이트: WR≥40% PF≥1 · min_trades≥10 +- min_trades=10: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 64건 · 사후합격(results_gated) 21건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 0.0%** · **적용 가능도 100.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈5일 — 표본 일수 상대적 양호 + - 거래 표본: +0.0p — 상위 후보 거래 11건 — 상대적 양호 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +0.0p — 사후합격 21건 · stable 9건 + - PnL 고원: +0.0p — 고원 비율 5% (1건) +- 임계값 분포 표본: results_gated 상위 21건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `max_daily_chg` | 27.0 | 22.0 ~ 32.0 | 22.0 (24%) | +| `vol_mult` | 1.3 | 1.2 ~ 1.6 | 1.2 (24%) | +| `vol_window` | 6.0 | 3.0 ~ 10.0 | 3 (14%) | +| `min_turnover_1m_pct` | 0.35 | 0.2 ~ 0.4 | 0.35 (24%) | +| `prev_chg_min` | 0.2 | 0.2 ~ 0.3 | 0.2 (52%) | +| `prev_chg_max` | 16.0 | 7.5 ~ 18.5 | 16.0 (19%) | +| `min_price` | 3500.0 | 2500.0 ~ 4000.0 | 3500 (33%) | +| `tp_pct` | 6.0 | 3.0 ~ 7.5 | 1.0 (14%) | +| `sl_mode` | — | — | atr (100%) | +| `trail_pct` | 2.3 | 1.3 ~ 3.8 | 2.4 (10%) | +| `trail_arm_pct` | 1.0 | 0.5 ~ 1.5 | 1.2 (14%) | +| `shoulder_min_high_pct` | 1.6 | 1.3 ~ 2.2 | 1.2 (14%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 최종 선택 후보 (규칙 · DB 적용 전 확인) +- 아래는 **역할이 다른** 4종 후보입니다. mode Top10 1위 ≠ 익절 대표 · **DB 1순위는 사후합격(gated)** 입니다. +- mode pool: `positive` · scoring: `band_proximity_p25_p75` + +### 후보 4종 +- **① 사후합격 1위 (apply: gated)** (trial #191): PnL 31,012원 · WR 63.6% · PF 3.02 · 거래 11건 · 안정점수 756 · 익절5.0% +- **② 안정 1위 (apply: stable)** (trial #191): PnL 31,012원 · WR 63.6% · PF 3.02 · 거래 11건 · 안정점수 756 · 익절5.0% +- **③ mode_combo (apply: mode)** (trial 없음(조립)): PnL 23,298원 · WR 77.8% · PF 2.24 · 거래 18건 · 안정점수 -2633 · 익절1.0% · tp 최빈 1.0% (3/22) +- **④ mode Top10 1위 (apply: consensus · 밴드 전형 trial)** (trial #50): PnL 1,341원 · WR 43.8% · PF 1.05 · 거래 16건 · 안정점수 -3860 · 근접 97.4% (23/30축 밴드안) · 익절2.5% + +- mode pool tp_pct 밴드 p25~p75: **3.125~7.5%** · Top10 1위 tp=2.5% → 밴드 **밖**(다른 축 보정으로 근접% 높음) +- mode Top10 meta: pool 22건 · 축 30개 + +### 추천 (자동 · confirm 필수) +1. **실매 DB 적용 1순위 → ① 사후합격 1위** (`optunaApply gated`) — WR/PF·min_trades 사후 통과. +2. **변동성·손실일 줄이기 → ② 안정 1위** (`stable`) — PnL보다 일별 안정 우선 시. +3. **2차 narrow·대표 숫자 → ③ mode_combo** — 축별 최빈 조립 · trial 번호 없음 · 익절은 pool 최빈값 참고. +4. **④ mode Top10 1위** — 양수 pool 전형 trial(다축 밴드 근접). **익절 하나로 쓰지 말 것** · 표에서 「보기」→ consensus apply. + +## 이전 장에서는 +- 실매 동기간 청산 27건 · 승률 59% · 합산 PnL 24,386원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #191 · 거래 11건 · 승률 63.6% · PF 3.02 · PnL 31,012원 + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-30 15:21:52 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +## 이전 장에서는 + +Optuna TPE 백테스트는 지난 5거래일간 총 31,012원 수익, 63.6% 승률을 기록했습니다. 사후합격된 #191번 전략이 이 기간 최적의 성과를 보였으나, 실매 동기간 PnL(24,386원) 및 승률(59%)과 차이가 있음을 인지해야 합니다. 이는 백테스트와 실매 환경의 자연스러운 괴리입니다. + +## 앞으로 장에서는 + +5일의 짧은 백테스트 기간은 과적합 및 표본 부족 위험을 내포합니다. 실매 환경에서는 고스트퍼지 등 백테스트와 괴리가 발생할 수 있습니다. ‘사후합격 1위’가 DB 적용 우선순위이나, ‘mode Top10 1위’를 익절 대표로 단정하지 마십시오. 특정 종목 매수 추천이나 확정 수익을 약속할 수 없습니다. 반드시 다일 재검증 및 소액/모의투자 후 적용하며 리스크 관리에 집중해 주세요. diff --git a/kis_trader/backtest/results/optuna_momentum_fast_20260826_030721.briefing.md b/kis_trader/backtest/results/optuna_momentum_fast_20260826_030721.briefing.md new file mode 100644 index 0000000..9dfd18b --- /dev/null +++ b/kis_trader/backtest/results/optuna_momentum_fast_20260826_030721.briefing.md @@ -0,0 +1,47 @@ +# Optuna 브리핑 — 모멘텀 (fast) +기간: 2026-08-25 ~ 2026-08-25 (거래일≈1일) · trials 5/5 +탐색 게이트: WR≥0% PF≥0 · min_trades≥1 | 후보 게이트: WR≥40% PF≥1 · min_trades≥1 +- min_trades=1: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 5건 · 사후합격(results_gated) 0건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 52.0%** · **적용 가능도 48.0%** · 판정: **주의** — 적용 가능도 중간 — 다일(≥5일) 재검증·웹백테 후 소액만 + - 표본 장일: +40.0p — 거래일≈1일 — 단일 장 과적합 위험 최대 + - 거래 표본: +0.0p — 상위 후보 거래 25건 — 상대적 양호 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +12.0p — 사후합격 0건 (학습 5) — DB 적용 비권장 + - PnL 고원: +0.0p — 고원 비율 20% (1건) +- 임계값 분포 표본: results(learning) 상위 5건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `mom_rsi_min` | 52.0 | 49.0 ~ 52.0 | 52 (60%) | +| `mom_rsi_max` | 90.0 | 80.0 ~ 90.0 | 90 (60%) | +| `mom_vol_mult` | 2.1 | 2.0 ~ 5.0 | 5.0 (40%) | +| `e_min_chg_pct` | 1.0 | 0.5 ~ 1.1 | 1.1 (40%) | +| `tp_pct` | 15.0 | 5.0 ~ 15.0 | 15.0 (60%) | +| `sl_pct` | 5.0 | 4.0 ~ 6.5 | 6.5 (40%) | +| `mom_time_end_hm` | 1430.0 | 1430.0 ~ 1520.0 | 1430 (60%) | +| `mom_max_from_open_pct` | 40.0 | 25.0 ~ 40.0 | 40.0 (60%) | +| `min_margin` | 0.5 | 0.5 ~ 0.5 | 0.5 (60%) | +| `shoulder_min_high` | 3.0 | 1.5 ~ 3.0 | 3.0 (60%) | +| `shoulder_cut_pct` | 0.2 | 0.2 ~ 0.4 | 0.2 (60%) | +| `trail_pct` | 3.0 | 3.0 ~ 3.1 | 3.0 (40%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 이전 장에서는 +- 실매 동기간 청산 6건 · 승률 67% · 합산 PnL -8,174원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 학습 1위(사후게이트 미달 가능): trial #3 · 거래 25건 · 승률 48.0% · PF 0.42 · PnL -44,022원 +- 표본 장이 1일뿐이라, ‘이전 장에 잘 먹힌 숫자’일 뿐 **추세가 바뀌면 바로 깨질 수 있습니다.** +- 플러스/학습 후보는 있으나 승률·PF 사후 기준은 못 넘김 → **당장 DB 적용은 비권장.** + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 현재 사후합격 0건 → 파라미터 유지(현행 DB) + 유니버스/데이터 점검이 우선입니다. + +_생성: 2026-08-26 03:07:24 (rule)_ + +## AI 코멘트 + +_(미생성: api_call_failed. 구글 키는 GEMINI_API_KEY 를 DB/환경에 넣으세요 — kis_trader/backtest/optuna_briefing.py 모듈 docstring 참고)_ diff --git a/kis_trader/backtest/results/optuna_momentum_tpe_20260823_194749.briefing.md b/kis_trader/backtest/results/optuna_momentum_tpe_20260823_194749.briefing.md new file mode 100644 index 0000000..def541f --- /dev/null +++ b/kis_trader/backtest/results/optuna_momentum_tpe_20260823_194749.briefing.md @@ -0,0 +1,49 @@ +# Optuna 브리핑 — 모멘텀 (tpe) +기간: 2026-08-18 ~ 2026-08-21 (거래일≈4일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥1 | 후보 게이트: WR≥40% PF≥1 · min_trades≥1 +- min_trades=1: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 187건 · 사후합격(results_gated) 83건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 16.0%** · **적용 가능도 84.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +16.0p — 거래일≈4일 — 다일 재검증 권장(≥5일) + - 거래 표본: +0.0p — 상위 후보 거래 34건 — 상대적 양호 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +0.0p — 사후합격 83건 · stable 10건 + - PnL 고원: +0.0p — 고원 비율 1% (1건) +- 임계값 분포 표본: results_gated 상위 30건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `use_ema_filter` | 0.0 | 0.0 ~ 0.0 | False (97%) | +| `ema_fast_period` | 18.0 | 17.0 ~ 19.75 | 18 (30%) | +| `ema_slow_period` | 48.0 | 45.0 ~ 49.0 | 48 (27%) | +| `mom_vol_mult` | 2.2 | 2.1 ~ 2.3 | 2.3 (23%) | +| `mom_vol_win` | 5.0 | 4.0 ~ 5.0 | 5 (43%) | +| `e_min_chg_pct` | 1.8 | 1.4 ~ 2.0 | 1.4 (13%) | +| `tp_pct` | 6.55 | 6.0 ~ 8.075 | 6.3 (10%) | +| `tp_max_pct` | 13.0 | 11.125 ~ 14.0 | 13.0 (17%) | +| `sl_pct` | 5.8 | 5.6 ~ 6.575 | 5.7 (13%) | +| `shoulder_min_high` | 1.0 | 0.9 ~ 1.1 | 0.9 (20%) | +| `shoulder_cut_pct` | 0.65 | 0.6 ~ 0.65 | 0.65 (43%) | +| `trail_pct` | 3.05 | 2.7 ~ 3.2 | 3.1 (20%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 이전 장에서는 +- 실매 동기간 청산 57건 · 승률 30% · 합산 PnL 92,958원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #161 · 거래 34건 · 승률 58.8% · PF 2.40 · PnL 86,136원 + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-23 19:48:02 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +「이전 장에서는」 +지난 4거래일간의 짧은 백테스트에서 Optuna TPE는 83개의 사후합격 전략을 도출했고, #161번 트라이얼이 승률 58.8%, PF 2.40으로 우수했습니다. 그러나 실제 매매는 57건 청산, 승률 30%, PnL 92,958원으로 백테스트와 큰 괴리를 보였습니다. 이는 짧은 기간으로 인한 과적합 및 실매와 백테스트 환경(고스트 등)의 차이를 명확히 시사합니다. + +「앞으로 장에서는」 +본 결과는 4일이라는 극히 짧은 기간의 학습으로, 과적합 위험(16.0%)과 표본 부족이 매우 높습니다. **절대 Optuna best를 바로 실매에 적용하지 마십시오.** 최소 5거래일 이상 다일 재검증 및 웹 백테스트가 필수입니다. 백테스트 수익은 미래를 보장하지 않으며, 실매에서 PnL 괴리(고스트퍼지 등)가 발생할 수 있습니다. 페이퍼/소액 모의로 손절 작동 여부를 충분히 관찰 후 신중히 확대해야 합니다. diff --git a/kis_trader/backtest/results/optuna_momentum_tpe_20260823_213105.briefing.md b/kis_trader/backtest/results/optuna_momentum_tpe_20260823_213105.briefing.md new file mode 100644 index 0000000..8c2ffe8 --- /dev/null +++ b/kis_trader/backtest/results/optuna_momentum_tpe_20260823_213105.briefing.md @@ -0,0 +1,51 @@ +# Optuna 브리핑 — 모멘텀 (tpe) +기간: 2026-08-18 ~ 2026-08-21 (거래일≈4일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥1 | 후보 게이트: WR≥40% PF≥1 · min_trades≥1 +- min_trades=1: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 181건 · 사후합격(results_gated) 62건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 36.0%** · **적용 가능도 64.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +16.0p — 거래일≈4일 — 다일 재검증 권장(≥5일) + - 거래 표본: +10.0p — 상위 후보 거래 5건 — 해석 시 주의 + - 승률·PF 이상치: +10.0p — PF 87.76 (소수 거래 폭증) + - 게이트 커버: +0.0p — 사후합격 62건 · stable 10건 + - PnL 고원: +0.0p — 고원 비율 2% (1건) +- 임계값 분포 표본: results_gated 상위 30건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `use_ema_filter` | 0.0 | 0.0 ~ 0.0 | False (100%) | +| `ema_fast_period` | 18.0 | 16.0 ~ 20.0 | 20 (33%) | +| `ema_slow_period` | 50.0 | 31.5 ~ 52.0 | 50 (13%) | +| `mom_vol_mult` | 2.85 | 2.8 ~ 3.0 | 2.8 (30%) | +| `mom_vol_win` | 10.0 | 9.0 ~ 10.0 | 10 (57%) | +| `e_min_chg_pct` | 1.6 | 1.425 ~ 1.8 | 1.5 (20%) | +| `tp_pct` | 12.25 | 11.85 ~ 12.675 | 12.1 (10%) | +| `tp_max_pct` | 17.0 | 16.125 ~ 18.0 | 18.0 (23%) | +| `sl_pct` | 2.75 | 2.6 ~ 3.375 | 2.7 (20%) | +| `shoulder_min_high` | 3.7 | 3.225 ~ 4.3 | 4.4 (17%) | +| `shoulder_cut_pct` | 0.525 | 0.5 ~ 0.55 | 0.5 (37%) | +| `trail_pct` | 3.2 | 0.525 ~ 3.475 | 3.4 (13%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 이전 장에서는 +- 실매 동기간 청산 57건 · 승률 30% · 합산 PnL 92,958원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #195 · 거래 5건 · 승률 80.0% · PF 87.76 · PnL 29,498원 + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-23 21:31:17 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +## 이전 장에서는 + +이번 4일간 Optuna 백테스트에서 #195번 트라이얼이 5건의 거래로 승률 80%, PF 87.76으로 최고 성과를 냈습니다. 그러나 실매 동기간은 57건 거래, 승률 30%로 큰 괴리를 보였습니다. 이는 백테스트 기간이 4일로 매우 짧고, 소수 거래에 의한 과적합 가능성이 높으며, 고스트 가격, 유니버스, 틱 경로 차이 등 실매와 백테스트 간의 필연적인 괴리 때문임을 명심해야 합니다. + +## 앞으로 장에서는 + +이번 Optuna 결과는 4일이라는 짧은 백테스트 기간과 소수 거래(상위 5건)로 인한 과적합 및 표본 부족 위험이 매우 높습니다. PF 87.76과 같은 비정상적 고수익은 특히 주의해야 합니다. 따라서 바로 실매에 적용하지 말고, 최소 5거래일 이상의 다일 재검증과 웹 백테스트를 선행해야 합니다. 실매 적용 시에는 `results_gated` 후보를 페이퍼/소액으로 1~2일 관찰 후 확대하고, 승률보다 손절매 기능이 정상 작동하는지 우선 확인하여 예측 불가능한 실매↔백테 괴리에 대비하십시오. diff --git a/kis_trader/backtest/results/optuna_momentum_tpe_20260828_004239.briefing.md b/kis_trader/backtest/results/optuna_momentum_tpe_20260828_004239.briefing.md new file mode 100644 index 0000000..6d66de2 --- /dev/null +++ b/kis_trader/backtest/results/optuna_momentum_tpe_20260828_004239.briefing.md @@ -0,0 +1,49 @@ +# Optuna 브리핑 — 모멘텀 (tpe) +기간: 2026-08-18 ~ 2026-08-28 (거래일≈11일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥1 | 후보 게이트: WR≥40% PF≥1 · min_trades≥1 +- min_trades=1: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 190건 · 사후합격(results_gated) 125건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 0.0%** · **적용 가능도 100.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈11일 — 표본 일수 상대적 양호 + - 거래 표본: +0.0p — 상위 후보 거래 23건 — 상대적 양호 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +0.0p — 사후합격 125건 · stable 10건 + - PnL 고원: +0.0p — 고원 비율 1% (1건) +- 임계값 분포 표본: results_gated 상위 30건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `use_ema_filter` | 1.0 | 1.0 ~ 1.0 | True (97%) | +| `ema_fast_period` | 14.0 | 13.0 ~ 15.75 | 14 (27%) | +| `ema_slow_period` | 39.0 | 33.5 ~ 41.0 | 42 (13%) | +| `mom_vol_mult` | 10.0 | 9.925 ~ 10.0 | 10.0 (73%) | +| `mom_vol_win` | 7.0 | 6.0 ~ 7.0 | 7 (40%) | +| `e_min_chg_pct` | 1.45 | 1.4 ~ 1.575 | 1.4 (33%) | +| `tp_pct` | 1.5 | 1.5 ~ 1.6 | 1.5 (67%) | +| `tp_max_pct` | 16.5 | 15.5 ~ 18.0 | 20.0 (20%) | +| `sl_pct` | 3.1 | 2.8 ~ 3.375 | 3.1 (13%) | +| `shoulder_min_high` | 3.7 | 3.0 ~ 3.975 | 3.7 (17%) | +| `shoulder_cut_pct` | 0.15 | 0.05 ~ 0.15 | 0.15 (43%) | +| `trail_pct` | 0.55 | 0.5 ~ 0.775 | 0.5 (30%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 이전 장에서는 +- 실매 동기간 청산 84건 · 승률 35% · 합산 PnL 54,432원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #118 · 거래 23건 · 승률 73.9% · PF 2.45 · PnL 65,428원 + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-28 00:42:57 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +「이전 장에서는」 +지난 11일간 모멘텀 백테스트에서 125개 유효 후보를 도출, #118은 23건 거래, 승률 73.9%, PnL 6.5만원 기록했습니다. 동기간 실매는 84건 청산, 승률 35%, PnL 5.4만원으로 백테와 실매 간 괴리(고스트·유니버스·틱 경로 차이)가 큽니다. 휴리스틱상 위험은 낮으나, 11일은 매우 짧은 표본 기간임을 경고합니다. + +「앞으로 장에서는」 +단 11일 백테스트는 과적합 및 표본 부족 위험이 극히 높습니다. 옵튜나 베스트를 즉시 실매에 적용 마십시오. 백테스트는 이상적 시뮬레이션일 뿐, 실제 매매는 유동성, 슬리피지 등으로 PnL 괴리가 발생합니다. 최소 5거래일 이상 재검증 및 웹백테스트 후, 소액/페이퍼 트레이딩으로 실매↔백테 괴리 및 손절 기능 안정성을 직접 관찰 후 적용하세요. 특정 종목 추천 및 확정 수익 약속은 불가합니다. diff --git a/kis_trader/backtest/results/optuna_momentum_tpe_20260828_044758.briefing.md b/kis_trader/backtest/results/optuna_momentum_tpe_20260828_044758.briefing.md new file mode 100644 index 0000000..bc1c1a3 --- /dev/null +++ b/kis_trader/backtest/results/optuna_momentum_tpe_20260828_044758.briefing.md @@ -0,0 +1,49 @@ +# Optuna 브리핑 — 모멘텀 (tpe) +기간: 2026-08-18 ~ 2026-08-28 (거래일≈11일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥36 | 후보 게이트: WR≥40% PF≥1 · min_trades≥36 +- min_trades=36: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 177건 · 사후합격(results_gated) 22건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 0.0%** · **적용 가능도 100.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈11일 — 표본 일수 상대적 양호 + - 거래 표본: +0.0p — 상위 후보 거래 54건 — 상대적 양호 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +0.0p — 사후합격 22건 · stable 2건 + - PnL 고원: +0.0p — 고원 비율 5% (1건) +- 임계값 분포 표본: results_gated 상위 22건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `use_ema_filter` | 1.0 | 0.0 ~ 1.0 | True (59%) | +| `ema_fast_period` | 7.0 | 6.0 ~ 10.75 | 5 (23%) | +| `ema_slow_period` | 35.5 | 27.5 ~ 45.25 | 26 (14%) | +| `mom_vol_mult` | 1.9 | 1.725 ~ 2.1 | 2.1 (18%) | +| `mom_vol_win` | 6.0 | 5.0 ~ 7.0 | 5 (32%) | +| `e_min_chg_pct` | 2.8 | 2.525 ~ 3.0 | 3.0 (46%) | +| `tp_pct` | 3.4 | 1.925 ~ 4.275 | 4.2 (9%) | +| `tp_max_pct` | 8.5 | 7.5 ~ 9.375 | 8.5 (18%) | +| `sl_pct` | 5.5 | 5.2 ~ 6.05 | 5.2 (9%) | +| `shoulder_min_high` | 3.85 | 3.4 ~ 4.0 | 4.0 (23%) | +| `shoulder_cut_pct` | 0.2 | 0.05 ~ 0.2875 | 0.05 (32%) | +| `trail_pct` | 2.85 | 1.525 ~ 3.3 | 3.3 (14%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 이전 장에서는 +- 실매 동기간 청산 84건 · 승률 35% · 합산 PnL 54,432원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #43 · 거래 54건 · 승률 55.6% · PF 1.45 · PnL 118,426원 + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-28 04:48:27 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +**이전 장에서는** +지난 11거래일간 모멘텀 전략 백테스트에서 Trial #43은 55.6% 승률로 11.8만원 PnL을 기록했습니다. 반면 실제 매매는 35% 승률, 5.4만원 PnL로, 백테스트와 상당한 괴리가 있었습니다. 이는 고스트 PnL, 유니버스, 틱 경로 등 실매 환경 차이 때문이며, 백테스트 숫자를 그대로 신뢰해선 안 됩니다. + +**앞으로 장에서는** +짧은 11일간의 데이터는 과적합 및 표본 부족 위험이 높습니다. Optuna 최고 결과라도 실매에 바로 적용하지 마시고, 백테스트 괴리 가능성을 항상 염두에 두세요. 최소 5거래일 이상 재검증 및 소액/모의 투자를 통해 손절 기준의 유효성을 먼저 확인해야 합니다. 특정 종목 추천이나 확정 수익은 불가하며, 신중한 접근을 강조합니다. diff --git a/kis_trader/backtest/results/optuna_momentum_tpe_20260830_012648.briefing.md b/kis_trader/backtest/results/optuna_momentum_tpe_20260830_012648.briefing.md new file mode 100644 index 0000000..c1df4f3 --- /dev/null +++ b/kis_trader/backtest/results/optuna_momentum_tpe_20260830_012648.briefing.md @@ -0,0 +1,72 @@ +# Optuna 브리핑 — 모멘텀 (tpe) +기간: 2026-08-24 ~ 2026-08-28 (거래일≈5일) · trials 5/5 +탐색 게이트: WR≥0% PF≥0 · min_trades≥10 | 후보 게이트: WR≥40% PF≥1 · min_trades≥10 +- min_trades=10: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 4건 · 사후합격(results_gated) 0건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 12.0%** · **적용 가능도 88.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈5일 — 표본 일수 상대적 양호 + - 거래 표본: +0.0p — 상위 후보 거래 77건 — 상대적 양호 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +12.0p — 사후합격 0건 (학습 4) — DB 적용 비권장 + - PnL 고원: +0.0p — 고원 비율 25% (1건) +- 임계값 분포 표본: results(learning) 상위 4건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `use_ema_filter` | 1.0 | 0.75 ~ 1.0 | True (75%) | +| `ema_fast_period` | 9.0 | 8.0 ~ 12.25 | 8 (50%) | +| `ema_slow_period` | 49.5 | 44.5 ~ 52.5 | 54 (25%) | +| `mom_vol_mult` | 4.65 | 4.5 ~ 6.0 | 4.5 (50%) | +| `mom_vol_win` | 9.5 | 8.5 ~ 10.0 | 10 (50%) | +| `e_min_chg_pct` | 0.9 | 0.375 ~ 1.8 | 1.4 (25%) | +| `tp_pct` | 4.55 | 3.25 ~ 6.375 | 1.6 (25%) | +| `tp_max_pct` | 10.25 | 8.75 ~ 12.625 | 6.5 (25%) | +| `sl_pct` | 5.15 | 4.05 ~ 5.575 | 5.1 (25%) | +| `shoulder_min_high` | 1.7 | 0.575 ~ 3.3 | 4.8 (25%) | +| `shoulder_cut_pct` | 0.425 | 0.125 ~ 0.725 | 0.15 (25%) | +| `trail_pct` | 2.45 | 1.575 ~ 3.15 | 0.3 (25%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 최종 선택 후보 (규칙 · DB 적용 전 확인) +- 아래는 **역할이 다른** 4종 후보입니다. mode Top10 1위 ≠ 익절 대표 · **DB 1순위는 사후합격(gated)** 입니다. +- mode pool: `positive` · scoring: `band_proximity_p25_p75` + +### 후보 4종 +- **① 사후합격 1위 (apply: gated)**: 없음 +- **② 안정 1위 (apply: stable)** (trial #1): PnL -23,495원 · WR 47.4% · PF 0.68 · 거래 19건 · 안정점수 -12332 · 호가ON spr6.7 r0.10 ask×23 · 익절9.6% 상한17.5% 손절6.7% · 휩쏘OFF +- **③ mode_combo (apply: mode)** (trial 없음(조립)): PnL -142,285원 · WR 37.1% · PF 0.32 · 거래 35건 · 안정점수 -47649 · 호가ON spr6.7 r0.10 ask×23 · 익절9.6% 상한17.5% 손절6.7% · 휩쏘OFF · tp 최빈 9.6% (1/4) +- **④ mode Top10 1위 (apply: consensus · 밴드 전형 trial)** (trial #3): PnL -133,782원 · WR 58.3% · PF 0.34 · 거래 48건 · 안정점수 -61522 · 근접 82.9% (29/43축 밴드안) · 호가ON spr6.3 r1.45 ask×54 · 익절3.8% 상한9.5% 손절5.2% · 휩쏘OFF + +- mode pool tp_pct 밴드 p25~p75: **3.25~6.375%** · Top10 1위 tp=3.8% → 밴드 **안** +- mode Top10 meta: pool 4건 · 축 43개 + +### 추천 (자동 · confirm 필수) +1. **사후합격 0건 → DB 적용 비권장.** 현행 DB 유지 + 유니버스·틱·기간 재검증. +2. 참고만: 학습 1위는 objective 최대일 뿐 사후 게이트 미통과일 수 있음. +2. **2차 narrow·대표 숫자 → ③ mode_combo** — 축별 최빈 조립 · trial 번호 없음 · 익절은 pool 최빈값 참고. +3. **④ mode Top10 1위** — 양수 pool 전형 trial(다축 밴드 근접). **익절 하나로 쓰지 말 것** · 표에서 「보기」→ consensus apply. +- ⚠ 안정 게이트 0건 → 안정 Top은 **점수순 폴백** (안정 게이트 0건 → 학습풀 안정점수순 TopN (마이너스 PnL 포함 · 상대비교)) + +## 이전 장에서는 +- 실매 동기간 청산 37건 · 승률 46% · 합산 PnL -53,280원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 학습 1위(사후게이트 미달 가능): trial #0 · 거래 77건 · 승률 53.2% · PF 0.86 · PnL -35,632원 +- 플러스/학습 후보는 있으나 승률·PF 사후 기준은 못 넘김 → **당장 DB 적용은 비권장.** + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 현재 사후합격 0건 → 파라미터 유지(현행 DB) + 유니버스/데이터 점검이 우선입니다. + +_생성: 2026-08-30 01:27:05 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +## 이전 장에서는 + +지난 5일간의 모멘텀 전략 Optuna 백테스트 결과, 학습용 후보 4건이 있었지만 **사후합격(실제 적용 기준)은 0건**입니다. 최고 학습 결과조차 손실(-3.5만원)로, 수익 기준을 충족하지 못했습니다. 동기간 실매는 -5.3만원으로 **백테스트와 괴리**가 있었습니다. 이는 **과적합 및 짧은 표본 기간**의 한계를 명확히 보여줍니다. 「mode Top10 1위」 등 특정 후보들이 있었으나, **DB 1순위는 사후합격된 조합이므로 익절 대표로 단정할 수 없음**을 강조합니다. + +## 앞으로 장에서는 + +**사후합격 0건**이므로 **현재 DB를 유지하고 유니버스/데이터 재검증이 우선**입니다. 이번 5일간의 짧은 백테스트는 **과적합 및 표본 부족 위험이 매우 높습니다.** Optuna 최적 조합을 바로 실매에 적용하는 것은 위험하며, 반드시 **다일(≥5거래일) 재검증과 실매↔백테 괴리(고스트퍼지 등)를 확인**해야 합니다. 시장은 언제든 변동성이 커질 수 있으니, 수익률보다 **손절 원칙이 지켜지는지**를 보십시오. 어떤 특정 종목 매수 추천이나 확정 수익을 약속드릴 수 없습니다. diff --git a/kis_trader/backtest/results/optuna_momentum_tpe_20260830_013051.briefing.md b/kis_trader/backtest/results/optuna_momentum_tpe_20260830_013051.briefing.md new file mode 100644 index 0000000..8614233 --- /dev/null +++ b/kis_trader/backtest/results/optuna_momentum_tpe_20260830_013051.briefing.md @@ -0,0 +1,72 @@ +# Optuna 브리핑 — 모멘텀 (tpe) +기간: 2026-08-24 ~ 2026-08-28 (거래일≈5일) · trials 5/5 +탐색 게이트: WR≥0% PF≥0 · min_trades≥10 | 후보 게이트: WR≥40% PF≥1 · min_trades≥10 +- min_trades=10: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 4건 · 사후합격(results_gated) 0건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 12.0%** · **적용 가능도 88.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈5일 — 표본 일수 상대적 양호 + - 거래 표본: +0.0p — 상위 후보 거래 70건 — 상대적 양호 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +12.0p — 사후합격 0건 (학습 4) — DB 적용 비권장 + - PnL 고원: +0.0p — 고원 비율 25% (1건) +- 임계값 분포 표본: results(learning) 상위 4건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `use_ema_filter` | 1.0 | 0.75 ~ 1.0 | True (75%) | +| `ema_fast_period` | 16.5 | 12.75 ~ 19.25 | 19 (25%) | +| `ema_slow_period` | 25.0 | 23.75 ~ 25.0 | 25 (75%) | +| `mom_vol_mult` | 5.1 | 3.175 ~ 7.25 | 3.3 (25%) | +| `mom_vol_win` | 8.0 | 7.75 ~ 8.5 | 8 (50%) | +| `e_min_chg_pct` | 1.35 | 1.05 ~ 1.8 | 3.0 (25%) | +| `tp_pct` | 5.55 | 4.4 ~ 7.65 | 4.5 (25%) | +| `tp_max_pct` | 12.75 | 11.875 ~ 15.125 | 20.0 (25%) | +| `sl_pct` | 3.0 | 2.475 ~ 3.8 | 2.9 (25%) | +| `shoulder_min_high` | 0.75 | 0.475 ~ 1.775 | 0.5 (25%) | +| `shoulder_cut_pct` | 0.275 | 0.1375 ~ 0.4125 | 0.1 (25%) | +| `trail_pct` | 2.8 | 1.775 ~ 3.675 | 2.0 (25%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 최종 선택 후보 (규칙 · DB 적용 전 확인) +- 아래는 **역할이 다른** 4종 후보입니다. mode Top10 1위 ≠ 익절 대표 · **DB 1순위는 사후합격(gated)** 입니다. +- mode pool: `positive` · scoring: `band_proximity_p25_p75` + +### 후보 4종 +- **① 사후합격 1위 (apply: gated)**: 없음 +- **② 안정 1위 (apply: stable)** (trial #4): PnL -51,870원 · WR 31.6% · PF 0.35 · 거래 19건 · 안정점수 -20090 · 호가ON spr7.6 r0.95 ask×17 · 익절10.8% 상한12.0% 손절3.1% · 휩쏘OFF +- **③ mode_combo (apply: mode)** (trial 없음(조립)): PnL -51,870원 · WR 31.6% · PF 0.35 · 거래 19건 · 안정점수 -20090 · 호가ON spr7.6 r1.15 ask×17 · 익절10.8% 상한12.0% 손절3.1% · 휩쏘OFF · tp 최빈 10.8% (1/4) +- **④ mode Top10 1위 (apply: consensus · 밴드 전형 trial)** (trial #2): PnL -156,853원 · WR 44.4% · PF 0.29 · 거래 54건 · 안정점수 -59494 · 근접 83.7% (28/43축 밴드안) · 호가ON spr2.8 r0.05 ask×77 · 익절6.6% 상한11.5% 손절5.9% · 휩쏘OFF + +- mode pool tp_pct 밴드 p25~p75: **4.4~7.65%** · Top10 1위 tp=6.6% → 밴드 **안** +- mode Top10 meta: pool 4건 · 축 43개 + +### 추천 (자동 · confirm 필수) +1. **사후합격 0건 → DB 적용 비권장.** 현행 DB 유지 + 유니버스·틱·기간 재검증. +2. 참고만: 학습 1위는 objective 최대일 뿐 사후 게이트 미통과일 수 있음. +2. **2차 narrow·대표 숫자 → ③ mode_combo** — 축별 최빈 조립 · trial 번호 없음 · 익절은 pool 최빈값 참고. +3. **④ mode Top10 1위** — 양수 pool 전형 trial(다축 밴드 근접). **익절 하나로 쓰지 말 것** · 표에서 「보기」→ consensus apply. +- ⚠ 안정 게이트 0건 → 안정 Top은 **점수순 폴백** (안정 게이트 0건 → 학습풀 안정점수순 TopN (마이너스 PnL 포함 · 상대비교)) + +## 이전 장에서는 +- 실매 동기간 청산 37건 · 승률 46% · 합산 PnL -53,280원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 학습 1위(사후게이트 미달 가능): trial #0 · 거래 70건 · 승률 40.0% · PF 0.57 · PnL -64,424원 +- 플러스/학습 후보는 있으나 승률·PF 사후 기준은 못 넘김 → **당장 DB 적용은 비권장.** + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 현재 사후합격 0건 → 파라미터 유지(현행 DB) + 유니버스/데이터 점검이 우선입니다. + +_생성: 2026-08-30 01:31:04 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +## 이전 장에서는 + +지난 5일간의 모멘텀 전략 백테스트 결과, 아쉽게도 **실제 적용 가능한(사후 합격) 전략은 단 한 건도 발견되지 않았습니다.** 학습 과정에서 몇몇 후보는 있었으나, **승률 40% 및 손익비(PF) 1.0 이상**의 엄격한 최종 기준을 통과하지 못했습니다. 특히 **5거래일이라는 짧은 기간**은 **과적합 위험**을 높이며, 백테스트상의 상위 결과(mode Top10 1위 등)가 **실제 시장에서 안정적인 수익으로 이어질 것이라는 보장은 없습니다.** 실제 거래 기록 또한 백테스트와 다른 흐름을 보였음을 확인했습니다. + +## 앞으로 장에서는 + +**현재로서는 새로운 전략을 실매에 즉시 적용하는 것을 강력히 비권장합니다.** 매우 **짧은 5일간의 백테스트** 결과는 **표본 부족과 과적합** 위험이 커, 일시적 우연에 의한 승률일 가능성이 높습니다. 향후에는 **최소 5거래일 이상**의 충분한 기간 동안 **재검증**을 수행하고, **실매와의 괴리**를 일으킬 수 있는 **고스트 슬리피지(Ghost Slippage)** 등의 요소를 항상 경계해야 합니다. 백테스트 상의 **긍정적인 PnL이 곧 확정 수익을 의미하지 않으며**, **특정 종목 매수 추천도 아닙니다.** 오직 **사후 합격(gated)된 검증된 전략만** 실제 운용에 참고해야 합니다. diff --git a/kis_trader/backtest/results/optuna_momentum_tpe_20260830_021942.briefing.md b/kis_trader/backtest/results/optuna_momentum_tpe_20260830_021942.briefing.md new file mode 100644 index 0000000..43437e6 --- /dev/null +++ b/kis_trader/backtest/results/optuna_momentum_tpe_20260830_021942.briefing.md @@ -0,0 +1,70 @@ +# Optuna 브리핑 — 모멘텀 (tpe) +기간: 2026-08-24 ~ 2026-08-28 (거래일≈5일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥10 | 후보 게이트: WR≥40% PF≥1 · min_trades≥10 +- min_trades=10: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 173건 · 사후합격(results_gated) 51건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 0.0%** · **적용 가능도 100.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈5일 — 표본 일수 상대적 양호 + - 거래 표본: +0.0p — 상위 후보 거래 17건 — 상대적 양호 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +0.0p — 사후합격 51건 · stable 10건 + - PnL 고원: +0.0p — 고원 비율 2% (1건) +- 임계값 분포 표본: results_gated 상위 30건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `use_ema_filter` | 1.0 | 1.0 ~ 1.0 | True (87%) | +| `ema_fast_period` | 10.0 | 9.0 ~ 12.0 | 10 (20%) | +| `ema_slow_period` | 38.0 | 35.0 ~ 55.75 | 58 (10%) | +| `mom_vol_mult` | 4.8 | 4.5 ~ 5.475 | 4.8 (13%) | +| `mom_vol_win` | 3.0 | 3.0 ~ 4.0 | 3 (70%) | +| `e_min_chg_pct` | 1.7 | 1.6 ~ 1.7 | 1.7 (33%) | +| `tp_pct` | 1.55 | 1.5 ~ 1.6 | 1.5 (50%) | +| `tp_max_pct` | 16.5 | 15.0 ~ 16.5 | 16.5 (30%) | +| `sl_pct` | 5.2 | 4.4 ~ 6.4 | 6.6 (13%) | +| `shoulder_min_high` | 1.6 | 1.4 ~ 3.1 | 1.4 (20%) | +| `shoulder_cut_pct` | 0.6 | 0.55 ~ 0.65 | 0.6 (33%) | +| `trail_pct` | 2.2 | 1.85 ~ 2.475 | 2.5 (17%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 최종 선택 후보 (규칙 · DB 적용 전 확인) +- 아래는 **역할이 다른** 4종 후보입니다. mode Top10 1위 ≠ 익절 대표 · **DB 1순위는 사후합격(gated)** 입니다. +- mode pool: `positive` · scoring: `band_proximity_p25_p75` + +### 후보 4종 +- **① 사후합격 1위 (apply: gated)** (trial #131): PnL 36,846원 · WR 70.6% · PF 4.83 · 거래 17건 · 안정점수 -1422 · 호가ON spr8.0 r0.45 ask×9 · 익절1.6% 상한16.5% 손절4.0% · 휩쏘ON sub15s lb60s dip0.70% tol0.20% +- **② 안정 1위 (apply: stable)** (trial #139): PnL 17,310원 · WR 50.0% · PF 2.14 · 거래 14건 · 안정점수 1036 · 호가ON spr8.0 r0.15 ask×11 · 익절3.2% 상한16.5% 손절4.5% · 휩쏘ON sub15s lb120s dip0.30% tol0.10% +- **③ mode_combo (apply: mode)** (trial 없음(조립)): PnL 43,094원 · WR 72.7% · PF 2.78 · 거래 22건 · 안정점수 -1645 · 호가ON spr8.0 r0.45 ask×12 · 익절1.5% 상한16.5% 손절6.6% · 휩쏘ON sub15s lb60s dip0.70% tol0.20% · tp 최빈 1.5% (25/56) +- **④ mode Top10 1위 (apply: consensus · 밴드 전형 trial)** (trial #38): PnL 13,862원 · WR 57.1% · PF 1.28 · 거래 28건 · 안정점수 -5311 · 근접 99.6% (42/43축 밴드안) · 호가ON spr8.0 r0.45 ask×13 · 익절1.6% 상한17.0% 손절4.7% · 휩쏘ON sub15s lb60s dip0.70% tol0.20% + +- mode pool tp_pct 밴드 p25~p75: **1.5~2.925%** · Top10 1위 tp=1.6% → 밴드 **안** +- mode Top10 meta: pool 56건 · 축 43개 + +### 추천 (자동 · confirm 필수) +1. **실매 DB 적용 1순위 → ① 사후합격 1위** (`optunaApply gated`) — WR/PF·min_trades 사후 통과. +2. **변동성·손실일 줄이기 → ② 안정 1위** (`stable`) — PnL보다 일별 안정 우선 시. +3. **2차 narrow·대표 숫자 → ③ mode_combo** — 축별 최빈 조립 · trial 번호 없음 · 익절은 pool 최빈값 참고. +4. **④ mode Top10 1위** — 양수 pool 전형 trial(다축 밴드 근접). **익절 하나로 쓰지 말 것** · 표에서 「보기」→ consensus apply. + +## 이전 장에서는 +- 실매 동기간 청산 37건 · 승률 46% · 합산 PnL -53,280원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #131 · 거래 17건 · 승률 70.6% · PF 4.83 · PnL 36,846원 + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-30 02:19:56 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +저희 주식 퀀트 헤지펀드 리스크 매니저로서 이번 Optuna TPE 모멘텀 전략 결과에 대한 코멘트를 드립니다. + +### 이전 장에서는 +실매 청산 37건에서 총 -53,280원 손실을 기록했습니다. 백테스트 상 사후합격 1위 전략은 17건 거래, 승률 70.6%로 36,846원의 PnL을 보였으나, 이처럼 백테스트와 실매 간의 큰 괴리(Ghost purge, 유니버스 차이 등)는 정상적인 현상임을 인지해야 합니다. + +### 앞으로 장에서는 +제시된 과적합 위험 0.0%는 휴리스틱 판단이며, 5거래일의 짧은 백테스트는 과적합 및 표본 부족 위험이 높습니다. Optuna 결과를 실매에 바로 적용하기보다 다일 재검증 후 검토하십시오. DB 적용은 `results_gated` 후보만 고려하며, `mode Top10 1위`는 익절 대표가 아닌 파라미터 밴드의 전형입니다. 시장 변동 시 손절 기능이 먼저 깨지므로, 승률보다 손절 방어를 우선 확인하세요. 페이퍼/소액 모의로 관찰 후 확대하며, 특정 종목 추천이나 확정 수익 약속은 불가합니다. diff --git a/kis_trader/backtest/results/optuna_momentum_tpe_20260830_025040.briefing.md b/kis_trader/backtest/results/optuna_momentum_tpe_20260830_025040.briefing.md new file mode 100644 index 0000000..bd313c3 --- /dev/null +++ b/kis_trader/backtest/results/optuna_momentum_tpe_20260830_025040.briefing.md @@ -0,0 +1,68 @@ +# Optuna 브리핑 — 모멘텀 (tpe) +기간: 2026-08-24 ~ 2026-08-28 (거래일≈5일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥10 | 후보 게이트: WR≥40% PF≥1 · min_trades≥10 +- min_trades=10: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 158건 · 사후합격(results_gated) 62건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 0.0%** · **적용 가능도 100.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈5일 — 표본 일수 상대적 양호 + - 거래 표본: +0.0p — 상위 후보 거래 63건 — 상대적 양호 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +0.0p — 사후합격 62건 · stable 10건 + - PnL 고원: +0.0p — 고원 비율 2% (1건) +- 임계값 분포 표본: results_gated 상위 30건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `use_ema_filter` | 1.0 | 1.0 ~ 1.0 | True (80%) | +| `ema_fast_period` | 15.0 | 14.0 ~ 17.75 | 14 (23%) | +| `ema_slow_period` | 43.0 | 42.0 ~ 46.0 | 43 (17%) | +| `mom_vol_mult` | 1.0 | 1.0 ~ 1.0 | 1.0 (90%) | +| `mom_vol_win` | 5.0 | 4.25 ~ 5.75 | 5 (47%) | +| `e_min_chg_pct` | 2.4 | 2.3 ~ 2.6 | 2.3 (20%) | +| `tp_pct` | 3.7 | 2.925 ~ 4.25 | 3.7 (10%) | +| `tp_max_pct` | 17.5 | 16.5 ~ 18.0 | 17.5 (23%) | +| `sl_pct` | 7.1 | 6.725 ~ 7.3 | 7.3 (17%) | +| `shoulder_min_high` | 3.2 | 3.0 ~ 3.675 | 3.0 (17%) | +| `shoulder_cut_pct` | 0.3 | 0.2 ~ 0.3875 | 0.35 (20%) | +| `trail_pct` | 1.0 | 0.7 ~ 1.175 | 0.7 (20%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 최종 선택 후보 (규칙 · DB 적용 전 확인) +- 아래는 **역할이 다른** 4종 후보입니다. mode Top10 1위 ≠ 익절 대표 · **DB 1순위는 사후합격(gated)** 입니다. +- mode pool: `positive` · scoring: `band_proximity_p25_p75` + +### 후보 4종 +- **① 사후합격 1위 (apply: gated)** (trial #83): PnL 114,986원 · WR 58.7% · PF 2.14 · 거래 63건 · 안정점수 8210 · 호가ON spr5.0 r0.85 ask×12 · 익절1.8% 상한16.0% 손절6.4% · 휩쏘ON sub60s lb180s dip0.90% tol0.05% +- **② 안정 1위 (apply: stable)** (trial #90): PnL 105,272원 · WR 58.3% · PF 1.86 · 거래 72건 · 안정점수 12983 · 호가ON spr4.6 r0.85 ask×13 · 익절1.6% 상한16.5% 손절6.3% · 휩쏘ON sub60s lb180s dip0.90% tol0.05% +- **③ mode_combo (apply: mode)** (trial 없음(조립)): PnL 32,439원 · WR 58.6% · PF 1.51 · 거래 29건 · 안정점수 -13112 · 호가ON spr5.3 r0.60 ask×8 · 익절1.5% 상한18.0% 손절6.7% · 휩쏘ON sub60s lb180s dip0.90% tol0.05% · tp 최빈 1.5% (5/62) +- **④ mode Top10 1위 (apply: consensus · 밴드 전형 trial)** (trial #61): PnL 50,041원 · WR 63.0% · PF 1.77 · 거래 27건 · 안정점수 -7612 · 근접 97.5% (40/43축 밴드안) · 호가ON spr4.5 r0.65 ask×8 · 익절3.4% 상한16.5% 손절6.7% · 휩쏘ON sub60s lb180s dip1.00% tol0.05% + +- mode pool tp_pct 밴드 p25~p75: **2.8~4.475%** · Top10 1위 tp=3.4% → 밴드 **안** +- mode Top10 meta: pool 62건 · 축 43개 + +### 추천 (자동 · confirm 필수) +1. **실매 DB 적용 1순위 → ① 사후합격 1위** (`optunaApply gated`) — WR/PF·min_trades 사후 통과. +2. **변동성·손실일 줄이기 → ② 안정 1위** (`stable`) — PnL보다 일별 안정 우선 시. +3. **2차 narrow·대표 숫자 → ③ mode_combo** — 축별 최빈 조립 · trial 번호 없음 · 익절은 pool 최빈값 참고. +4. **④ mode Top10 1위** — 양수 pool 전형 trial(다축 밴드 근접). **익절 하나로 쓰지 말 것** · 표에서 「보기」→ consensus apply. + +## 이전 장에서는 +- 실매 동기간 청산 37건 · 승률 46% · 합산 PnL -53,280원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #83 · 거래 63건 · 승률 58.7% · PF 2.14 · PnL 114,986원 + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-30 02:50:57 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +**「이전 장에서는」** +모멘텀 전략 백테스트 결과, 사후합격 1위(trial #83)가 5일간 63건 거래로 PnL 11.5만원을 기록했습니다. 하지만 실제 매매는 동기간 PnL -5.3만원으로 큰 괴리를 보였습니다. 이는 고스트 PnL, 유니버스, 틱 경로 등 백테스트와 실매 환경의 근본적인 차이 때문입니다. + +**「앞으로 장에서는」** +짧은 5일 백테스트는 과적합 위험 및 표본 부족 가능성이 높습니다. Optuna 최적값을 바로 실매에 적용하지 말고, 반드시 다일 재검증 및 웹백테스트, 소액 모의 매매로 신중히 관찰하세요. `mode Top10 1위`는 파라미터 경향 파악용이며, 익절 대표로 단정하지 마세요. 실제 적용은 `사후합격 1위` 후보로 합니다. 장세 변화 시 손절 작동 여부를 최우선으로 확인해야 하며, 어떤 결과도 확정 수익을 약속하지 않습니다. diff --git a/kis_trader/backtest/results/optuna_momentum_tpe_20260830_104429.briefing.md b/kis_trader/backtest/results/optuna_momentum_tpe_20260830_104429.briefing.md new file mode 100644 index 0000000..4812a62 --- /dev/null +++ b/kis_trader/backtest/results/optuna_momentum_tpe_20260830_104429.briefing.md @@ -0,0 +1,68 @@ +# Optuna 브리핑 — 모멘텀 (tpe) +기간: 2026-08-24 ~ 2026-08-28 (거래일≈5일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥10 | 후보 게이트: WR≥40% PF≥1 · min_trades≥10 +- min_trades=10: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 168건 · 사후합격(results_gated) 51건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 0.0%** · **적용 가능도 100.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈5일 — 표본 일수 상대적 양호 + - 거래 표본: +0.0p — 상위 후보 거래 17건 — 상대적 양호 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +0.0p — 사후합격 51건 · stable 9건 + - PnL 고원: +0.0p — 고원 비율 2% (1건) +- 임계값 분포 표본: results_gated 상위 30건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `use_ema_filter` | 1.0 | 1.0 ~ 1.0 | True (93%) | +| `ema_fast_period` | 18.0 | 17.0 ~ 19.0 | 18 (27%) | +| `ema_slow_period` | 53.5 | 51.0 ~ 58.5 | 59 (13%) | +| `mom_vol_mult` | 9.0 | 8.7 ~ 10.0 | 10.0 (40%) | +| `mom_vol_win` | 10.0 | 9.0 ~ 10.0 | 10 (67%) | +| `e_min_chg_pct` | 0.85 | 0.3 ~ 2.6 | 2.6 (23%) | +| `tp_pct` | 1.6 | 1.5 ~ 2.65 | 1.5 (43%) | +| `tp_max_pct` | 20.0 | 19.5 ~ 20.0 | 20.0 (70%) | +| `sl_pct` | 1.9 | 1.625 ~ 2.2 | 1.6 (17%) | +| `shoulder_min_high` | 1.2 | 1.1 ~ 1.375 | 1.1 (27%) | +| `shoulder_cut_pct` | 0.05 | 0.05 ~ 0.15 | 0.05 (53%) | +| `trail_pct` | 3.4 | 3.125 ~ 3.575 | 3.6 (23%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 최종 선택 후보 (규칙 · DB 적용 전 확인) +- 아래는 **역할이 다른** 4종 후보입니다. mode Top10 1위 ≠ 익절 대표 · **DB 1순위는 사후합격(gated)** 입니다. +- mode pool: `positive` · scoring: `band_proximity_p25_p75` + +### 후보 4종 +- **① 사후합격 1위 (apply: gated)** (trial #127): PnL 28,760원 · WR 70.6% · PF 2.17 · 거래 17건 · 안정점수 -2996 · 호가ON spr0.3 r0.95 ask×71 · 익절1.5% 상한20.0% 손절2.7% · 휩쏘ON sub30s lb90s dip0.90% tol0.30% +- **② 안정 1위 (apply: stable)** (trial #41): PnL 10,044원 · WR 68.2% · PF 1.32 · 거래 22건 · 안정점수 871 · 호가ON spr1.5 r1.50 ask×37 · 익절8.3% 상한15.0% 손절2.1% · 휩쏘ON sub45s lb180s dip0.70% tol0.20% +- **③ mode_combo (apply: mode)** (trial 없음(조립)): PnL 22,264원 · WR 73.7% · PF 1.74 · 거래 19건 · 안정점수 -1895 · 호가ON spr1.0 r0.90 ask×71 · 익절1.5% 상한20.0% 손절1.6% · 휩쏘ON sub30s lb120s dip1.00% tol0.25% · tp 최빈 1.5% (19/51) +- **④ mode Top10 1위 (apply: consensus · 밴드 전형 trial)** (trial #115): PnL 17,550원 · WR 68.4% · PF 1.53 · 거래 19건 · 안정점수 -2724 · 근접 100.0% (43/43축 밴드안) · 호가ON spr0.8 r1.00 ask×68 · 익절1.5% 상한20.0% 손절2.0% · 휩쏘ON sub30s lb120s dip1.00% tol0.25% + +- mode pool tp_pct 밴드 p25~p75: **1.5~3.15%** · Top10 1위 tp=1.5% → 밴드 **안** +- mode Top10 meta: pool 51건 · 축 43개 + +### 추천 (자동 · confirm 필수) +1. **실매 DB 적용 1순위 → ① 사후합격 1위** (`optunaApply gated`) — WR/PF·min_trades 사후 통과. +2. **변동성·손실일 줄이기 → ② 안정 1위** (`stable`) — PnL보다 일별 안정 우선 시. +3. **2차 narrow·대표 숫자 → ③ mode_combo** — 축별 최빈 조립 · trial 번호 없음 · 익절은 pool 최빈값 참고. +4. **④ mode Top10 1위** — 양수 pool 전형 trial(다축 밴드 근접). **익절 하나로 쓰지 말 것** · 표에서 「보기」→ consensus apply. + +## 이전 장에서는 +- 실매 동기간 청산 37건 · 승률 46% · 합산 PnL -53,280원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #127 · 거래 17건 · 승률 70.6% · PF 2.17 · PnL 28,760원 + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-30 10:44:41 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +**「이전 장에서는」** +지난주 실매 결과는 청산 37건, 승률 46%, 합산 PnL -53,280원이었습니다. 백테스트 Optuna 사후합격 1위(trial #127)는 거래 17건, 승률 70.6%, PnL 28,760원으로 나타났지만, 이는 백테와 실매 간 고스트·유니버스·틱 경로 차이로 인한 괴리입니다. + +**「앞으로 장에서는」** +단 5일간의 백테 결과는 과적합 및 표본 부족 위험이 높습니다. Optuna best를 바로 실매에 적용하기보다, 다일(5거래일 이상) 재검증과 웹백테스트 후 검토가 필수입니다. 실매 적용 시에는 `results_gated` 후보만 고려하고, '내일도 같은 장'은 없음을 명심하여 페이퍼/소액으로 1~2일 관찰 후 확대하세요. 급변하는 장에서는 승률보다 손절 방어가 최우선입니다. 특정 종목 추천이나 확정 수익은 약속하지 않습니다. diff --git a/kis_trader/backtest/results/optuna_momentum_tpe_20260830_110838.briefing.md b/kis_trader/backtest/results/optuna_momentum_tpe_20260830_110838.briefing.md new file mode 100644 index 0000000..0b98e0a --- /dev/null +++ b/kis_trader/backtest/results/optuna_momentum_tpe_20260830_110838.briefing.md @@ -0,0 +1,70 @@ +# Optuna 브리핑 — 모멘텀 (tpe) +기간: 2026-08-24 ~ 2026-08-28 (거래일≈5일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥10 | 후보 게이트: WR≥40% PF≥1 · min_trades≥10 +- min_trades=10: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 184건 · 사후합격(results_gated) 80건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 0.0%** · **적용 가능도 100.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈5일 — 표본 일수 상대적 양호 + - 거래 표본: +0.0p — 상위 후보 거래 25건 — 상대적 양호 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +0.0p — 사후합격 80건 · stable 10건 + - PnL 고원: +0.0p — 고원 비율 1% (1건) +- 임계값 분포 표본: results_gated 상위 30건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `use_ema_filter` | 0.0 | 0.0 ~ 0.0 | False (100%) | +| `ema_fast_period` | 10.0 | 9.0 ~ 10.75 | 10 (30%) | +| `ema_slow_period` | 48.0 | 45.25 ~ 50.75 | 49 (13%) | +| `mom_vol_mult` | 10.0 | 10.0 ~ 10.0 | 10.0 (83%) | +| `mom_vol_win` | 7.0 | 7.0 ~ 7.0 | 7 (67%) | +| `e_min_chg_pct` | 2.3 | 1.825 ~ 2.5 | 2.5 (17%) | +| `tp_pct` | 1.5 | 1.5 ~ 1.6 | 1.5 (60%) | +| `tp_max_pct` | 9.75 | 7.125 ~ 11.875 | 16.5 (10%) | +| `sl_pct` | 4.8 | 2.35 ~ 5.65 | 2.1 (7%) | +| `shoulder_min_high` | 3.9 | 3.6 ~ 4.4 | 4.4 (17%) | +| `shoulder_cut_pct` | 0.05 | 0.05 ~ 0.05 | 0.05 (87%) | +| `trail_pct` | 2.6 | 0.5 ~ 2.875 | 2.8 (20%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 최종 선택 후보 (규칙 · DB 적용 전 확인) +- 아래는 **역할이 다른** 4종 후보입니다. mode Top10 1위 ≠ 익절 대표 · **DB 1순위는 사후합격(gated)** 입니다. +- mode pool: `positive` · scoring: `band_proximity_p25_p75` + +### 후보 4종 +- **① 사후합격 1위 (apply: gated)** (trial #121): PnL 46,925원 · WR 68.0% · PF 2.32 · 거래 25건 · 안정점수 4491 · 호가ON spr1.4 r0.25 ask×35 · 익절1.5% 상한15.0% 손절2.1% · 휩쏘OFF +- **② 안정 1위 (apply: stable)** (trial #112): PnL 46,470원 · WR 69.6% · PF 2.52 · 거래 23건 · 안정점수 6492 · 호가ON spr1.5 r0.30 ask×22 · 익절1.5% 상한16.5% 손절2.3% · 휩쏘OFF +- **③ mode_combo (apply: mode)** (trial 없음(조립)): PnL 18,260원 · WR 60.0% · PF 1.34 · 거래 25건 · 안정점수 -3252 · 호가ON spr5.1 r0.05 ask×65 · 익절1.5% 상한6.5% 손절2.3% · 휩쏘OFF · tp 최빈 1.5% (48/80) +- **④ mode Top10 1위 (apply: consensus · 밴드 전형 trial)** (trial #93): PnL 42,308원 · WR 78.3% · PF 2.00 · 거래 23건 · 안정점수 -2222 · 근접 99.7% (42/43축 밴드안) · 호가ON spr4.7 r0.25 ask×46 · 익절1.5% 상한9.0% 손절5.8% · 휩쏘OFF + +- mode pool tp_pct 밴드 p25~p75: **1.5~1.6%** · Top10 1위 tp=1.5% → 밴드 **안** +- mode Top10 meta: pool 80건 · 축 43개 + +### 추천 (자동 · confirm 필수) +1. **실매 DB 적용 1순위 → ① 사후합격 1위** (`optunaApply gated`) — WR/PF·min_trades 사후 통과. +2. **변동성·손실일 줄이기 → ② 안정 1위** (`stable`) — PnL보다 일별 안정 우선 시. +3. **2차 narrow·대표 숫자 → ③ mode_combo** — 축별 최빈 조립 · trial 번호 없음 · 익절은 pool 최빈값 참고. +4. **④ mode Top10 1위** — 양수 pool 전형 trial(다축 밴드 근접). **익절 하나로 쓰지 말 것** · 표에서 「보기」→ consensus apply. + +## 이전 장에서는 +- 실매 동기간 청산 37건 · 승률 46% · 합산 PnL -53,280원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #121 · 거래 25건 · 승률 68.0% · PF 2.32 · PnL 46,925원 + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-30 11:08:52 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +## 이전 장에서는 + +지난 5거래일간 실매매는 총 37건의 거래에서 46% 승률로 53,280원의 손실을 기록했습니다. 이는 같은 기간 Optuna 백테스트 사후합격 1위(25건, 승률 68%, PnL 46,925원)와 큰 괴리를 보입니다. 백테스트 결과는 고스트 퍼지, 유니버스 차이 등으로 실제 시장과 다를 수 있음을 분명히 인지해야 합니다. + +## 앞으로 장에서는 + +5거래일 백테스트는 과적합 및 표본 부족 위험이 매우 높습니다. 휴리스틱상 위험이 낮더라도 `사후합격 1위` 결과를 맹신해 즉시 실매에 적용하는 것은 금물입니다. 실매 적용 전 반드시 페이퍼/소액 거래로 1~2일 관찰하고, 다일 재검증 후 신중하게 검토하십시오. `mode Top10 1위`는 익절 대표가 아니며, 최종 DB 1순위는 `사후합격(gated)`입니다. 장세 변화 시 손절 파라미터가 깨지기 쉬우니 승률보다 손절 작동 여부를 최우선으로 확인하세요. diff --git a/kis_trader/backtest/results/optuna_scalp_fast_20260826_030850.briefing.md b/kis_trader/backtest/results/optuna_scalp_fast_20260826_030850.briefing.md new file mode 100644 index 0000000..3c9dbb0 --- /dev/null +++ b/kis_trader/backtest/results/optuna_scalp_fast_20260826_030850.briefing.md @@ -0,0 +1,46 @@ +# Optuna 브리핑 — 스캘핑 (fast) +기간: 2026-08-25 ~ 2026-08-25 (거래일≈1일) · trials 5/5 +탐색 게이트: WR≥0% PF≥0 · min_trades≥1 | 후보 게이트: WR≥40% PF≥1 · min_trades≥1 +- min_trades=1: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 4건 · 사후합격(results_gated) 4건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 50.0%** · **적용 가능도 50.0%** · 판정: **주의** — 적용 가능도 중간 — 다일(≥5일) 재검증·웹백테 후 소액만 + - 표본 장일: +40.0p — 거래일≈1일 — 단일 장 과적합 위험 최대 + - 거래 표본: +0.0p — 상위 후보 거래 10건 — 상대적 양호 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +10.0p — gated/all=4/4 — 단일에 대부분 통과(필터 느슨·노이즈) + - PnL 고원: +0.0p — 고원 비율 25% (1건) +- 임계값 분포 표본: results_gated 상위 4건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `rsi_period` | 7.0 | 6.25 ~ 7.0 | 7 (75%) | +| `rsi_oversold` | 23.0 | 22.5 ~ 23.5 | 23.0 (50%) | +| `sl_pct` | 4.05 | 2.5 ~ 5.6 | 2.5 (50%) | +| `tp_pct` | 2.75 | 2.375 ~ 3.7 | 2.0 (25%) | +| `tp_max_pct` | 4.5 | 4.0 ~ 6.375 | 4.0 (50%) | +| `shoulder_min_high` | 1.35 | 0.8 ~ 2.175 | 0.8 (50%) | +| `shoulder_cut_pct` | 0.3 | 0.2 ~ 0.4 | 0.4 (50%) | +| `drop_rate` | 3.0 | 2.75 ~ 3.25 | 3.0 (50%) | +| `high_chase_thr` | 0.98 | 0.98 ~ 0.985 | 0.98 (75%) | +| `vol_mult` | 1.35 | 1.125 ~ 1.5 | 1.5 (50%) | +| `require_reversal_candle` | 0.5 | 0.0 ~ 1.0 | False (50%) | +| `cooldown_min` | 3.0 | 1.0 ~ 5.0 | 1 (50%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 이전 장에서는 +- 실매 동기간 청산 9건 · 승률 67% · 합산 PnL 33,264원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #0 · 거래 10건 · 승률 80.0% · PF 93.07 · PnL 63,344원 +- 표본 장이 1일뿐이라, ‘이전 장에 잘 먹힌 숫자’일 뿐 **추세가 바뀌면 바로 깨질 수 있습니다.** + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-26 03:08:52 (rule)_ + +## AI 코멘트 + +_(미생성: api_call_failed. 구글 키는 GEMINI_API_KEY 를 DB/환경에 넣으세요 — kis_trader/backtest/optuna_briefing.py 모듈 docstring 참고)_ diff --git a/kis_trader/backtest/results/optuna_scalp_tpe_20260823_180528.briefing.md b/kis_trader/backtest/results/optuna_scalp_tpe_20260823_180528.briefing.md new file mode 100644 index 0000000..a386cb9 --- /dev/null +++ b/kis_trader/backtest/results/optuna_scalp_tpe_20260823_180528.briefing.md @@ -0,0 +1,34 @@ +# Optuna 브리핑 — 스캘핑 (tpe) +기간: 2026-08-21 ~ 2026-08-21 (거래일≈1일) · trials 5/5 +탐색 게이트: WR≥0% PF≥0 · min_trades≥1 | 후보 게이트: WR≥40% PF≥1 · min_trades≥1 +- min_trades=1: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 0건 · 사후합격(results_gated) 0건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 73.0%** · **적용 가능도 27.0%** · 판정: **비권장** — 과적합·표본부족 위험 높음 — 실매 DB 즉시 적용 비권장 + - 표본 장일: +40.0p — 거래일≈1일 — 단일 장 과적합 위험 최대 + - 거래 표본: +25.0p — 통과 후보 없음 — 적용 불가 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +8.0p — 학습·gated 모두 비어 있음 + - PnL 고원: +0.0p — 고원 판정 스킵 +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 이전 장에서는 +- 실매(2026-08-21~2026-08-21) 해당 전략 청산 기록 없음(또는 전략명 불일치). +- 이 기간 백테에서는 **유의미한 통과 조합이 거의 없었습니다.** 유니버스·틱·손절·시간창을 먼저 점검하세요 (돌파 0건이면 특히). + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 현재 사후합격 0건 → 파라미터 유지(현행 DB) + 유니버스/데이터 점검이 우선입니다. + +_생성: 2026-08-23 18:05:28 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +**「이전 장에서는」** +단 하루(1일)의 백테스트로는 유의미한 전략 파라미터 조합을 전혀 찾지 못했습니다. 이는 과적합 위험을 극도로 높이며, 표본 부족으로 신뢰할 수 있는 결과가 없음을 의미합니다. 이대로는 우연한 승률에 불과할 가능성이 큽니다. + +**「앞으로 장에서는」** +현 전략은 과적합 위험이 73%로 매우 높아, 실매 적용이 '비권장'됩니다. 짧은 백테스트 기간은 실제 시장에서 백테스트와 다른 '고스트 P&L'로 이어질 가능성이 큽니다. 최소 5거래일 이상의 충분한 기간으로 재검증하고, 특히 손절매 기능이 제대로 작동하는지 우선 확인해야 합니다. 현재로서는 전략 수정 및 데이터 점검이 시급합니다. diff --git a/kis_trader/backtest/results/optuna_scalp_tpe_20260823_183144.briefing.md b/kis_trader/backtest/results/optuna_scalp_tpe_20260823_183144.briefing.md new file mode 100644 index 0000000..99c0ed0 --- /dev/null +++ b/kis_trader/backtest/results/optuna_scalp_tpe_20260823_183144.briefing.md @@ -0,0 +1,51 @@ +# Optuna 브리핑 — 스캘핑 (tpe) +기간: 2026-08-18 ~ 2026-08-21 (거래일≈4일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥1 | 후보 게이트: WR≥40% PF≥1 · min_trades≥1 +- min_trades=1: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 163건 · 사후합격(results_gated) 75건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 16.0%** · **적용 가능도 84.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +16.0p — 거래일≈4일 — 다일 재검증 권장(≥5일) + - 거래 표본: +0.0p — 상위 후보 거래 15건 — 상대적 양호 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +0.0p — 사후합격 75건 · stable 10건 + - PnL 고원: +0.0p — 고원 비율 1% (1건) +- 임계값 분포 표본: results_gated 상위 30건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `rsi_period` | 4.0 | 3.0 ~ 4.0 | 3 (43%) | +| `rsi_oversold` | 20.5 | 18.5 ~ 22.25 | 21.0 (10%) | +| `rsi_overbought` | 73.75 | 68.5 ~ 76.375 | 79.5 (10%) | +| `sl_pct` | 5.4 | 4.7 ~ 5.7 | 5.6 (17%) | +| `tp_pct` | 4.75 | 2.85 ~ 6.2 | 5.8 (7%) | +| `tp_max_pct` | 9.0 | 6.5 ~ 10.5 | 12.0 (13%) | +| `drop_rate` | 4.85 | 3.425 ~ 6.075 | 4.5 (7%) | +| `shoulder_min_high` | 1.7 | 1.625 ~ 1.9 | 1.7 (33%) | +| `shoulder_cut_pct` | 0.1 | 0.05 ~ 0.15 | 0.05 (43%) | +| `cooldown_min` | 4.5 | 3.0 ~ 9.0 | 4 (20%) | +| `high_chase_thr` | 0.96 | 0.95 ~ 0.97 | 0.95 (33%) | +| `max_daily_chg` | 16.5 | 14.25 ~ 18.75 | 15.0 (13%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 이전 장에서는 +- 실매 동기간 청산 8건 · 승률 0% · 합산 PnL -32,761원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #65 · 거래 15건 · 승률 53.3% · PF 2.06 · PnL 20,428원 + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-23 18:31:59 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +저희 퀀트 헤지펀드 리스크 매니저로서, Optuna 스캘핑 전략의 백테스트 결과를 토대로 아래와 같이 코멘트 드립니다. + +「이전 장에서는」 +이번 Optuna 백테스트는 4거래일(2026-08-18~21)의 짧은 기간 동안 진행되었습니다. 사후합격 1위 전략(Trial #65)은 백테스트에서 15번의 거래로 53.3% 승률, PF 2.06, PnL 20,428원을 기록했습니다. 하지만 동일 기간 실제 매매에서는 8건의 거래에서 승률 0%로 -32,761원의 손실이 발생했습니다. 이는 백테스트와 실제 매매 환경 간의 괴리(고스트퍼지, 유니버스 차이 등)를 명확히 보여줍니다. + +「앞으로 장에서는」 +현재 백테스트는 4일이라는 매우 짧은 표본 기간으로 인해 과적합 및 표본 부족 우려가 큽니다. 앞선 실매 결과처럼 백테스트 수익이 실제 수익으로 이어지지 않는 괴리(고스트퍼지 등)가 발생할 수 있습니다. 따라서 Optuna 최적의 결과를 곧바로 실매에 적용하기보다, 최소 5거래일 이상 다일 재검증과 웹백테스트를 통해 안정성을 먼저 확인해야 합니다. 시장은 늘 변하므로 페이퍼/소액 모의투자로 손절 기능 작동을 면밀히 관찰 후 적용을 확대하시길 강력히 권고합니다. 특정 종목 추천이나 확정 수익을 보장하지 않습니다. diff --git a/kis_trader/backtest/results/optuna_scalp_tpe_20260830_013404.briefing.md b/kis_trader/backtest/results/optuna_scalp_tpe_20260830_013404.briefing.md new file mode 100644 index 0000000..dd52140 --- /dev/null +++ b/kis_trader/backtest/results/optuna_scalp_tpe_20260830_013404.briefing.md @@ -0,0 +1,53 @@ +# Optuna 브리핑 — 스캘핑 (tpe) +기간: 2026-08-24 ~ 2026-08-28 (거래일≈5일) · trials 5/5 +탐색 게이트: WR≥0% PF≥0 · min_trades≥10 | 후보 게이트: WR≥40% PF≥1 · min_trades≥10 +- min_trades=10: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 0건 · 사후합격(results_gated) 0건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 33.0%** · **적용 가능도 67.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈5일 — 표본 일수 상대적 양호 + - 거래 표본: +25.0p — 통과 후보 없음 — 적용 불가 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +8.0p — 학습·gated 모두 비어 있음 + - PnL 고원: +0.0p — 고원 판정 스킵 +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 최종 선택 후보 (규칙 · DB 적용 전 확인) +- 아래는 **역할이 다른** 4종 후보입니다. mode Top10 1위 ≠ 익절 대표 · **DB 1순위는 사후합격(gated)** 입니다. +- mode pool: `positive` · scoring: `band_proximity_p25_p75` + +### 후보 4종 +- **① 사후합격 1위 (apply: gated)**: 없음 +- **② 안정 1위 (apply: stable)**: 없음 +- **③ mode_combo (apply: mode)**: 없음 (미산출·구 JSON) +- **④ mode Top10 1위 (apply: consensus · 밴드 전형 trial)**: 없음 +- mode Top10 meta: pool 0건 · 축 0개 + +### 추천 (자동 · confirm 필수) +1. **사후합격 0건 → DB 적용 비권장.** 현행 DB 유지 + 유니버스·틱·기간 재검증. +- ⚠ 안정 게이트 0건 → 안정 Top은 **점수순 폴백** (안정 게이트 0건 → 학습풀 안정점수순 TopN (마이너스 PnL 포함 · 상대비교)) + +## 이전 장에서는 +- 실매 동기간 청산 33건 · 승률 48% · 합산 PnL 48,710원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 이 기간 백테에서는 **유의미한 통과 조합이 거의 없었습니다.** 유니버스·틱·손절·시간창을 먼저 점검하세요 (돌파 0건이면 특히). + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 현재 사후합격 0건 → 파라미터 유지(현행 DB) + 유니버스/데이터 점검이 우선입니다. + +_생성: 2026-08-30 01:34:04 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +당신은 한국 주식 퀀트 헤지펀드 리스크 매니저입니다. 아래 Optuna TPE 결과와 규칙 브리핑을 읽고, 초보자도 이해하게 「이전 장에서는」/「앞으로 장에서는」 두 절로만 한국어 코멘트를 쓰세요. 과적합·표본부족·실매↔백테 괴리(고스트퍼지 등)를 분명히 경고하세요. mode Top10 1위를 익절 대표로 단정하지 마세요 — DB 1순위는 사후합격(gated). 특정 종목 매수 추천·확정 수익 약속 금지. 200~400자. + +--- + +**「이전 장에서는」** +지난 5거래일 간 실매는 일부 수익을 보였으나, Optuna 백테스트에서는 **기간이 짧고 거래 샘플이 부족해 유효한 전략 조합을 전혀 찾지 못했습니다.** 이는 **과적합 및 표본 부족** 위험이 매우 높다는 경고등입니다. 실제 수익이 백테스트 결과와 다른 **실매↔백테 괴리(고스트퍼지 등)** 가능성도 항상 염두에 두어야 합니다. + +**「앞으로 장에서는」** +**사후합격 전략이 0건이므로, 새로운 전략 적용은 절대 금지합니다.** 현행 DB를 유지하며, 유니버스, 틱 데이터, 기간 재검증이 필수입니다. 5일간의 짧은 기간은 **과적합 및 표본 부족** 위험을 증폭시키며, 백테스트 결과만으로 **실매↔백테 괴리(고스트퍼지)** 없이 수익을 확신할 수 없습니다. `mode Top10 1위`를 익절 대표로 단정하지 마세요. `DB 1순위는 사후합격(gated)`인데 현재는 없습니다. 안정적인 성과를 위해선 **다일 검증과 손절매 작동 여부** 확인이 최우선입니다. 특정 종목 매수 추천이나 확정 수익은 보장할 수 없습니다. diff --git a/kis_trader/backtest/results/optuna_scalp_tpe_20260830_013701.briefing.md b/kis_trader/backtest/results/optuna_scalp_tpe_20260830_013701.briefing.md new file mode 100644 index 0000000..7a90b56 --- /dev/null +++ b/kis_trader/backtest/results/optuna_scalp_tpe_20260830_013701.briefing.md @@ -0,0 +1,68 @@ +# Optuna 브리핑 — 스캘핑 (tpe) +기간: 2026-08-24 ~ 2026-08-28 (거래일≈5일) · trials 5/5 +탐색 게이트: WR≥0% PF≥0 · min_trades≥10 | 후보 게이트: WR≥40% PF≥1 · min_trades≥10 +- min_trades=10: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 1건 · 사후합격(results_gated) 1건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 0.0%** · **적용 가능도 100.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈5일 — 표본 일수 상대적 양호 + - 거래 표본: +0.0p — 상위 후보 거래 36건 — 상대적 양호 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +0.0p — 사후합격 1건 · stable 1건 + - PnL 고원: +0.0p — 고원 비율 100% (1건) +- 임계값 분포 표본: results_gated 상위 1건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `rsi_period` | 7.0 | 7.0 ~ 7.0 | 7 (100%) | +| `rsi_oversold` | 22.5 | 22.5 ~ 22.5 | 22.5 (100%) | +| `rsi_overbought` | 78.0 | 78.0 ~ 78.0 | 78.0 (100%) | +| `sl_pct` | 4.9 | 4.9 ~ 4.9 | 4.9 (100%) | +| `tp_pct` | 4.9 | 4.9 ~ 4.9 | 4.9 (100%) | +| `tp_max_pct` | 5.0 | 5.0 ~ 5.0 | 5.0 (100%) | +| `drop_rate` | 1.9 | 1.9 ~ 1.9 | 1.9 (100%) | +| `shoulder_min_high` | 1.0 | 1.0 ~ 1.0 | 1.0 (100%) | +| `shoulder_cut_pct` | 0.3 | 0.3 ~ 0.3 | 0.3 (100%) | +| `cooldown_min` | 0.0 | 0.0 ~ 0.0 | 0 (100%) | +| `high_chase_thr` | 0.985 | 0.985 ~ 0.985 | 0.985 (100%) | +| `max_daily_chg` | 54.0 | 54.0 ~ 54.0 | 54.0 (100%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 최종 선택 후보 (규칙 · DB 적용 전 확인) +- 아래는 **역할이 다른** 4종 후보입니다. mode Top10 1위 ≠ 익절 대표 · **DB 1순위는 사후합격(gated)** 입니다. +- mode pool: `positive` · scoring: `band_proximity_p25_p75` + +### 후보 4종 +- **① 사후합격 1위 (apply: gated)** (trial #4): PnL 66,717원 · WR 63.9% · PF 2.74 · 거래 36건 · 안정점수 -2048 · 호가OFF · 익절4.9% 상한5.0% 손절4.9% · 휩쏘ON sub15s lb90s dip1.00% tol0.25% +- **② 안정 1위 (apply: stable)** (trial #4): PnL 66,717원 · WR 63.9% · PF 2.74 · 거래 36건 · 안정점수 -2048 · 호가OFF · 익절4.9% 상한5.0% 손절4.9% · 휩쏘ON sub15s lb90s dip1.00% tol0.25% +- **③ mode_combo (apply: mode)** (trial 없음(조립)): PnL 66,717원 · WR 63.9% · PF 2.74 · 거래 36건 · 안정점수 -2048 · 호가OFF · 익절4.9% 상한5.0% 손절4.9% · 휩쏘ON sub15s lb90s dip1.00% tol0.25% · tp 최빈 4.9% (1/1) +- **④ mode Top10 1위 (apply: consensus · 밴드 전형 trial)** (trial #4): PnL 66,717원 · WR 63.9% · PF 2.74 · 거래 36건 · 안정점수 -2048 · 근접 100.0% (30/30축 밴드안) · 호가OFF · 익절4.9% 상한5.0% 손절4.9% · 휩쏘ON sub15s lb90s dip1.00% tol0.25% + +- mode pool tp_pct 밴드 p25~p75: **4.9~4.9%** · Top10 1위 tp=4.9% → 밴드 **안** +- mode Top10 meta: pool 1건 · 축 30개 + +### 추천 (자동 · confirm 필수) +1. **실매 DB 적용 1순위 → ① 사후합격 1위** (`optunaApply gated`) — WR/PF·min_trades 사후 통과. +2. **변동성·손실일 줄이기 → ② 안정 1위** (`stable`) — PnL보다 일별 안정 우선 시. +3. **2차 narrow·대표 숫자 → ③ mode_combo** — 축별 최빈 조립 · trial 번호 없음 · 익절은 pool 최빈값 참고. +4. **④ mode Top10 1위** — 양수 pool 전형 trial(다축 밴드 근접). **익절 하나로 쓰지 말 것** · 표에서 「보기」→ consensus apply. + +## 이전 장에서는 +- 실매 동기간 청산 33건 · 승률 48% · 합산 PnL 48,710원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #4 · 거래 36건 · 승률 63.9% · PF 2.74 · PnL 66,717원 + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-30 01:37:15 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +### 이전 장에서는 +지난 5거래일 간 Optuna 백테스트 사후합격 전략은 PnL 6.6만원, 승률 63.9%(36건)를 기록했습니다. 그러나 동기간 실매는 PnL 4.8만원, 승률 48%(33건)로 백테스트와 괴리가 있었습니다. 이는 고스트퍼지 등 실매 환경 차이로 인한 자연스러운 현상입니다. + +### 앞으로 장에서는 +이 5거래일 백테스트는 표본 부족과 과적합 위험이 크므로, 확정 수익으로 받아들이지 마십시오. 실매와 백테스트 간 괴리는 늘 존재합니다. `mode Top10 1위`를 익절 대표로 단정해서도 안 됩니다. 새로운 전략은 페이퍼/소액 모의로 1~2일 관찰 후 신중히 확대하고, 승률보다 손절 작동 여부를 중요하게 확인해야 합니다. diff --git a/kis_trader/backtest/results/optuna_scalp_tpe_20260830_073905.briefing.md b/kis_trader/backtest/results/optuna_scalp_tpe_20260830_073905.briefing.md new file mode 100644 index 0000000..691ad46 --- /dev/null +++ b/kis_trader/backtest/results/optuna_scalp_tpe_20260830_073905.briefing.md @@ -0,0 +1,68 @@ +# Optuna 브리핑 — 스캘핑 (tpe) +기간: 2026-08-24 ~ 2026-08-28 (거래일≈5일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥10 | 후보 게이트: WR≥40% PF≥1 · min_trades≥10 +- min_trades=10: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 148건 · 사후합격(results_gated) 146건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 0.0%** · **적용 가능도 100.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈5일 — 표본 일수 상대적 양호 + - 거래 표본: +0.0p — 상위 후보 거래 40건 — 상대적 양호 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +0.0p — 사후합격 146건 · stable 10건 + - PnL 고원: +0.0p — 고원 비율 1% (1건) +- 임계값 분포 표본: results_gated 상위 30건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `rsi_period` | 3.0 | 3.0 ~ 11.75 | 3 (63%) | +| `rsi_oversold` | 20.5 | 19.5 ~ 27.75 | 20.5 (13%) | +| `rsi_overbought` | 71.5 | 67.25 ~ 73.875 | 72.0 (10%) | +| `sl_pct` | 3.9 | 3.35 ~ 4.85 | 3.9 (17%) | +| `tp_pct` | 3.25 | 2.7 ~ 4.1 | 2.7 (7%) | +| `tp_max_pct` | 6.0 | 5.0 ~ 7.375 | 7.0 (17%) | +| `drop_rate` | 4.0 | 2.65 ~ 4.75 | 4.0 (10%) | +| `shoulder_min_high` | 0.4 | 0.3 ~ 1.35 | 0.3 (47%) | +| `shoulder_cut_pct` | 0.25 | 0.2 ~ 0.2875 | 0.2 (33%) | +| `cooldown_min` | 2.0 | 0.0 ~ 4.0 | 0 (43%) | +| `high_chase_thr` | 0.97 | 0.965 ~ 0.97 | 0.97 (30%) | +| `max_daily_chg` | 50.5 | 48.0 ~ 56.5 | 60.0 (17%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 최종 선택 후보 (규칙 · DB 적용 전 확인) +- 아래는 **역할이 다른** 4종 후보입니다. mode Top10 1위 ≠ 익절 대표 · **DB 1순위는 사후합격(gated)** 입니다. +- mode pool: `positive` · scoring: `band_proximity_p25_p75` + +### 후보 4종 +- **① 사후합격 1위 (apply: gated)** (trial #42): PnL 131,777원 · WR 67.5% · PF 5.15 · 거래 40건 · 안정점수 10143 · 호가OFF · 익절2.9% 상한4.0% 손절5.4% · 휩쏘ON sub30s lb60s dip1.00% tol0.25% +- **② 안정 1위 (apply: stable)** (trial #22): PnL 135,456원 · WR 60.4% · PF 3.91 · 거래 48건 · 안정점수 12279 · 호가OFF · 익절2.1% 상한4.5% 손절3.9% · 휩쏘ON sub30s lb60s dip0.90% tol0.25% +- **③ mode_combo (apply: mode)** (trial 없음(조립)): PnL 99,582원 · WR 56.9% · PF 3.58 · 거래 51건 · 안정점수 184 · 호가OFF · 익절3.5% 상한7.5% 손절3.5% · 휩쏘ON sub30s lb60s dip0.90% tol0.25% · tp 최빈 3.5% (10/146) +- **④ mode Top10 1위 (apply: consensus · 밴드 전형 trial)** (trial #122): PnL 100,348원 · WR 60.0% · PF 3.24 · 거래 35건 · 안정점수 3757 · 근접 97.6% (26/30축 밴드안) · 호가OFF · 익절2.9% 상한7.5% 손절5.8% · 휩쏘ON sub30s lb60s dip0.90% tol0.25% + +- mode pool tp_pct 밴드 p25~p75: **2.125~4.1%** · Top10 1위 tp=2.9% → 밴드 **안** +- mode Top10 meta: pool 146건 · 축 30개 + +### 추천 (자동 · confirm 필수) +1. **실매 DB 적용 1순위 → ① 사후합격 1위** (`optunaApply gated`) — WR/PF·min_trades 사후 통과. +2. **변동성·손실일 줄이기 → ② 안정 1위** (`stable`) — PnL보다 일별 안정 우선 시. +3. **2차 narrow·대표 숫자 → ③ mode_combo** — 축별 최빈 조립 · trial 번호 없음 · 익절은 pool 최빈값 참고. +4. **④ mode Top10 1위** — 양수 pool 전형 trial(다축 밴드 근접). **익절 하나로 쓰지 말 것** · 표에서 「보기」→ consensus apply. + +## 이전 장에서는 +- 실매 동기간 청산 33건 · 승률 48% · 합산 PnL 48,710원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #42 · 거래 40건 · 승률 67.5% · PF 5.15 · PnL 131,777원 + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-30 07:39:26 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +**이전 장에서는** +실제 매매에서는 33건 거래로 승률 48%, PnL 약 4.8만원을 기록했습니다. 이는 백테스트 사후합격 1위의 40건 거래, 승률 67.5%, PnL 약 13.1만원과 상당한 괴리를 보입니다. 이러한 실매와 백테스트 간의 차이(고스트퍼지, 유니버스, 틱 경로 등)는 항상 발생할 수 있는 현상이므로, 백테스트 결과만으로 성과를 단정해서는 안 됩니다. + +**앞으로 장에서는** +이번 Optuna TPE 결과는 5거래일이라는 매우 짧은 기간의 백테스트로, **과적합 및 표본 부족 위험이 높습니다.** 휴리스틱상 위험이 낮더라도 이는 절대적인 지표가 아닙니다. `mode Top10 1위`를 익절 대표로 단정하지 마세요. DB 적용 1순위는 사후합격(gated)이지만, 실매와 백테스트 간 괴리(고스트퍼지 등)를 항상 경계해야 합니다. 즉시 적용보다 다일 재검증과 페이퍼/소액 관찰을 권하며, 시장 변동 시 **손절이 살아있는지**를 최우선으로 점검해야 합니다. 특정 종목 매수 추천이나 확정 수익은 불가합니다. diff --git a/kis_trader/backtest/results/optuna_scalp_tpe_20260830_080601.briefing.md b/kis_trader/backtest/results/optuna_scalp_tpe_20260830_080601.briefing.md new file mode 100644 index 0000000..dd58b41 --- /dev/null +++ b/kis_trader/backtest/results/optuna_scalp_tpe_20260830_080601.briefing.md @@ -0,0 +1,70 @@ +# Optuna 브리핑 — 스캘핑 (tpe) +기간: 2026-08-24 ~ 2026-08-28 (거래일≈5일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥10 | 후보 게이트: WR≥40% PF≥1 · min_trades≥10 +- min_trades=10: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 169건 · 사후합격(results_gated) 168건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 0.0%** · **적용 가능도 100.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈5일 — 표본 일수 상대적 양호 + - 거래 표본: +0.0p — 상위 후보 거래 49건 — 상대적 양호 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +0.0p — 사후합격 168건 · stable 10건 + - PnL 고원: +0.0p — 고원 비율 1% (1건) +- 임계값 분포 표본: results_gated 상위 30건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `rsi_period` | 13.0 | 11.0 ~ 14.0 | 14 (33%) | +| `rsi_oversold` | 28.5 | 27.125 ~ 29.0 | 29.0 (20%) | +| `rsi_overbought` | 73.0 | 71.5 ~ 74.875 | 73.0 (13%) | +| `sl_pct` | 4.2 | 3.925 ~ 4.5 | 4.0 (20%) | +| `tp_pct` | 4.75 | 3.65 ~ 5.25 | 5.0 (10%) | +| `tp_max_pct` | 11.5 | 11.0 ~ 12.0 | 12.0 (40%) | +| `drop_rate` | 1.7 | 1.225 ~ 2.0 | 1.8 (17%) | +| `shoulder_min_high` | 0.3 | 0.3 ~ 0.775 | 0.3 (57%) | +| `shoulder_cut_pct` | 0.2 | 0.2 ~ 0.25 | 0.2 (63%) | +| `cooldown_min` | 13.0 | 4.5 ~ 13.75 | 13 (30%) | +| `high_chase_thr` | 0.99 | 0.955 ~ 0.995 | 0.995 (23%) | +| `max_daily_chg` | 20.5 | 19.0 ~ 25.0 | 20.0 (13%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 최종 선택 후보 (규칙 · DB 적용 전 확인) +- 아래는 **역할이 다른** 4종 후보입니다. mode Top10 1위 ≠ 익절 대표 · **DB 1순위는 사후합격(gated)** 입니다. +- mode pool: `positive` · scoring: `band_proximity_p25_p75` + +### 후보 4종 +- **① 사후합격 1위 (apply: gated)** (trial #162): PnL 106,366원 · WR 67.3% · PF 7.56 · 거래 49건 · 안정점수 7454 · 호가OFF · 익절4.7% 상한12.0% 손절4.2% · 휩쏘ON sub45s lb180s dip1.00% tol0.30% +- **② 안정 1위 (apply: stable)** (trial #159): PnL 88,870원 · WR 69.8% · PF 5.19 · 거래 53건 · 안정점수 10620 · 호가OFF · 익절5.0% 상한11.0% 손절4.3% · 휩쏘ON sub45s lb180s dip0.90% tol0.30% +- **③ mode_combo (apply: mode)** (trial 없음(조립)): PnL 108,368원 · WR 62.0% · PF 6.88 · 거래 50건 · 안정점수 6944 · 호가OFF · 익절5.0% 상한12.0% 손절4.0% · 휩쏘ON sub45s lb120s dip1.00% tol0.30% · tp 최빈 5.0% (8/168) +- **④ mode Top10 1위 (apply: consensus · 밴드 전형 trial)** (trial #149): PnL 99,901원 · WR 60.4% · PF 5.31 · 거래 48건 · 안정점수 4808 · 근접 99.1% (27/30축 밴드안) · 호가OFF · 익절4.8% 상한11.5% 손절4.7% · 휩쏘ON sub45s lb120s dip1.00% tol0.30% + +- mode pool tp_pct 밴드 p25~p75: **1.875~5.4%** · Top10 1위 tp=4.8% → 밴드 **안** +- mode Top10 meta: pool 168건 · 축 30개 + +### 추천 (자동 · confirm 필수) +1. **실매 DB 적용 1순위 → ① 사후합격 1위** (`optunaApply gated`) — WR/PF·min_trades 사후 통과. +2. **변동성·손실일 줄이기 → ② 안정 1위** (`stable`) — PnL보다 일별 안정 우선 시. +3. **2차 narrow·대표 숫자 → ③ mode_combo** — 축별 최빈 조립 · trial 번호 없음 · 익절은 pool 최빈값 참고. +4. **④ mode Top10 1위** — 양수 pool 전형 trial(다축 밴드 근접). **익절 하나로 쓰지 말 것** · 표에서 「보기」→ consensus apply. + +## 이전 장에서는 +- 실매 동기간 청산 33건 · 승률 48% · 합산 PnL 48,710원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #162 · 거래 49건 · 승률 67.3% · PF 7.56 · PnL 106,366원 + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-30 08:06:18 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +## 이전 장에서는 + +지난 5일간 스캘핑 전략 최적화 백테스트에서, 사후합격 1위는 높은 PnL과 승률을 보였습니다. 하지만 같은 기간 실제 매매는 훨씬 저조한 PnL과 승률을 기록하며, 백테스트와 실매 환경 간의 고질적인 괴리(고스트퍼지 등)를 재확인시켜주었습니다. 맹신은 금물입니다. + +## 앞으로 장에서는 + +이번 5일간의 짧은 백테스트는 표본 부족과 과적합 위험을 내포합니다. '과적합 위험 0%' 표시는 휴리스틱일 뿐이므로, 결과를 맹신해 즉시 실매에 적용하는 것은 위험합니다. 실매 적용 전 최소 5거래일 이상 재검증과 소액 모의투자를 통해 전략의 견고함을 반드시 확인하세요. DB 사후합격 1위가 실제 적용 대상이며, mode Top10 1위는 익절 대표가 아님을 인지해야 합니다. 시장은 끊임없이 변하며, 승률보다 손절 원칙 준수가 더 중요합니다. 특정 종목 매수 추천이나 확정 수익은 절대 보장할 수 없습니다. diff --git a/kis_trader/backtest/results/optuna_scalp_tpe_20260830_154234.briefing.md b/kis_trader/backtest/results/optuna_scalp_tpe_20260830_154234.briefing.md new file mode 100644 index 0000000..c3af299 --- /dev/null +++ b/kis_trader/backtest/results/optuna_scalp_tpe_20260830_154234.briefing.md @@ -0,0 +1,68 @@ +# Optuna 브리핑 — 스캘핑 (tpe) +기간: 2026-08-24 ~ 2026-08-28 (거래일≈5일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥10 | 후보 게이트: WR≥40% PF≥1 · min_trades≥10 +- min_trades=10: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 134건 · 사후합격(results_gated) 134건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 0.0%** · **적용 가능도 100.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈5일 — 표본 일수 상대적 양호 + - 거래 표본: +0.0p — 상위 후보 거래 24건 — 상대적 양호 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +0.0p — 사후합격 134건 · stable 10건 + - PnL 고원: +0.0p — 고원 비율 1% (1건) +- 임계값 분포 표본: results_gated 상위 30건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `rsi_period` | 4.0 | 4.0 ~ 5.0 | 4 (37%) | +| `rsi_oversold` | 15.5 | 15.0 ~ 16.5 | 15.0 (47%) | +| `rsi_overbought` | 74.5 | 73.625 ~ 77.5 | 74.5 (17%) | +| `sl_pct` | 5.5 | 4.85 ~ 5.8 | 5.5 (23%) | +| `tp_pct` | 7.1 | 6.4 ~ 7.675 | 7.5 (13%) | +| `tp_max_pct` | 9.0 | 8.0 ~ 9.5 | 9.0 (27%) | +| `drop_rate` | 2.0 | 1.625 ~ 2.5 | 2.6 (17%) | +| `shoulder_min_high` | 3.6 | 3.5 ~ 3.875 | 3.6 (30%) | +| `shoulder_cut_pct` | 0.3 | 0.25 ~ 0.35 | 0.35 (30%) | +| `cooldown_min` | 1.0 | 0.0 ~ 2.0 | 0 (43%) | +| `high_chase_thr` | 0.99 | 0.99 ~ 0.99 | 0.99 (60%) | +| `max_daily_chg` | 30.5 | 26.0 ~ 34.0 | 34.0 (13%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 최종 선택 후보 (규칙 · DB 적용 전 확인) +- 아래는 **역할이 다른** 4종 후보입니다. mode Top10 1위 ≠ 익절 대표 · **DB 1순위는 사후합격(gated)** 입니다. +- mode pool: `positive` · scoring: `band_proximity_p25_p75` + +### 후보 4종 +- **① 사후합격 1위 (apply: gated)** (trial #131): PnL 132,942원 · WR 75.0% · PF 12.90 · 거래 24건 · 안정점수 918 · 호가ON spr1.6 r0.40 ask×24 · 익절7.5% 상한8.0% 손절5.5% · 휩쏘ON sub45s lb60s dip1.00% tol0.25% +- **② 안정 1위 (apply: stable)** (trial #176): PnL 73,452원 · WR 60.0% · PF 3.43 · 거래 25건 · 안정점수 6394 · 호가OFF · 익절6.0% 상한10.5% 손절6.0% · 휩쏘OFF +- **③ mode_combo (apply: mode)** (trial 없음(조립)): PnL 115,042원 · WR 68.4% · PF 12.04 · 거래 19건 · 안정점수 -3995 · 호가ON spr2.2 r0.45 ask×17 · 익절7.7% 상한9.0% 손절5.9% · 휩쏘ON sub45s lb180s dip1.00% tol0.30% · tp 최빈 7.7% (8/134) +- **④ mode Top10 1위 (apply: consensus · 밴드 전형 trial)** (trial #129): PnL 103,333원 · WR 75.0% · PF 11.91 · 거래 20건 · 안정점수 -8055 · 근접 99.8% (29/30축 밴드안) · 호가ON spr1.9 r0.40 ask×24 · 익절7.6% 상한9.0% 손절5.5% · 휩쏘ON sub45s lb60s dip1.00% tol0.25% + +- mode pool tp_pct 밴드 p25~p75: **3.15~7.275%** · Top10 1위 tp=7.6% → 밴드 **밖**(다른 축 보정으로 근접% 높음) +- mode Top10 meta: pool 134건 · 축 30개 + +### 추천 (자동 · confirm 필수) +1. **실매 DB 적용 1순위 → ① 사후합격 1위** (`optunaApply gated`) — WR/PF·min_trades 사후 통과. +2. **변동성·손실일 줄이기 → ② 안정 1위** (`stable`) — PnL보다 일별 안정 우선 시. +3. **2차 narrow·대표 숫자 → ③ mode_combo** — 축별 최빈 조립 · trial 번호 없음 · 익절은 pool 최빈값 참고. +4. **④ mode Top10 1위** — 양수 pool 전형 trial(다축 밴드 근접). **익절 하나로 쓰지 말 것** · 표에서 「보기」→ consensus apply. + +## 이전 장에서는 +- 실매 동기간 청산 33건 · 승률 48% · 합산 PnL 48,710원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #131 · 거래 24건 · 승률 75.0% · PF 12.90 · PnL 132,942원 + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-30 15:42:45 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +「이전 장에서는」 +5거래일 단기 백테스트 결과, `#131` 트라이얼이 PnL 13.3만원, 승률 75%로 사후합격 1위를 기록했습니다. 그러나 동기간 실매는 33건, 승률 48%, PnL 4.8만원으로 백테스트와 큰 괴리를 보였습니다. 이는 `고스트 매매` 등 실매 환경의 차이로 정상적인 현상임을 다시 확인했습니다. + +「앞으로 장에서는」 +이번 5거래일 백테스트 결과는 `과적합 위험`이 높고 `표본이 부족`합니다. `mode Top10 1위`가 아닌 `사후합격 1위(#131)`를 보더라도 `실매↔백테 괴리`가 크니 맹신은 금물입니다. `휴리스틱상` 위험이 낮더라도, 절대 바로 실매 적용하지 마시고 페이퍼 트레이딩 또는 소액으로 최소 5거래일 이상 `재검증` 후 시장 변화에 대응하세요. 확정 수익은 약속할 수 없습니다. diff --git a/kis_trader/backtest/results/optuna_scalp_tpe_20260830_161031.briefing.md b/kis_trader/backtest/results/optuna_scalp_tpe_20260830_161031.briefing.md new file mode 100644 index 0000000..7237b45 --- /dev/null +++ b/kis_trader/backtest/results/optuna_scalp_tpe_20260830_161031.briefing.md @@ -0,0 +1,70 @@ +# Optuna 브리핑 — 스캘핑 (tpe) +기간: 2026-08-24 ~ 2026-08-28 (거래일≈5일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥10 | 후보 게이트: WR≥40% PF≥1 · min_trades≥10 +- min_trades=10: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 150건 · 사후합격(results_gated) 149건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 0.0%** · **적용 가능도 100.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈5일 — 표본 일수 상대적 양호 + - 거래 표본: +0.0p — 상위 후보 거래 30건 — 상대적 양호 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +0.0p — 사후합격 149건 · stable 10건 + - PnL 고원: +0.0p — 고원 비율 1% (1건) +- 임계값 분포 표본: results_gated 상위 30건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `rsi_period` | 5.0 | 5.0 ~ 6.75 | 5 (57%) | +| `rsi_oversold` | 29.0 | 28.0 ~ 29.5 | 29.0 (23%) | +| `rsi_overbought` | 69.5 | 68.125 ~ 71.25 | 69.0 (13%) | +| `sl_pct` | 5.35 | 4.925 ~ 5.875 | 6.0 (20%) | +| `tp_pct` | 5.25 | 4.825 ~ 6.275 | 4.9 (10%) | +| `tp_max_pct` | 11.5 | 10.625 ~ 12.0 | 11.5 (40%) | +| `drop_rate` | 2.5 | 0.7 ~ 3.875 | 0.5 (23%) | +| `shoulder_min_high` | 3.3 | 3.025 ~ 3.3 | 3.3 (40%) | +| `shoulder_cut_pct` | 0.125 | 0.05 ~ 0.25 | 0.05 (30%) | +| `cooldown_min` | 6.0 | 3.0 ~ 7.0 | 7 (37%) | +| `high_chase_thr` | 0.965 | 0.96 ~ 0.97 | 0.96 (40%) | +| `max_daily_chg` | 22.0 | 20.25 ~ 24.0 | 22.0 (20%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 최종 선택 후보 (규칙 · DB 적용 전 확인) +- 아래는 **역할이 다른** 4종 후보입니다. mode Top10 1위 ≠ 익절 대표 · **DB 1순위는 사후합격(gated)** 입니다. +- mode pool: `positive` · scoring: `band_proximity_p25_p75` + +### 후보 4종 +- **① 사후합격 1위 (apply: gated)** (trial #167): PnL 153,102원 · WR 70.0% · PF 7.45 · 거래 30건 · 안정점수 5208 · 호가OFF · 익절5.7% 상한11.5% 손절6.0% · 휩쏘OFF +- **② 안정 1위 (apply: stable)** (trial #46): PnL 66,539원 · WR 61.5% · PF 2.12 · 거래 39건 · 안정점수 7485 · 호가OFF · 익절3.1% 상한5.5% 손절3.1% · 휩쏘OFF +- **③ mode_combo (apply: mode)** (trial 없음(조립)): PnL 125,400원 · WR 65.5% · PF 5.37 · 거래 29건 · 안정점수 653 · 호가OFF · 익절6.2% 상한11.5% 손절6.0% · 휩쏘OFF · tp 최빈 6.2% (6/150) +- **④ mode Top10 1위 (apply: consensus · 밴드 전형 trial)** (trial #161): PnL 123,839원 · WR 61.3% · PF 4.82 · 거래 31건 · 안정점수 3155 · 근접 98.4% (27/30축 밴드안) · 호가OFF · 익절4.9% 상한12.0% 손절5.3% · 휩쏘OFF + +- mode pool tp_pct 밴드 p25~p75: **2.9~6%** · Top10 1위 tp=4.9% → 밴드 **안** +- mode Top10 meta: pool 150건 · 축 30개 + +### 추천 (자동 · confirm 필수) +1. **실매 DB 적용 1순위 → ① 사후합격 1위** (`optunaApply gated`) — WR/PF·min_trades 사후 통과. +2. **변동성·손실일 줄이기 → ② 안정 1위** (`stable`) — PnL보다 일별 안정 우선 시. +3. **2차 narrow·대표 숫자 → ③ mode_combo** — 축별 최빈 조립 · trial 번호 없음 · 익절은 pool 최빈값 참고. +4. **④ mode Top10 1위** — 양수 pool 전형 trial(다축 밴드 근접). **익절 하나로 쓰지 말 것** · 표에서 「보기」→ consensus apply. + +## 이전 장에서는 +- 실매 동기간 청산 33건 · 승률 48% · 합산 PnL 48,710원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #167 · 거래 30건 · 승률 70.0% · PF 7.45 · PnL 153,102원 + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-30 16:10:51 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +## 이전 장에서는 + +지난 5거래일 간 Optuna 스캘핑 전략 백테스트 결과, 사후합격 1위 전략은 높은 PnL과 70%의 승률을 기록했습니다. 하지만 실제 매매에서는 백테스트 대비 낮은 PnL과 48%의 승률을 보이며 큰 괴리를 보였습니다. 이는 백테스트 환경과 실제 매매 환경의 차이, 즉 고스트퍼지 등의 요인으로 인한 결과로 분석됩니다. 5일이라는 짧은 백테스트 기간은 과적합 위험을 내포하고 있습니다. + +## 앞으로 장에서는 + +앞으로 전략 적용 시, 5일간의 짧은 백테스트 기간은 과적합 및 표본 부족 위험이 매우 높으므로 맹신은 금물입니다. 백테스트 결과와 실제 매매 간 괴리(고스트퍼지)가 크다는 점을 명심하고, 「사후합격」 후보만을 활용하되, 다일 재검증과 웹백테스트를 통해 추가 검증이 필수입니다. 특히 mode Top10 1위는 익절 대표가 아닌 다양한 후보 중 하나이며, DB 1순위는 사후합격(gated)입니다. 특정 종목 매수 추천이나 확정 수익 약속은 불가합니다. 초기에는 페이퍼 또는 소액으로 최소 1~2일 이상 관찰하며 손절 기준이 유효한지 점검 후 점진적으로 확대해야 합니다. diff --git a/kis_trader/backtest/results/optuna_tail_fast_20260826_030604.briefing.md b/kis_trader/backtest/results/optuna_tail_fast_20260826_030604.briefing.md new file mode 100644 index 0000000..4052f77 --- /dev/null +++ b/kis_trader/backtest/results/optuna_tail_fast_20260826_030604.briefing.md @@ -0,0 +1,46 @@ +# Optuna 브리핑 — 꼬리 (fast) +기간: 2026-08-25 ~ 2026-08-25 (거래일≈1일) · trials 5/5 +탐색 게이트: WR≥0% PF≥0 · min_trades≥1 | 후보 게이트: WR≥40% PF≥1 · min_trades≥1 +- min_trades=1: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 2건 · 사후합격(results_gated) 2건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 74.0%** · **적용 가능도 26.0%** · 판정: **비권장** — 과적합·표본부족 위험 높음 — 실매 DB 즉시 적용 비권장 + - 표본 장일: +40.0p — 거래일≈1일 — 단일 장 과적합 위험 최대 + - 거래 표본: +18.0p — 상위 후보 거래 3건 — 표본 부족 + - 승률·PF 이상치: +6.0p — PF 10.83 + - 게이트 커버: +10.0p — gated/all=2/2 — 단일에 대부분 통과(필터 느슨·노이즈) + - PnL 고원: +0.0p — 고원 비율 50% (1건) +- 임계값 분포 표본: results_gated 상위 2건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `entry_mode` | — | — | align (100%) | +| `cand_limit` | 10.0 | 5.0 ~ 15.0 | 20 (50%) | +| `max_daily_change` | 32.5 | 31.25 ~ 33.75 | 35.0 (50%) | +| `min_drop_rate` | 0.0225 | 0.01875 ~ 0.02625 | 0.015 (50%) | +| `min_recovery_ratio` | 0.175 | 0.1375 ~ 0.2125 | 0.1 (50%) | +| `tail_ratio_min` | 0.8 | 0.7 ~ 0.9 | 1.0 (50%) | +| `tail_pct_min` | 0.003 | 0.002 ~ 0.004 | 0.005 (50%) | +| `max_rec_3m` | 0.875 | 0.8625 ~ 0.8875 | 0.85 (50%) | +| `shoulder_min_high` | 0.007 | 0.007 ~ 0.007 | 0.007 (100%) | +| `shoulder_cut_pct` | 0.0015 | 0.00125 ~ 0.00175 | 0.001 (50%) | +| `stop_atr_mult` | 1.75 | 1.625 ~ 1.875 | 1.5 (50%) | +| `target_atr_mult` | 2.0 | 1.75 ~ 2.25 | 1.5 (50%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 이전 장에서는 +- 실매(2026-08-25~2026-08-25) 해당 전략 청산 기록 없음(또는 전략명 불일치). +- 사후합격 1위: trial #1 · 거래 3건 · 승률 66.7% · PF 10.83 · PnL 4,080원 +- 표본 장이 1일뿐이라, ‘이전 장에 잘 먹힌 숫자’일 뿐 **추세가 바뀌면 바로 깨질 수 있습니다.** + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-26 03:06:07 (rule)_ + +## AI 코멘트 + +_(미생성: api_call_failed. 구글 키는 GEMINI_API_KEY 를 DB/환경에 넣으세요 — kis_trader/backtest/optuna_briefing.py 모듈 docstring 참고)_ diff --git a/kis_trader/backtest/results/optuna_tail_tpe_20260823_220226.briefing.md b/kis_trader/backtest/results/optuna_tail_tpe_20260823_220226.briefing.md new file mode 100644 index 0000000..d3c6667 --- /dev/null +++ b/kis_trader/backtest/results/optuna_tail_tpe_20260823_220226.briefing.md @@ -0,0 +1,49 @@ +# Optuna 브리핑 — 꼬리 (tpe) +기간: 2026-08-18 ~ 2026-08-21 (거래일≈4일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥1 | 후보 게이트: WR≥40% PF≥1 · min_trades≥1 +- min_trades=1: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 145건 · 사후합격(results_gated) 68건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 16.0%** · **적용 가능도 84.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +16.0p — 거래일≈4일 — 다일 재검증 권장(≥5일) + - 거래 표본: +0.0p — 상위 후보 거래 26건 — 상대적 양호 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +0.0p — 사후합격 68건 · stable 10건 + - PnL 고원: +0.0p — 고원 비율 1% (1건) +- 임계값 분포 표본: results_gated 상위 30건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `entry_mode` | — | — | align (100%) | +| `cand_limit` | 0.0 | 0.0 ~ 0.0 | 0 (83%) | +| `max_daily_change` | 24.5 | 19.0 ~ 32.0 | 26.0 (10%) | +| `min_drop_rate` | 0.1075 | 0.095 ~ 0.13 | 0.095 (17%) | +| `min_recovery_ratio` | 0.14 | 0.13 ~ 0.16 | 0.14 (17%) | +| `tail_ratio_min` | 1.1 | 0.9 ~ 1.2 | 1.1 (23%) | +| `tail_pct_min` | 0.005 | 0.0035 ~ 0.006 | 0.0035 (17%) | +| `max_rec_3m` | 0.915 | 0.8725 ~ 0.96 | 0.97 (13%) | +| `shoulder_min_high` | 0.01 | 0.008 ~ 0.01275 | 0.011 (17%) | +| `shoulder_cut_pct` | 0.003 | 0.0025 ~ 0.003875 | 0.003 (27%) | +| `stop_atr_mult` | 2.05 | 1.7 ~ 2.6 | 1.6 (10%) | +| `target_atr_mult` | 3.05 | 2.8 ~ 3.375 | 3.4 (17%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 이전 장에서는 +- 실매 동기간 청산 1건 · 승률 0% · 합산 PnL 0원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #183 · 거래 26건 · 승률 69.2% · PF 1.95 · PnL 23,467원 + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-23 22:02:45 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +**이전 장에서는** +지난 4거래일 Optuna 백테스트에서 사후합격 1위 전략은 26건의 거래로 69.2% 승률과 PF 1.95를 기록하며 23,467원의 수익을 냈습니다. 그러나 실제 매매에서는 단 1건 청산에 PnL 0원으로, 백테스트와 큰 괴리를 보였습니다. 이는 고스트퍼지, 유니버스, 슬리피지 등 실매와 백테스트 환경 차이로 인한 일반적인 현상입니다. + +**앞으로 장에서는** +이번 백테스트는 4거래일이라는 매우 짧은 기간으로, 과적합 및 표본 부족 위험이 높습니다. 따라서 이 결과를 곧바로 실매에 적용하는 것은 절대 금물입니다. 최소 5일 이상의 다일 재검증과 웹백테스트를 통해 유효성을 충분히 검증해야 합니다. 실제 시장은 백테스트와 항상 다를 수 있으니, 초기에는 소액 또는 모의 투자로 시장 적합성을 면밀히 관찰하며 진입하시길 강력히 권고합니다. diff --git a/kis_trader/backtest/results/optuna_tail_tpe_20260823_222304.briefing.md b/kis_trader/backtest/results/optuna_tail_tpe_20260823_222304.briefing.md new file mode 100644 index 0000000..68b4c79 --- /dev/null +++ b/kis_trader/backtest/results/optuna_tail_tpe_20260823_222304.briefing.md @@ -0,0 +1,50 @@ +# Optuna 브리핑 — 꼬리 (tpe) +기간: 2026-08-18 ~ 2026-08-21 (거래일≈4일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥1 | 후보 게이트: WR≥40% PF≥1 · min_trades≥1 +- min_trades=1: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 75건 · 사후합격(results_gated) 73건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 66.0%** · **적용 가능도 34.0%** · 판정: **주의** — 적용 가능도 중간 — 다일(≥5일) 재검증·웹백테 후 소액만 + - 표본 장일: +16.0p — 거래일≈4일 — 다일 재검증 권장(≥5일) + - 거래 표본: +25.0p — 상위 후보 거래 1건 — 우연 승·과적합 가능 + - 승률·PF 이상치: +25.0p — 승률 100.0% + 거래 1건 · PF 9999.00 (소수 거래 폭증) + - 게이트 커버: +0.0p — 사후합격 73건 · stable 5건 + - PnL 고원: +0.0p — 고원 비율 12% (9건) +- 임계값 분포 표본: results_gated 상위 30건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `entry_mode` | — | — | limit_atr (100%) | +| `cand_limit` | 0.0 | 0.0 ~ 0.0 | 0 (87%) | +| `max_daily_change` | 47.0 | 40.0 ~ 56.0 | 40.0 (13%) | +| `min_drop_rate` | 0.055 | 0.0225 ~ 0.07875 | 0.055 (13%) | +| `min_recovery_ratio` | 0.145 | 0.12 ~ 0.1775 | 0.19 (13%) | +| `tail_ratio_min` | 0.95 | 0.6 ~ 1.2 | 0.6 (13%) | +| `tail_pct_min` | 0.008 | 0.006125 ~ 0.008875 | 0.0085 (17%) | +| `max_rec_3m` | 0.875 | 0.8225 ~ 0.91 | 0.84 (10%) | +| `shoulder_min_high` | 0.007 | 0.004 ~ 0.008 | 0.007 (23%) | +| `shoulder_cut_pct` | 0.002 | 0.0015 ~ 0.0025 | 0.002 (23%) | +| `stop_atr_mult` | 2.5 | 2.2 ~ 2.6 | 2.6 (20%) | +| `target_atr_mult` | 2.7 | 2.3 ~ 3.0 | 3.2 (13%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 이전 장에서는 +- 실매 동기간 청산 1건 · 승률 0% · 합산 PnL 0원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #92 · 거래 1건 · 승률 100.0% · PF 9999.00 · PnL 2,061원 +- 승률이 비정상적으로 높고 표본이 작습니다 → **해당 장에만 맞는 과적합** 신호. + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-23 22:23:15 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +## 「이전 장에서는」 +지난 4일간의 Optuna TPE 백테스트에서는 우수한 PF와 100% 승률을 보인 전략 후보들이 나왔습니다. 특히 단 1건의 거래로 높은 PnL을 기록한 조합도 있었습니다. 그러나 이는 거래 샘플이 극히 적어 우연한 승리일 가능성이 높고, **과적합 위험이 66%에 달한다는 점**을 명확히 인지해야 합니다. 백테스트 결과와 달리 실제 계좌에서는 동 기간 1건의 거래에서 손실이 발생하여, **실매와 백테스트 간의 큰 괴리('고스트 퍼지' 등)**가 나타났습니다. + +## 「앞으로 장에서는」 +**표본 부족**과 **과적합 위험**이 매우 높아 이 전략을 당장 실매에 적용하는 것은 위험합니다. 최소 5거래일 이상의 긴 기간으로 재검증하고, 웹백테스트를 통해 시장 상황 변화에 대한 강건성을 반드시 확인해야 합니다. 만약 적용을 고려한다면, 페이퍼 트레이딩 또는 소액 모의 투자를 통해 실시간 환경에서의 실제 성과를 꼼꼼히 관찰하세요. 백테스트의 비정상적인 승률이나 높은 PnL보다, 시장 변동성에 대한 **손절 기능 작동 여부** 등 리스크 관리에 집중해야 합니다. 백테스트 결과는 미래 수익을 보장하지 않으며, 특정 종목 매수 추천이나 확정 수익을 약속할 수 없음을 다시 한번 강조합니다. diff --git a/kis_trader/backtest/results/optuna_tail_tpe_20260828_053537.briefing.md b/kis_trader/backtest/results/optuna_tail_tpe_20260828_053537.briefing.md new file mode 100644 index 0000000..fc06de7 --- /dev/null +++ b/kis_trader/backtest/results/optuna_tail_tpe_20260828_053537.briefing.md @@ -0,0 +1,34 @@ +# Optuna 브리핑 — 꼬리 (tpe) +기간: 2026-08-18 ~ 2026-08-28 (거래일≈11일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥36 | 후보 게이트: WR≥40% PF≥1 · min_trades≥36 +- min_trades=36: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 0건 · 사후합격(results_gated) 0건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 33.0%** · **적용 가능도 67.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈11일 — 표본 일수 상대적 양호 + - 거래 표본: +25.0p — 통과 후보 없음 — 적용 불가 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +8.0p — 학습·gated 모두 비어 있음 + - PnL 고원: +0.0p — 고원 판정 스킵 +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 이전 장에서는 +- 실매 동기간 청산 6건 · 승률 67% · 합산 PnL -4,743원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 이 기간 백테에서는 **유의미한 통과 조합이 거의 없었습니다.** 유니버스·틱·손절·시간창을 먼저 점검하세요 (돌파 0건이면 특히). + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 현재 사후합격 0건 → 파라미터 유지(현행 DB) + 유니버스/데이터 점검이 우선입니다. + +_생성: 2026-08-28 05:35:37 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +「이전 장에서는」 +지난 11거래일 실매는 소폭 손실을 기록했으며, 백테스트에서는 **최소 기준을 통과한 유효한 전략 조합이 단 한 건도 없었습니다.** 이는 해당 짧은 기간 내 로버스트한 전략을 찾기 어려웠음을 시사하며, 실매와 백테스트 간의 괴리는 고스트 퍼지, 유니버스 차이 등으로 발생할 수 있음을 항상 유념해야 합니다. + +「앞으로 장에서는」 +현재 사후 합격된 전략이 전무하므로, **파라미터 유지 및 유니버스/데이터 점검이 최우선입니다.** 어떤 조합이라도 **과적합 및 표본 부족 위험**이 항상 존재하며, 실매↔백테스트 괴리는 필연적입니다. 따라서 1-2일치 Optuna 결과만으로 실매에 적용하는 것은 절대 금물이며, 최소 5거래일 이상 **다일 재검증**을 통해 전략의 견고함을 반드시 확인해야 합니다. 확정 수익을 약속하거나 특정 종목 매수를 추천하지 않습니다. diff --git a/kis_trader/backtest/results/optuna_tail_tpe_20260828_060704.briefing.md b/kis_trader/backtest/results/optuna_tail_tpe_20260828_060704.briefing.md new file mode 100644 index 0000000..6d92baf --- /dev/null +++ b/kis_trader/backtest/results/optuna_tail_tpe_20260828_060704.briefing.md @@ -0,0 +1,34 @@ +# Optuna 브리핑 — 꼬리 (tpe) +기간: 2026-08-18 ~ 2026-08-28 (거래일≈11일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥36 | 후보 게이트: WR≥40% PF≥1 · min_trades≥36 +- min_trades=36: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 0건 · 사후합격(results_gated) 0건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 33.0%** · **적용 가능도 67.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈11일 — 표본 일수 상대적 양호 + - 거래 표본: +25.0p — 통과 후보 없음 — 적용 불가 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +8.0p — 학습·gated 모두 비어 있음 + - PnL 고원: +0.0p — 고원 판정 스킵 +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 이전 장에서는 +- 실매 동기간 청산 6건 · 승률 67% · 합산 PnL -4,743원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 이 기간 백테에서는 **유의미한 통과 조합이 거의 없었습니다.** 유니버스·틱·손절·시간창을 먼저 점검하세요 (돌파 0건이면 특히). + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 현재 사후합격 0건 → 파라미터 유지(현행 DB) + 유니버스/데이터 점검이 우선입니다. + +_생성: 2026-08-28 06:07:04 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +**이전 장에서는** +지난 11거래일간의 Optuna TPE 백테스트에서 유의미한 전략 조합이 단 한 건도 발견되지 않았습니다. 이는 '최소 거래 36건' 기준을 충족한 조합이 없어 **표본 부족** 또는 전략 자체에 문제가 있음을 시사합니다. 동기간 실제 매매에서는 손실(-4,743원)을 기록했으며, 이는 백테스트 환경(고스트·유니버스·틱 경로 차이)과 실제 시장 간의 **괴리**를 명확히 보여줍니다. 백테스트 결과가 좋았더라도 실매와 큰 차이를 보일 수 있음을 인지해야 합니다. + +**앞으로 장에서는** +현재 결과는 전략의 **과적합 및 표본 부족** 위험이 매우 높으므로, 어떠한 조합도 실매에 바로 적용해서는 안 됩니다. **실매와 백테의 괴리**를 줄이고자 한다면, 먼저 유니버스, 틱 데이터, 손절/시간창 등 전략의 기본 설정을 면밀히 점검해야 합니다. `results_gated`에 합격한 후보가 0건인 만큼, 새로운 파라미터 탐색보다 현재 전략의 근본적인 문제점을 찾는 것이 우선입니다. 최소 5거래일 이상의 다일 재검증과 웹백테를 통해 전략의 안정성을 충분히 확인한 후 신중하게 접근하시길 바랍니다. diff --git a/kis_trader/backtest/results/optuna_tail_tpe_20260830_031403.briefing.md b/kis_trader/backtest/results/optuna_tail_tpe_20260830_031403.briefing.md new file mode 100644 index 0000000..23f3efc --- /dev/null +++ b/kis_trader/backtest/results/optuna_tail_tpe_20260830_031403.briefing.md @@ -0,0 +1,66 @@ +# Optuna 브리핑 — 꼬리 (tpe) +기간: 2026-08-24 ~ 2026-08-28 (거래일≈5일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥1 | 후보 게이트: WR≥40% PF≥1 · min_trades≥1 +- min_trades=1: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 163건 · 사후합격(results_gated) 129건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 0.0%** · **적용 가능도 100.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈5일 — 표본 일수 상대적 양호 + - 거래 표본: +0.0p — 상위 후보 거래 27건 — 상대적 양호 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +0.0p — 사후합격 129건 · stable 10건 + - PnL 고원: +0.0p — 고원 비율 1% (1건) +- 임계값 분포 표본: results_gated 상위 30건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `entry_mode` | — | — | align (100%) | +| `cand_limit` | 0.0 | 0.0 ~ 20.0 | 0 (67%) | +| `max_daily_change` | 35.5 | 30.0 ~ 47.75 | 29.0 (10%) | +| `min_drop_rate` | 0.115 | 0.09 ~ 0.12875 | 0.105 (13%) | +| `min_recovery_ratio` | 0.12 | 0.1 ~ 0.15 | 0.12 (17%) | +| `tail_ratio_min` | 0.9 | 0.7 ~ 1.1 | 0.5 (17%) | +| `tail_pct_min` | 0.00575 | 0.004125 ~ 0.006375 | 0.006 (23%) | +| `max_rec_3m` | 0.895 | 0.8225 ~ 0.9275 | 0.95 (10%) | +| `shoulder_min_high` | 0.008 | 0.006 ~ 0.00975 | 0.009 (20%) | +| `shoulder_cut_pct` | 0.0015 | 0.000625 ~ 0.002 | 0.0005 (27%) | +| `stop_atr_mult` | 2.6 | 2.025 ~ 2.775 | 2.7 (20%) | +| `target_atr_mult` | 2.65 | 1.925 ~ 3.1 | 3.4 (10%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 최종 선택 후보 (규칙 · DB 적용 전 확인) +- 아래는 **역할이 다른** 4종 후보입니다. mode Top10 1위 ≠ 익절 대표 · **DB 1순위는 사후합격(gated)** 입니다. +- mode pool: `positive` · scoring: `band_proximity_p25_p75` + +### 후보 4종 +- **① 사후합격 1위 (apply: gated)** (trial #71): PnL 42,930원 · WR 66.7% · PF 3.17 · 거래 27건 · 안정점수 -254 · 호가OFF · 휩쏘ON sub15s lb120s dip0.90% tol0.10% +- **② 안정 1위 (apply: stable)** (trial #188): PnL 37,427원 · WR 75.0% · PF 4.77 · 거래 12건 · 안정점수 5866 · 호가OFF · 휩쏘ON sub45s lb120s dip0.90% tol0.15% +- **③ mode_combo (apply: mode)** (trial 없음(조립)): PnL 34,062원 · WR 81.2% · PF 4.15 · 거래 16건 · 안정점수 5235 · 호가OFF · 휩쏘ON sub45s lb120s dip0.20% tol0.05% +- **④ mode Top10 1위 (apply: consensus · 밴드 전형 trial)** (trial #57): PnL 31,464원 · WR 70.6% · PF 2.73 · 거래 17건 · 안정점수 849 · 근접 97.0% (46/52축 밴드안) · 호가OFF · 휩쏘ON sub45s lb120s dip0.40% tol0.10% +- mode Top10 meta: pool 129건 · 축 52개 + +### 추천 (자동 · confirm 필수) +1. **실매 DB 적용 1순위 → ① 사후합격 1위** (`optunaApply gated`) — WR/PF·min_trades 사후 통과. +2. **변동성·손실일 줄이기 → ② 안정 1위** (`stable`) — PnL보다 일별 안정 우선 시. +3. **2차 narrow·대표 숫자 → ③ mode_combo** — 축별 최빈 조립 · trial 번호 없음 · 익절은 pool 최빈값 참고. +4. **④ mode Top10 1위** — 양수 pool 전형 trial(다축 밴드 근접). **익절 하나로 쓰지 말 것** · 표에서 「보기」→ consensus apply. + +## 이전 장에서는 +- 실매 동기간 청산 6건 · 승률 67% · 합산 PnL -9,520원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #71 · 거래 27건 · 승률 66.7% · PF 3.17 · PnL 42,930원 + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-30 03:14:17 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +**이전 장에서는** +지난 실제 거래에서는 6건의 청산으로 -9,520원의 PnL을 기록했습니다. 이는 Optuna 백테스트 사후합격 1위(42,930원, 거래 27건, WR 66.7%)와 차이가 있지만, 실매와 백테스트 간 괴리(고스트 퍼지 등)는 정상적인 현상임을 인지해야 합니다. + +**앞으로 장에서는** +Optuna 최적 조합을 단기간 내 실매에 즉시 적용하는 것은 위험합니다. 기간이 짧아 과적합, 표본 부족 위험이 높으며, 백테스트와 실제 거래 간 괴리를 늘 경고합니다. 특히 `mode Top10 1위`를 익절 대표로 단정하지 마시고, DB 1순위는 사후합격임을 기억해야 합니다. 변동성 장세에서는 손절 기능이 살아있는지 먼저 확인하고, 페이퍼/소액으로 1~2일 관찰 후 확대해야 합니다. 특정 종목 추천이나 확정 수익은 약속할 수 없습니다. diff --git a/kis_trader/backtest/results/optuna_tail_tpe_20260830_033748.briefing.md b/kis_trader/backtest/results/optuna_tail_tpe_20260830_033748.briefing.md new file mode 100644 index 0000000..2e961bf --- /dev/null +++ b/kis_trader/backtest/results/optuna_tail_tpe_20260830_033748.briefing.md @@ -0,0 +1,66 @@ +# Optuna 브리핑 — 꼬리 (tpe) +기간: 2026-08-24 ~ 2026-08-28 (거래일≈5일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥1 | 후보 게이트: WR≥40% PF≥1 · min_trades≥1 +- min_trades=1: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 162건 · 사후합격(results_gated) 112건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 0.0%** · **적용 가능도 100.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈5일 — 표본 일수 상대적 양호 + - 거래 표본: +0.0p — 상위 후보 거래 22건 — 상대적 양호 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +0.0p — 사후합격 112건 · stable 10건 + - PnL 고원: +0.0p — 고원 비율 1% (1건) +- 임계값 분포 표본: results_gated 상위 30건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `entry_mode` | — | — | align (100%) | +| `cand_limit` | 0.0 | 0.0 ~ 20.0 | 0 (53%) | +| `max_daily_change` | 48.0 | 38.0 ~ 56.75 | 39.0 (13%) | +| `min_drop_rate` | 0.1125 | 0.09125 ~ 0.12875 | 0.11 (17%) | +| `min_recovery_ratio` | 0.14 | 0.1125 ~ 0.1575 | 0.15 (17%) | +| `tail_ratio_min` | 0.9 | 0.6 ~ 1.1 | 0.9 (20%) | +| `tail_pct_min` | 0.005 | 0.004 ~ 0.006875 | 0.005 (13%) | +| `max_rec_3m` | 0.915 | 0.8925 ~ 0.94 | 0.92 (17%) | +| `shoulder_min_high` | 0.008 | 0.005 ~ 0.012 | 0.005 (20%) | +| `shoulder_cut_pct` | 0.002 | 0.0015 ~ 0.0025 | 0.0025 (27%) | +| `stop_atr_mult` | 1.7 | 1.4 ~ 2.075 | 1.5 (13%) | +| `target_atr_mult` | 2.0 | 1.625 ~ 2.375 | 1.9 (10%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 최종 선택 후보 (규칙 · DB 적용 전 확인) +- 아래는 **역할이 다른** 4종 후보입니다. mode Top10 1위 ≠ 익절 대표 · **DB 1순위는 사후합격(gated)** 입니다. +- mode pool: `positive` · scoring: `band_proximity_p25_p75` + +### 후보 4종 +- **① 사후합격 1위 (apply: gated)** (trial #164): PnL 53,356원 · WR 63.6% · PF 5.80 · 거래 22건 · 안정점수 2079 · 호가OFF · 휩쏘ON sub15s lb60s dip0.20% tol0.25% +- **② 안정 1위 (apply: stable)** (trial #182): PnL 13,739원 · WR 72.7% · PF 2.69 · 거래 11건 · 안정점수 3172 · 호가OFF · 휩쏘OFF +- **③ mode_combo (apply: mode)** (trial 없음(조립)): PnL 28,189원 · WR 69.2% · PF 1.81 · 거래 26건 · 안정점수 -1705 · 호가OFF · 휩쏘ON sub45s lb60s dip0.20% tol0.25% +- **④ mode Top10 1위 (apply: consensus · 밴드 전형 trial)** (trial #72): PnL 24,601원 · WR 84.6% · PF 7.48 · 거래 13건 · 안정점수 84 · 근접 94.7% (44/52축 밴드안) · 호가OFF · 휩쏘ON sub45s lb60s dip0.20% tol0.25% +- mode Top10 meta: pool 113건 · 축 52개 + +### 추천 (자동 · confirm 필수) +1. **실매 DB 적용 1순위 → ① 사후합격 1위** (`optunaApply gated`) — WR/PF·min_trades 사후 통과. +2. **변동성·손실일 줄이기 → ② 안정 1위** (`stable`) — PnL보다 일별 안정 우선 시. +3. **2차 narrow·대표 숫자 → ③ mode_combo** — 축별 최빈 조립 · trial 번호 없음 · 익절은 pool 최빈값 참고. +4. **④ mode Top10 1위** — 양수 pool 전형 trial(다축 밴드 근접). **익절 하나로 쓰지 말 것** · 표에서 「보기」→ consensus apply. + +## 이전 장에서는 +- 실매 동기간 청산 6건 · 승률 67% · 합산 PnL -9,520원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #164 · 거래 22건 · 승률 63.6% · PF 5.80 · PnL 53,356원 + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-30 03:38:02 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +**이전 장에서는** +지난 5거래일(2026-08-24~28) 백테스트에서 Optuna 사후합격 1위는 PnL 53,356원(거래 22건)을 기록했습니다. 하지만 실제 운용에서는 PnL -9,520원(거래 6건)으로 백테스트와 실매 간의 괴리가 컸습니다. 이는 고스트 슬리피지 등 실전 환경 차이로 인한 것입니다. + +**앞으로 장에서는** +5거래일은 표본 부족으로 과적합 위험이 크니, Optuna best를 바로 실매 적용해선 안 됩니다. mode Top10 1위는 익절 대표가 아니며, DB 1순위는 사후합격입니다. 반드시 페이퍼/소액·모의로 1~2일 추가 검증하고, 백테스트-실매 괴리(고스트 슬리피지)와 손절 작동 여부를 확인하세요. 어떠한 종목 추천이나 확정 수익도 보장하지 않습니다. diff --git a/kis_trader/backtest/results/optuna_tail_tpe_20260830_035630.briefing.md b/kis_trader/backtest/results/optuna_tail_tpe_20260830_035630.briefing.md new file mode 100644 index 0000000..38e629c --- /dev/null +++ b/kis_trader/backtest/results/optuna_tail_tpe_20260830_035630.briefing.md @@ -0,0 +1,68 @@ +# Optuna 브리핑 — 꼬리 (tpe) +기간: 2026-08-24 ~ 2026-08-28 (거래일≈5일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥1 | 후보 게이트: WR≥40% PF≥1 · min_trades≥1 +- min_trades=1: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 60건 · 사후합격(results_gated) 56건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 24.0%** · **적용 가능도 76.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈5일 — 표본 일수 상대적 양호 + - 거래 표본: +18.0p — 상위 후보 거래 3건 — 표본 부족 + - 승률·PF 이상치: +6.0p — PF 11.40 + - 게이트 커버: +0.0p — 사후합격 56건 · stable 8건 + - PnL 고원: +0.0p — 고원 비율 2% (1건) +- 임계값 분포 표본: results_gated 상위 30건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `entry_mode` | — | — | limit_atr (100%) | +| `cand_limit` | 0.0 | 0.0 ~ 20.0 | 0 (57%) | +| `max_daily_change` | 40.5 | 34.0 ~ 52.0 | 52.0 (13%) | +| `min_drop_rate` | 0.06 | 0.045 ~ 0.09625 | 0.06 (10%) | +| `min_recovery_ratio` | 0.135 | 0.1025 ~ 0.17 | 0.08 (17%) | +| `tail_ratio_min` | 1.05 | 0.8 ~ 1.275 | 1.2 (17%) | +| `tail_pct_min` | 0.00675 | 0.006 ~ 0.0085 | 0.0065 (17%) | +| `max_rec_3m` | 0.945 | 0.8775 ~ 0.97 | 0.98 (20%) | +| `shoulder_min_high` | 0.0075 | 0.006 ~ 0.009 | 0.008 (20%) | +| `shoulder_cut_pct` | 0.0035 | 0.003 ~ 0.0045 | 0.0045 (27%) | +| `stop_atr_mult` | 1.9 | 1.3 ~ 2.475 | 1.3 (10%) | +| `target_atr_mult` | 2.55 | 2.225 ~ 2.9 | 2.9 (13%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 최종 선택 후보 (규칙 · DB 적용 전 확인) +- 아래는 **역할이 다른** 4종 후보입니다. mode Top10 1위 ≠ 익절 대표 · **DB 1순위는 사후합격(gated)** 입니다. +- mode pool: `positive` · scoring: `band_proximity_p25_p75` + +### 후보 4종 +- **① 사후합격 1위 (apply: gated)** (trial #157): PnL 3,701원 · WR 66.7% · PF 11.40 · 거래 3건 · 안정점수 100 · 호가OFF · 휩쏘OFF +- **② 안정 1위 (apply: stable)** (trial #176): PnL 4,057원 · WR 100.0% · PF 9999.00 · 거래 2건 · 안정점수 1850 · 호가OFF · 휩쏘OFF +- **③ mode_combo (apply: mode)** (trial 없음(조립)): PnL 1,494원 · WR 50.0% · PF 5.20 · 거래 2건 · 안정점수 -356 · 호가OFF · 휩쏘OFF +- **④ mode Top10 1위 (apply: consensus · 밴드 전형 trial)** (trial #17): PnL 1,494원 · WR 50.0% · PF 5.20 · 거래 2건 · 안정점수 -356 · 근접 95.2% (43/53축 밴드안) · 호가OFF · 휩쏘OFF +- mode Top10 meta: pool 56건 · 축 53개 + +### 추천 (자동 · confirm 필수) +1. **실매 DB 적용 1순위 → ① 사후합격 1위** (`optunaApply gated`) — WR/PF·min_trades 사후 통과. +2. **변동성·손실일 줄이기 → ② 안정 1위** (`stable`) — PnL보다 일별 안정 우선 시. +3. **2차 narrow·대표 숫자 → ③ mode_combo** — 축별 최빈 조립 · trial 번호 없음 · 익절은 pool 최빈값 참고. +4. **④ mode Top10 1위** — 양수 pool 전형 trial(다축 밴드 근접). **익절 하나로 쓰지 말 것** · 표에서 「보기」→ consensus apply. + +## 이전 장에서는 +- 실매 동기간 청산 6건 · 승률 67% · 합산 PnL -9,520원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #157 · 거래 3건 · 승률 66.7% · PF 11.40 · PnL 3,701원 + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-30 03:56:41 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +## 이전 장에서는 + +이전 5거래일 실매는 6건, 승률 67%, -9,520원 PnL로 백테와 괴리가 있습니다. Optuna 사후합격 1위(trial #157)는 3건 거래, 승률 66.7%, PF 11.40, PnL 3,701원을 기록했습니다. + +## 앞으로 장에서는 + +5일 백테 결과는 기간/거래 3건으로 과적합 위험이 높습니다(적용 가능도 76%는 상대적 지표). 백테-실매 괴리(고스트퍼지 등)를 항상 유념하십시오. `사후합격 1위`라도 바로 실매 적용보다 다일 재검증, 페이퍼/소액 관찰 후 확대하세요. 시장 변동 시 승률보다 손절선 유지에 집중하며, `mode Top10 1위`를 익절 대표로 단정하지 마세요. DB 1순위는 `사후합격`입니다. diff --git a/kis_trader/backtest/results/optuna_tail_tpe_20260830_041434.briefing.md b/kis_trader/backtest/results/optuna_tail_tpe_20260830_041434.briefing.md new file mode 100644 index 0000000..0f66c10 --- /dev/null +++ b/kis_trader/backtest/results/optuna_tail_tpe_20260830_041434.briefing.md @@ -0,0 +1,67 @@ +# Optuna 브리핑 — 꼬리 (tpe) +기간: 2026-08-24 ~ 2026-08-28 (거래일≈5일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥1 | 후보 게이트: WR≥40% PF≥1 · min_trades≥1 +- min_trades=1: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 36건 · 사후합격(results_gated) 36건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 43.0%** · **적용 가능도 57.0%** · 판정: **주의** — 적용 가능도 중간 — 다일(≥5일) 재검증·웹백테 후 소액만 + - 표본 장일: +0.0p — 거래일≈5일 — 표본 일수 상대적 양호 + - 거래 표본: +18.0p — 상위 후보 거래 3건 — 표본 부족 + - 승률·PF 이상치: +25.0p — 승률 100.0% + 거래 3건 · PF 9999.00 (소수 거래 폭증) + - 게이트 커버: +0.0p — 사후합격 36건 · stable 4건 + - PnL 고원: +0.0p — 고원 비율 3% (1건) +- 임계값 분포 표본: results_gated 상위 30건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `entry_mode` | — | — | limit_atr (100%) | +| `cand_limit` | 0.0 | 0.0 ~ 0.0 | 0 (87%) | +| `max_daily_change` | 49.0 | 42.0 ~ 55.5 | 49.0 (13%) | +| `min_drop_rate` | 0.0525 | 0.0225 ~ 0.08375 | 0.03 (17%) | +| `min_recovery_ratio` | 0.215 | 0.165 ~ 0.255 | 0.24 (17%) | +| `tail_ratio_min` | 0.8 | 0.525 ~ 1.0 | 0.8 (17%) | +| `tail_pct_min` | 0.0025 | 0.001 ~ 0.002875 | 0.0025 (27%) | +| `max_rec_3m` | 0.81 | 0.76 ~ 0.8575 | 0.81 (17%) | +| `shoulder_min_high` | 0.009 | 0.00725 ~ 0.012 | 0.009 (27%) | +| `shoulder_cut_pct` | 0.0025 | 0.002 ~ 0.003 | 0.002 (33%) | +| `stop_atr_mult` | 1.25 | 1.1 ~ 1.675 | 1.1 (23%) | +| `target_atr_mult` | 2.5 | 1.7 ~ 2.875 | 3.1 (13%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 최종 선택 후보 (규칙 · DB 적용 전 확인) +- 아래는 **역할이 다른** 4종 후보입니다. mode Top10 1위 ≠ 익절 대표 · **DB 1순위는 사후합격(gated)** 입니다. +- mode pool: `positive` · scoring: `band_proximity_p25_p75` + +### 후보 4종 +- **① 사후합격 1위 (apply: gated)** (trial #196): PnL 3,226원 · WR 100.0% · PF 9999.00 · 거래 3건 · 안정점수 136 · 호가OFF · 휩쏘OFF +- **② 안정 1위 (apply: stable)** (trial #196): PnL 3,226원 · WR 100.0% · PF 9999.00 · 거래 3건 · 안정점수 136 · 호가OFF · 휩쏘OFF +- **③ mode_combo (apply: mode)** (trial 없음(조립)): PnL 1,066원 · WR 100.0% · PF 9999.00 · 거래 1건 · 안정점수 1066 · 호가ON spr5.3 r0.25 ask×64 · 휩쏘ON sub30s lb60s dip0.60% tol0.15% +- **④ mode Top10 1위 (apply: consensus · 밴드 전형 trial)** (trial #19): PnL 1,066원 · WR 100.0% · PF 9999.00 · 거래 1건 · 안정점수 1066 · 근접 95.5% (43/53축 밴드안) · 호가ON spr2.8 r0.30 ask×65 · 휩쏘OFF +- mode Top10 meta: pool 36건 · 축 53개 + +### 추천 (자동 · confirm 필수) +1. **실매 DB 적용 1순위 → ① 사후합격 1위** (`optunaApply gated`) — WR/PF·min_trades 사후 통과. +2. **변동성·손실일 줄이기 → ② 안정 1위** (`stable`) — PnL보다 일별 안정 우선 시. +3. **2차 narrow·대표 숫자 → ③ mode_combo** — 축별 최빈 조립 · trial 번호 없음 · 익절은 pool 최빈값 참고. +4. **④ mode Top10 1위** — 양수 pool 전형 trial(다축 밴드 근접). **익절 하나로 쓰지 말 것** · 표에서 「보기」→ consensus apply. + +## 이전 장에서는 +- 실매 동기간 청산 6건 · 승률 67% · 합산 PnL -9,520원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #196 · 거래 3건 · 승률 100.0% · PF 9999.00 · PnL 3,226원 +- 승률이 비정상적으로 높고 표본이 작습니다 → **해당 장에만 맞는 과적합** 신호. + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-30 04:14:45 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +## 이전 장에서는 +지난 5거래일 백테스트는 3건 거래, 100% 승률로 PnL 3,226원을 기록했으나, 이는 **표본 부족**과 **과적합 위험**이 매우 높습니다. 실제 매매는 -9,520원 손실로, 백테스트와 실매 간의 **괴리(고스트퍼지)**가 명확히 드러났습니다. + +## 앞으로 장에서는 +**과적합 경고('주의')**로 인해 Optuna 최적값을 즉시 실매에 적용은 위험합니다. `mode Top10 1위`를 익절 대표로 단정해선 안 됩니다. `사후합격` 후보도 다일 재검증 후 소액·모의 투자로 1~2일 **괴리**를 확인해야 합니다. 승률보다 **손절 기능**을 최우선으로 관리하며, 특정 종목 추천이나 확정 수익 약속은 불가합니다. diff --git a/kis_trader/backtest/results/optuna_tail_tpe_20260830_113222.briefing.md b/kis_trader/backtest/results/optuna_tail_tpe_20260830_113222.briefing.md new file mode 100644 index 0000000..7459369 --- /dev/null +++ b/kis_trader/backtest/results/optuna_tail_tpe_20260830_113222.briefing.md @@ -0,0 +1,66 @@ +# Optuna 브리핑 — 꼬리 (tpe) +기간: 2026-08-24 ~ 2026-08-28 (거래일≈5일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥1 | 후보 게이트: WR≥40% PF≥1 · min_trades≥1 +- min_trades=1: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 155건 · 사후합격(results_gated) 109건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 0.0%** · **적용 가능도 100.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈5일 — 표본 일수 상대적 양호 + - 거래 표본: +0.0p — 상위 후보 거래 13건 — 상대적 양호 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +0.0p — 사후합격 109건 · stable 10건 + - PnL 고원: +0.0p — 고원 비율 1% (1건) +- 임계값 분포 표본: results_gated 상위 30건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `entry_mode` | — | — | align (100%) | +| `cand_limit` | 0.0 | 0.0 ~ 20.0 | 0 (60%) | +| `max_daily_change` | 46.0 | 36.25 ~ 50.0 | 50.0 (17%) | +| `min_drop_rate` | 0.105 | 0.08125 ~ 0.13125 | 0.14 (13%) | +| `min_recovery_ratio` | 0.19 | 0.1625 ~ 0.2275 | 0.13 (10%) | +| `tail_ratio_min` | 0.8 | 0.7 ~ 0.9 | 0.7 (33%) | +| `tail_pct_min` | 0.00525 | 0.0045 ~ 0.0055 | 0.0055 (27%) | +| `max_rec_3m` | 0.945 | 0.9 ~ 0.9675 | 0.97 (20%) | +| `shoulder_min_high` | 0.011 | 0.009 ~ 0.012 | 0.012 (20%) | +| `shoulder_cut_pct` | 0.003 | 0.0025 ~ 0.003875 | 0.003 (27%) | +| `stop_atr_mult` | 2.8 | 2.4 ~ 2.9 | 3.0 (23%) | +| `target_atr_mult` | 2.3 | 2.025 ~ 2.9 | 2.1 (17%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 최종 선택 후보 (규칙 · DB 적용 전 확인) +- 아래는 **역할이 다른** 4종 후보입니다. mode Top10 1위 ≠ 익절 대표 · **DB 1순위는 사후합격(gated)** 입니다. +- mode pool: `positive` · scoring: `band_proximity_p25_p75` + +### 후보 4종 +- **① 사후합격 1위 (apply: gated)** (trial #119): PnL 27,780원 · WR 92.3% · PF 27.58 · 거래 13건 · 안정점수 1307 · 호가OFF · 휩쏘ON sub15s lb120s dip0.30% tol0.05% +- **② 안정 1위 (apply: stable)** (trial #120): PnL 17,933원 · WR 100.0% · PF 9999.00 · 거래 6건 · 안정점수 4649 · 호가OFF · 휩쏘ON sub15s lb120s dip0.20% tol0.05% +- **③ mode_combo (apply: mode)** (trial 없음(조립)): PnL 28,631원 · WR 100.0% · PF 9999.00 · 거래 11건 · 안정점수 1381 · 호가OFF · 휩쏘ON sub15s lb180s dip0.20% tol0.05% +- **④ mode Top10 1위 (apply: consensus · 밴드 전형 trial)** (trial #135): PnL 9,625원 · WR 85.7% · PF 10.80 · 거래 7건 · 안정점수 -319 · 근접 96.4% (43/52축 밴드안) · 호가OFF · 휩쏘ON sub15s lb120s dip0.20% tol0.15% +- mode Top10 meta: pool 111건 · 축 52개 + +### 추천 (자동 · confirm 필수) +1. **실매 DB 적용 1순위 → ① 사후합격 1위** (`optunaApply gated`) — WR/PF·min_trades 사후 통과. +2. **변동성·손실일 줄이기 → ② 안정 1위** (`stable`) — PnL보다 일별 안정 우선 시. +3. **2차 narrow·대표 숫자 → ③ mode_combo** — 축별 최빈 조립 · trial 번호 없음 · 익절은 pool 최빈값 참고. +4. **④ mode Top10 1위** — 양수 pool 전형 trial(다축 밴드 근접). **익절 하나로 쓰지 말 것** · 표에서 「보기」→ consensus apply. + +## 이전 장에서는 +- 실매 동기간 청산 6건 · 승률 67% · 합산 PnL -9,520원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #119 · 거래 13건 · 승률 92.3% · PF 27.58 · PnL 27,780원 + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-30 11:32:35 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +**이전 장에서는** +지난주 실매는 6건 거래에 -9,520원 손실을 기록했습니다. 백테스트 Optuna는 사후합격 1위가 13건 거래, 승률 92.3%, PnL 27,780원을 보여주었죠. 백테와 실매 결과는 늘 차이가 크니 유의해야 합니다. + +**앞으로 장에서는** +이번 Optuna는 5거래일의 짧은 기간으로, 과적합·표본 부족 위험이 높습니다. '사후합격 1위'만 주목하며, mode Top10 1위가 익절 대표는 아님을 명심하세요. 백테스트 결과는 고스트퍼지 등 실매 환경과 괴리가 큽니다. 바로 적용보다 페이퍼/소액으로 1~2일 관찰하며 손절 생존 여부를 먼저 확인해야 합니다. 특정 종목 추천이나 확정 수익은 보장할 수 없습니다. diff --git a/kis_trader/backtest/results/optuna_tail_tpe_20260830_115645.briefing.md b/kis_trader/backtest/results/optuna_tail_tpe_20260830_115645.briefing.md new file mode 100644 index 0000000..cd71c02 --- /dev/null +++ b/kis_trader/backtest/results/optuna_tail_tpe_20260830_115645.briefing.md @@ -0,0 +1,70 @@ +# Optuna 브리핑 — 꼬리 (tpe) +기간: 2026-08-24 ~ 2026-08-28 (거래일≈5일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥1 | 후보 게이트: WR≥40% PF≥1 · min_trades≥1 +- min_trades=1: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 180건 · 사후합격(results_gated) 130건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 0.0%** · **적용 가능도 100.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈5일 — 표본 일수 상대적 양호 + - 거래 표본: +0.0p — 상위 후보 거래 14건 — 상대적 양호 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +0.0p — 사후합격 130건 · stable 10건 + - PnL 고원: +0.0p — 고원 비율 1% (1건) +- 임계값 분포 표본: results_gated 상위 30건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `entry_mode` | — | — | align (100%) | +| `cand_limit` | 0.0 | 0.0 ~ 0.0 | 0 (83%) | +| `max_daily_change` | 44.0 | 31.5 ~ 52.5 | 47.0 (7%) | +| `min_drop_rate` | 0.09 | 0.06 ~ 0.105 | 0.115 (13%) | +| `min_recovery_ratio` | 0.12 | 0.1 ~ 0.14 | 0.14 (23%) | +| `tail_ratio_min` | 1.0 | 0.8 ~ 1.175 | 1.1 (17%) | +| `tail_pct_min` | 0.005 | 0.0045 ~ 0.0055 | 0.0055 (23%) | +| `max_rec_3m` | 0.765 | 0.72 ~ 0.8575 | 0.72 (13%) | +| `shoulder_min_high` | 0.006 | 0.004 ~ 0.00775 | 0.006 (23%) | +| `shoulder_cut_pct` | 0.0015 | 0.001 ~ 0.001875 | 0.001 (30%) | +| `stop_atr_mult` | 2.25 | 1.825 ~ 2.8 | 2.8 (13%) | +| `target_atr_mult` | 1.65 | 1.025 ~ 2.55 | 0.9 (13%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 최종 선택 후보 (규칙 · DB 적용 전 확인) +- 아래는 **역할이 다른** 4종 후보입니다. mode Top10 1위 ≠ 익절 대표 · **DB 1순위는 사후합격(gated)** 입니다. +- mode pool: `positive` · scoring: `band_proximity_p25_p75` + +### 후보 4종 +- **① 사후합격 1위 (apply: gated)** (trial #172): PnL 43,526원 · WR 78.6% · PF 4.97 · 거래 14건 · 안정점수 6225 · 호가OFF · 휩쏘ON sub15s lb180s dip0.70% tol0.05% +- **② 안정 1위 (apply: stable)** (trial #172): PnL 43,526원 · WR 78.6% · PF 4.97 · 거래 14건 · 안정점수 6225 · 호가OFF · 휩쏘ON sub15s lb180s dip0.70% tol0.05% +- **③ mode_combo (apply: mode)** (trial 없음(조립)): PnL 35,182원 · WR 84.6% · PF 4.55 · 거래 13건 · 안정점수 4945 · 호가OFF · 휩쏘ON sub15s lb180s dip1.00% tol0.20% +- **④ mode Top10 1위 (apply: consensus · 밴드 전형 trial)** (trial #20): PnL 34,651원 · WR 79.0% · PF 4.97 · 거래 19건 · 안정점수 3897 · 근접 98.7% (47/52축 밴드안) · 호가OFF · 휩쏘ON sub15s lb180s dip1.00% tol0.20% +- mode Top10 meta: pool 131건 · 축 52개 + +### 추천 (자동 · confirm 필수) +1. **실매 DB 적용 1순위 → ① 사후합격 1위** (`optunaApply gated`) — WR/PF·min_trades 사후 통과. +2. **변동성·손실일 줄이기 → ② 안정 1위** (`stable`) — PnL보다 일별 안정 우선 시. +3. **2차 narrow·대표 숫자 → ③ mode_combo** — 축별 최빈 조립 · trial 번호 없음 · 익절은 pool 최빈값 참고. +4. **④ mode Top10 1위** — 양수 pool 전형 trial(다축 밴드 근접). **익절 하나로 쓰지 말 것** · 표에서 「보기」→ consensus apply. + +## 이전 장에서는 +- 실매 동기간 청산 6건 · 승률 67% · 합산 PnL -9,520원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #172 · 거래 14건 · 승률 78.6% · PF 4.97 · PnL 43,526원 + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-30 11:56:58 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +안녕하세요, 한국 주식 퀀트 헤지펀드 리스크 매니저입니다. Optuna TPE 결과에 대한 코멘트입니다. + +--- + +### 이전 장에서는 +지난주(5거래일) 실매에서는 6건의 청산 거래로 승률 67%, 합산 PnL -9,520원을 기록했습니다. 반면, Optuna 백테스트의 사후합격 1위(trial #172)는 같은 기간 14건 거래, 승률 78.6%, PF 4.97, PnL 43,526원이라는 상이한 결과를 보였습니다. 이는 실매 환경과 백테스트 간의 고스트 스프레드, 틱 데이터 경로 차이 등 여러 괴리에서 기인합니다. + +### 앞으로 장에서는 +5거래일이라는 짧은 백테스트 기간은 과적합 및 표본 부족 위험이 높으므로, `optunaApply gated`된 전략이라도 즉시 실매에 적용하는 것은 위험합니다. 반드시 다일 재검증과 웹백테스트 과정을 거쳐 신중히 검토하시길 바랍니다. `mode Top10 1위`는 단순 참고용이며, 이를 익절 대표로 단정해선 안 됩니다. 실매 적용 전 페이퍼/소액으로 시장 괴리(고스트 스프레드 등)를 관찰하고, 장 변화 시 손절 기능이 정상 작동하는지 확인 후 확대하십시오. diff --git a/kis_trader/backtest/results/optuna_tail_tpe_20260830_121621.briefing.md b/kis_trader/backtest/results/optuna_tail_tpe_20260830_121621.briefing.md new file mode 100644 index 0000000..c928b3f --- /dev/null +++ b/kis_trader/backtest/results/optuna_tail_tpe_20260830_121621.briefing.md @@ -0,0 +1,68 @@ +# Optuna 브리핑 — 꼬리 (tpe) +기간: 2026-08-24 ~ 2026-08-28 (거래일≈5일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥1 | 후보 게이트: WR≥40% PF≥1 · min_trades≥1 +- min_trades=1: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 62건 · 사후합격(results_gated) 55건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 18.0%** · **적용 가능도 82.0%** · 판정: **상대적으로낮음** — 휴리스틱상 위험 상대적 낮음 — 그래도 다일 확인 권장 + - 표본 장일: +0.0p — 거래일≈5일 — 표본 일수 상대적 양호 + - 거래 표본: +18.0p — 상위 후보 거래 2건 — 표본 부족 + - 승률·PF 이상치: +0.0p — 이상치 없음 + - 게이트 커버: +0.0p — 사후합격 55건 · stable 5건 + - PnL 고원: +0.0p — 고원 비율 2% (1건) +- 임계값 분포 표본: results_gated 상위 30건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `entry_mode` | — | — | limit_atr (100%) | +| `cand_limit` | 0.0 | 0.0 ~ 20.0 | 0 (67%) | +| `max_daily_change` | 47.5 | 43.0 ~ 55.0 | 45.0 (10%) | +| `min_drop_rate` | 0.065 | 0.025 ~ 0.095 | 0.025 (13%) | +| `min_recovery_ratio` | 0.12 | 0.09 ~ 0.14 | 0.08 (17%) | +| `tail_ratio_min` | 0.8 | 0.6 ~ 1.075 | 0.8 (20%) | +| `tail_pct_min` | 0.00775 | 0.0065 ~ 0.0095 | 0.0095 (17%) | +| `max_rec_3m` | 0.78 | 0.76 ~ 0.8075 | 0.76 (20%) | +| `shoulder_min_high` | 0.008 | 0.004 ~ 0.009 | 0.009 (23%) | +| `shoulder_cut_pct` | 0.0035 | 0.003 ~ 0.0045 | 0.0035 (23%) | +| `stop_atr_mult` | 1.8 | 1.225 ~ 2.4 | 2.9 (10%) | +| `target_atr_mult` | 1.65 | 1.325 ~ 2.15 | 1.5 (13%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 최종 선택 후보 (규칙 · DB 적용 전 확인) +- 아래는 **역할이 다른** 4종 후보입니다. mode Top10 1위 ≠ 익절 대표 · **DB 1순위는 사후합격(gated)** 입니다. +- mode pool: `positive` · scoring: `band_proximity_p25_p75` + +### 후보 4종 +- **① 사후합격 1위 (apply: gated)** (trial #27): PnL 3,051원 · WR 50.0% · PF 9.57 · 거래 2건 · 안정점수 -356 · 호가OFF · 휩쏘OFF +- **② 안정 1위 (apply: stable)** (trial #27): PnL 3,051원 · WR 50.0% · PF 9.57 · 거래 2건 · 안정점수 -356 · 호가OFF · 휩쏘OFF +- **③ mode_combo (apply: mode)** (trial 없음(조립)): PnL 1,163원 · WR 100.0% · PF 9999.00 · 거래 1건 · 안정점수 1163 · 호가OFF · 휩쏘OFF +- **④ mode Top10 1위 (apply: consensus · 밴드 전형 trial)** (trial #14): PnL 1,659원 · WR 100.0% · PF 9999.00 · 거래 1건 · 안정점수 1659 · 근접 97.7% (47/53축 밴드안) · 호가OFF · 휩쏘OFF +- mode Top10 meta: pool 55건 · 축 53개 + +### 추천 (자동 · confirm 필수) +1. **실매 DB 적용 1순위 → ① 사후합격 1위** (`optunaApply gated`) — WR/PF·min_trades 사후 통과. +2. **변동성·손실일 줄이기 → ② 안정 1위** (`stable`) — PnL보다 일별 안정 우선 시. +3. **2차 narrow·대표 숫자 → ③ mode_combo** — 축별 최빈 조립 · trial 번호 없음 · 익절은 pool 최빈값 참고. +4. **④ mode Top10 1위** — 양수 pool 전형 trial(다축 밴드 근접). **익절 하나로 쓰지 말 것** · 표에서 「보기」→ consensus apply. + +## 이전 장에서는 +- 실매 동기간 청산 6건 · 승률 67% · 합산 PnL -9,520원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #27 · 거래 2건 · 승률 50.0% · PF 9.57 · PnL 3,051원 + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-30 12:16:31 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +## 이전 장에서는 + +이전 장에서는 실제 매매 손실(-9,520원)에도 불구하고, Optuna 백테스트 사후합격 1위는 3,051원의 수익을 기록했습니다. 이는 실매와 백테스트 간 고스트퍼지, 데이터 괴리로 인해 발생할 수 있는 정상적인 차이이지만, 동시에 강력한 경고입니다. 특히 백테스트 결과조차 단 2건의 거래로 표본이 매우 부족합니다. + +## 앞으로 장에서는 + +이번 Optuna 결과는 5일이라는 짧은 기간과 상위 후보 **거래 단 2건**이라는 **심각한 표본 부족**으로 과적합 위험이 높습니다. 백테스트의 높은 승률·PF는 실전에서 재현되기 어렵고, 실제 매매와의 괴리(고스트퍼지)가 발생할 수 있음을 강력히 경고합니다. Mode Top10 1위를 익절 대표로 단정 금지. DB 적용은 사후합격 1위 후보를 참조하되, 절대 수익을 보장하지 않습니다. 실매 전 반드시 페이퍼/소액 모의투자로 손절 기능과 로직을 충분히 검증하세요. diff --git a/kis_trader/backtest/results/optuna_tail_tpe_20260830_123432.briefing.md b/kis_trader/backtest/results/optuna_tail_tpe_20260830_123432.briefing.md new file mode 100644 index 0000000..17a67ad --- /dev/null +++ b/kis_trader/backtest/results/optuna_tail_tpe_20260830_123432.briefing.md @@ -0,0 +1,70 @@ +# Optuna 브리핑 — 꼬리 (tpe) +기간: 2026-08-24 ~ 2026-08-28 (거래일≈5일) · trials 200/200 +탐색 게이트: WR≥0% PF≥0 · min_trades≥1 | 후보 게이트: WR≥40% PF≥1 · min_trades≥1 +- min_trades=1: **합격 인정 최소 거래 건수**(표본 부족·우연 승률 방지). 이보다 적은 조합은 학습/후보 집계에서 제외됩니다. +학습용 통과 22건 · 사후합격(results_gated) 22건 + +## 과적합·적용 가능도 (휴리스틱) +- **과적합 위험 43.0%** · **적용 가능도 57.0%** · 판정: **주의** — 적용 가능도 중간 — 다일(≥5일) 재검증·웹백테 후 소액만 + - 표본 장일: +0.0p — 거래일≈5일 — 표본 일수 상대적 양호 + - 거래 표본: +18.0p — 상위 후보 거래 2건 — 표본 부족 + - 승률·PF 이상치: +25.0p — 승률 100.0% + 거래 2건 · PF 9999.00 (소수 거래 폭증) + - 게이트 커버: +0.0p — 사후합격 22건 · stable 10건 + - PnL 고원: +0.0p — 고원 비율 5% (1건) +- 임계값 분포 표본: results_gated 상위 22건 +| 파라미터 | median | p25~p75 | mode(비율) | +|---|---:|---|---| +| `entry_mode` | — | — | limit_atr (100%) | +| `cand_limit` | 20.0 | 20.0 ~ 20.0 | 20 (82%) | +| `max_daily_change` | 28.5 | 15.5 ~ 37.0 | 37.0 (14%) | +| `min_drop_rate` | 0.0575 | 0.03125 ~ 0.07875 | 0.085 (14%) | +| `min_recovery_ratio` | 0.19 | 0.1325 ~ 0.2425 | 0.22 (14%) | +| `tail_ratio_min` | 0.65 | 0.5 ~ 0.875 | 0.5 (23%) | +| `tail_pct_min` | 0.001 | 0.001 ~ 0.002 | 0.001 (36%) | +| `max_rec_3m` | 0.865 | 0.83 ~ 0.915 | 0.92 (14%) | +| `shoulder_min_high` | 0.008 | 0.006 ~ 0.009 | 0.008 (18%) | +| `shoulder_cut_pct` | 0.003 | 0.003 ~ 0.004375 | 0.003 (46%) | +| `stop_atr_mult` | 2.7 | 2.425 ~ 2.875 | 2.4 (18%) | +| `target_atr_mult` | 3.0 | 2.425 ~ 3.2 | 3.2 (14%) | +- _참고: 과적합%는 교차검증 점수가 아니라 표본·이상치·고원 휴리스틱입니다. 적용 가능도%=100−과적합위험%. DB 적용 버튼 활성 조건(gated PnL>0)과는 별개입니다._ + +## 최종 선택 후보 (규칙 · DB 적용 전 확인) +- 아래는 **역할이 다른** 4종 후보입니다. mode Top10 1위 ≠ 익절 대표 · **DB 1순위는 사후합격(gated)** 입니다. +- mode pool: `positive` · scoring: `band_proximity_p25_p75` + +### 후보 4종 +- **① 사후합격 1위 (apply: gated)** (trial #40): PnL 3,709원 · WR 100.0% · PF 9999.00 · 거래 2건 · 안정점수 3709 · 호가ON spr1.0 r0.95 ask×71 · 휩쏘OFF +- **② 안정 1위 (apply: stable)** (trial #40): PnL 3,709원 · WR 100.0% · PF 9999.00 · 거래 2건 · 안정점수 3709 · 호가ON spr1.0 r0.95 ask×71 · 휩쏘OFF +- **③ mode_combo (apply: mode)** (trial 없음(조립)): PnL 1,066원 · WR 100.0% · PF 9999.00 · 거래 1건 · 안정점수 1066 · 호가ON spr1.8 r1.00 ask×59 · 휩쏘OFF +- **④ mode Top10 1위 (apply: consensus · 밴드 전형 trial)** (trial #102): PnL 1,066원 · WR 100.0% · PF 9999.00 · 거래 1건 · 안정점수 1066 · 근접 98.5% (46/53축 밴드안) · 호가ON spr0.9 r1.00 ask×59 · 휩쏘OFF +- mode Top10 meta: pool 22건 · 축 53개 + +### 추천 (자동 · confirm 필수) +1. **실매 DB 적용 1순위 → ① 사후합격 1위** (`optunaApply gated`) — WR/PF·min_trades 사후 통과. +2. **변동성·손실일 줄이기 → ② 안정 1위** (`stable`) — PnL보다 일별 안정 우선 시. +3. **2차 narrow·대표 숫자 → ③ mode_combo** — 축별 최빈 조립 · trial 번호 없음 · 익절은 pool 최빈값 참고. +4. **④ mode Top10 1위** — 양수 pool 전형 trial(다축 밴드 근접). **익절 하나로 쓰지 말 것** · 표에서 「보기」→ consensus apply. +- ⚠ 안정 게이트 0건 → 안정 Top은 **점수순 폴백** (안정 게이트 0건 → 학습풀 안정점수순 TopN (마이너스 PnL 포함 · 상대비교)) + +## 이전 장에서는 +- 실매 동기간 청산 6건 · 승률 67% · 합산 PnL -9,520원 (백테 Optuna와 숫자 달라도 정상 — 고스트·유니버스·틱 경로 차이). +- 사후합격 1위: trial #40 · 거래 2건 · 승률 100.0% · PF 9999.00 · PnL 3,709원 +- 승률이 비정상적으로 높고 표본이 작습니다 → **해당 장에만 맞는 과적합** 신호. + +## 앞으로 장에서는 +- **1~2일 Optuna best를 바로 실매에 넣지 마세요.** 다일(≥5거래일) 재검증·웹백테 1회 후 검토. +- 적용 시에는 `results_gated` 후보만. 탐색용 best(objective)와 다를 수 있습니다. +- 장이 좁아지거나 변동성이 커지면: 손절·트레일·시간창이 먼저 깨집니다. 승률 자랑보다 **손절이 살아 있는지**를 보세요. +- 사후합격이 있어도 ‘내일도 같은 장’이 아닙니다. 페이퍼/소액·모의로 1~2일 관찰 후 확대하세요. + +_생성: 2026-08-30 12:34:42 (rule)_ + +## AI 코멘트 (이전 장 / 앞으로 장) [gemini] + +## 이전 장에서는 + +직전 실매는 6건 거래에 합산 PnL -9,520원으로 마감했습니다. 반면 Optuna 백테스트 사후합격 1위는 2건 거래로 승률 100%, PnL 3,709원을 기록했죠. 이는 백테스트 기간(5일)이 극히 짧고 거래 건수(1~2건)가 매우 적어 **과적합 위험**이 높다는 강력한 신호입니다. 실매와 백테스트 간의 **괴리(고스트퍼지 등)**도 분명히 확인됩니다. + +## 앞으로 장에서는 + +이번 Optuna 결과는 **주의** 등급이므로, 곧바로 실매 적용은 금물입니다. 현재 결과는 **표본 부족**과 **과적합**이 강하게 의심되며, DB 1순위가 특정 전략의 「익절 대표」로 단정될 수 없습니다. 최소 5거래일 이상 **재검증**과 **웹백테스트**를 거친 후 **페이퍼/소액**으로 운용하며 추가 검증이 필수입니다. 확정 수익을 약속하거나 특정 종목 매수를 추천하지 않습니다. diff --git a/kis_trader/database/db_manager.py b/kis_trader/database/db_manager.py index 8179322..0501fd2 100644 --- a/kis_trader/database/db_manager.py +++ b/kis_trader/database/db_manager.py @@ -108,6 +108,7 @@ class TradeDBExt: self._migrate_orders_drop_daily_side_unique() self._migrate_orders_pk_scope() self._migrate_orders_is_mock() + self._migrate_orders_broker_debug_columns() logger.info("📊 orders 테이블 확인/생성 완료") except Exception as e: logger.warning("orders 테이블 생성 실패(무시·폴백): %s", e) @@ -228,6 +229,25 @@ class TradeDBExt: except Exception as e: logger.warning("orders.is_mock 마이그레이션 실패(무시·폴백): %s", e) + def _migrate_orders_broker_debug_columns(self) -> None: + """orders — cancelable reconcile 디버그 스냅샷 컬럼.""" + specs = { + "broker_open_qty": "INT NULL", + "broker_open_odno": "VARCHAR(30) NULL", + "broker_reconcile_at": "VARCHAR(30) NULL", + "broker_reconcile_note": "VARCHAR(200) NULL", + } + try: + cols = self.conn.get_columns("orders") + for col, ddl in specs.items(): + if col not in cols: + self.conn.execute( + f"ALTER TABLE orders ADD COLUMN `{col}` {ddl}" + ) + logger.info("📌 orders.%s 컬럼 추가 (reconcile 디버그)", col) + except Exception as e: + logger.warning("orders broker debug 마이그레이션 실패(무시·폴백): %s", e) + # ------------------------------------------------------------------ # orders CRUD # ------------------------------------------------------------------ @@ -360,23 +380,121 @@ class TradeDBExt: code: str, status: str, ord_date: Optional[str] = None, + broker_reconcile_note: Optional[str] = None, ) -> bool: """체결 대기 등 — filled_qty 없이 status 만 갱신 (strategy_id·code·ord_date·is_mock 로 행 한정).""" od = ord_date or datetime.datetime.now().strftime("%Y-%m-%d") mock_flag = self._resolve_is_mock(None) + note = str(broker_reconcile_note or "").strip() or None try: with self.conn: - self.conn.execute( - "UPDATE orders SET status=%s " - "WHERE ord_no=%s AND strategy_id=%s AND code=%s AND ord_date=%s " - "AND is_mock=%s", - (status, ord_no, strategy_id, code, od, mock_flag), - ) + if note: + self.conn.execute( + "UPDATE orders SET status=%s, broker_reconcile_note=%s, " + "broker_reconcile_at=%s " + "WHERE ord_no=%s AND strategy_id=%s AND code=%s AND ord_date=%s " + "AND is_mock=%s", + ( + status, + note[:200], + datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + ord_no, + strategy_id, + code, + od, + mock_flag, + ), + ) + else: + self.conn.execute( + "UPDATE orders SET status=%s " + "WHERE ord_no=%s AND strategy_id=%s AND code=%s AND ord_date=%s " + "AND is_mock=%s", + (status, ord_no, strategy_id, code, od, mock_flag), + ) return True except Exception as e: logger.error("update_order_status 실패 (%s): %s", ord_no, e) return False + def update_order_broker_snapshot( + self, + *, + ord_no: str, + strategy_id: str, + code: str, + open_qty: Optional[int] = None, + open_odno: Optional[str] = None, + note: Optional[str] = None, + ord_date: Optional[str] = None, + ) -> bool: + """cancelable reconcile — 브로커 미체결 스냅샷 기록.""" + od = ord_date or datetime.datetime.now().strftime("%Y-%m-%d") + mock_flag = self._resolve_is_mock(None) + now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + try: + with self.conn: + self.conn.execute( + """ + UPDATE orders + SET broker_open_qty=%s, + broker_open_odno=%s, + broker_reconcile_at=%s, + broker_reconcile_note=%s + WHERE ord_no=%s AND strategy_id=%s AND code=%s + AND ord_date=%s AND is_mock=%s + """, + ( + int(open_qty) if open_qty is not None else None, + str(open_odno or "").strip() or None, + now, + str(note or "").strip()[:200] or None, + ord_no, + strategy_id, + code, + od, + mock_flag, + ), + ) + return True + except Exception as e: + logger.error("update_order_broker_snapshot 실패 (%s): %s", ord_no, e) + return False + + def get_latest_sell_order_for_code( + self, + strategy_id: str, + code: str, + *, + statuses: Optional[tuple] = None, + ) -> Optional[Dict]: + """당일·전략·종목 최신 매도 1건 (reconcile용).""" + today = datetime.datetime.now().strftime("%Y-%m-%d") + mock_flag = self._resolve_is_mock(None) + st = statuses or ("CANCELLED", "PENDING_FILL", "SUBMITTED", "PARTIAL", "FILLED") + placeholders = ",".join(["%s"] * len(st)) + try: + row = self.conn.execute( + f""" + SELECT * FROM orders + WHERE ord_date=%s AND is_mock=%s + AND strategy_id=%s AND code=%s AND side='SELL' + AND status IN ({placeholders}) + ORDER BY submitted_at DESC + LIMIT 1 + """, + (today, mock_flag, strategy_id, code, *st), + ).fetchone() + return dict(row) if row else None + except Exception as e: + logger.error( + "get_latest_sell_order_for_code 실패 (%s/%s): %s", + strategy_id, + code, + e, + ) + return None + def get_pending_fill_orders(self) -> List[Dict]: """ 당일 미확인·부분체결 주문 (체결 qty < 주문 qty). diff --git a/kis_trader/execution/kis_client.py b/kis_trader/execution/kis_client.py index fd377a3..c99ec79 100644 --- a/kis_trader/execution/kis_client.py +++ b/kis_trader/execution/kis_client.py @@ -37,6 +37,42 @@ from ..utils.request_handler import SafeRequest logger = get_logger("kis_trader.kis_client") + +def log_kis_api_response( + tag: str, + j: Optional[dict], + *, + http_status: Optional[int] = None, + tr_id: Optional[str] = None, + extra: Optional[str] = None, + level: str = "warning", +) -> None: + """한투 REST rt_cd/msg_cd/msg1 — 디버그·reconcile용 통일 로그.""" + if not isinstance(j, dict): + j = {} + rt_cd = str(j.get("rt_cd") or "") + msg_cd = str(j.get("msg_cd") or "") + msg1 = str(j.get("msg1") or "") + parts = [ + f"[{tag}]", + f"rt_cd={rt_cd or '-'}", + f"msg_cd={msg_cd or '-'}", + f"msg1={msg1 or '-'}", + ] + if http_status is not None: + parts.append(f"http={http_status}") + if tr_id: + parts.append(f"tr_id={tr_id}") + if extra: + parts.append(str(extra)) + line = " ".join(parts) + if level == "info": + logger.info(line) + elif level == "error": + logger.error(line) + else: + logger.warning(line) + # 토큰 캐시 경로 (프로젝트 루트와 동일 위치 공유) _PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent @@ -197,6 +233,9 @@ class KISClient(SafeRequest): self._last_order_msg1: Optional[str] = None self._last_sell_msg_cd: Optional[str] = None self._last_sell_msg1: Optional[str] = None + self._last_cancel_rt_cd: Optional[str] = None + self._last_cancel_msg_cd: Optional[str] = None + self._last_cancel_msg1: Optional[str] = None self._inquire_price_cache: dict = {} # 같은 요청에서 holdings_map + 계좌요약이 inquire-balance 를 두 번 치지 않게 self._last_inquire_balance: Optional[dict] = None @@ -971,8 +1010,13 @@ class KISClient(SafeRequest): return result def _open_sell_map_from_daily_ccld(self) -> Optional[Dict[str, Dict]]: - """모의·정정취소가능 미지원 시: 당일 주문체결에서 미체결 매도 잔량.""" - j = self.get_order_history_today(odno="") + """모의·정정취소가능 미지원 시: 당일 주문체결에서 미체결 매도 잔량. + + cancelable 전용 페이지 상한: CANCELABLE_CCLD_MAX_PAGES (기본 3). + pending fill 일괄 조회는 get_order_history_today() 기본 DAILY_CCLD_MAX_PAGES 유지. + """ + _ccld_max = max(1, int(get_env_int("CANCELABLE_CCLD_MAX_PAGES", 3))) + j = self.get_order_history_today(odno="", max_pages=_ccld_max) if j is None: return None out1 = j.get("output1") or [] @@ -1164,18 +1208,25 @@ class KISClient(SafeRequest): code=code, qty=qty, price=int(price), order_type="00", side="BUY" ) - def cancel_order( + def cancel_order_detail( self, org_odno: str, *, org_branch: str = "", qty: int = 0, order_dvsn: str = "00", - ) -> bool: - """미체결 주문 전량 취소 (RVSE_CNCL_DVSN_CD=02).""" + ) -> Dict[str, object]: + """미체결 주문 전량 취소 (RVSE_CNCL_DVSN_CD=02). rt_cd/msg_cd/msg1 포함.""" odno = str(org_odno or "").strip() + empty = { + "ok": False, + "rt_cd": "", + "msg_cd": "", + "msg1": "", + "http_status": 0, + } if not odno: - return False + return empty tr_id = "VTTC0803U" if self.mock else "TTTC0803U" path = "/uapi/domestic-stock/v1/trading/order-rvsecncl" body = { @@ -1191,20 +1242,87 @@ class KISClient(SafeRequest): } try: r = self._post(path, tr_id, body) + http_st = int(getattr(r, "status_code", 0) or 0) if r.status_code != 200: - logger.error("주문취소 HTTP 에러 odno=%s status=%s", odno, r.status_code) - return False + log_kis_api_response( + "cancel_order", + None, + http_status=http_st, + tr_id=tr_id, + extra=f"odno={odno}", + ) + self._last_cancel_rt_cd = f"HTTP_{http_st}" + self._last_cancel_msg_cd = "" + self._last_cancel_msg1 = (r.text or "")[:200] + return { + "ok": False, + "rt_cd": self._last_cancel_rt_cd, + "msg_cd": "", + "msg1": self._last_cancel_msg1, + "http_status": http_st, + } j = r.json() - if j.get("rt_cd") == "0": - return True - logger.warning( - "주문취소 실패 odno=%s rt_cd=%s msg=%s", - odno, j.get("rt_cd"), j.get("msg1"), - ) - return False + rt_cd = str(j.get("rt_cd") or "") + msg_cd = str(j.get("msg_cd") or "") + msg1 = str(j.get("msg1") or "") + self._last_cancel_rt_cd = rt_cd + self._last_cancel_msg_cd = msg_cd + self._last_cancel_msg1 = msg1 + ok = rt_cd == "0" + if ok: + log_kis_api_response( + "cancel_order", + j, + http_status=http_st, + tr_id=tr_id, + extra=f"odno={odno} ok", + level="info", + ) + else: + log_kis_api_response( + "cancel_order", + j, + http_status=http_st, + tr_id=tr_id, + extra=f"odno={odno}", + ) + return { + "ok": ok, + "rt_cd": rt_cd, + "msg_cd": msg_cd, + "msg1": msg1, + "http_status": http_st, + } except Exception as e: logger.error("주문취소 예외 odno=%s: %s", odno, e) - return False + self._last_cancel_rt_cd = "EXC" + self._last_cancel_msg_cd = "EXC" + self._last_cancel_msg1 = str(e)[:200] + return { + "ok": False, + "rt_cd": "EXC", + "msg_cd": "EXC", + "msg1": self._last_cancel_msg1, + "http_status": 0, + } + + def cancel_order( + self, + org_odno: str, + *, + org_branch: str = "", + qty: int = 0, + order_dvsn: str = "00", + ) -> bool: + """미체결 주문 전량 취소 (RVSE_CNCL_DVSN_CD=02).""" + return bool( + self.cancel_order_detail( + org_odno, + org_branch=org_branch, + qty=qty, + order_dvsn=order_dvsn, + ).get("ok") + ) def sell_limit_order(self, code: str, qty: int, price: int) -> Optional[str]: """지정가 매도 (ORD_DVSN=00). 익절 시 매수 1호가 등.""" @@ -1615,7 +1733,9 @@ class KISClient(SafeRequest): logger.debug("조건검색 결과 조회 실패 (seq=%s): %s", seq, e) return [] - def get_order_history_today(self, odno: str = "") -> Optional[dict]: + def get_order_history_today( + self, odno: str = "", max_pages: Optional[int] = None + ) -> Optional[dict]: """당일 주문 내역 조회 [국내주식-005 inquire-daily-ccld]. TR_ID: 공식 샘플 기준 3개월이내 = 실전 ``TTTC0081R`` / 모의 ``VTTC0081R``. @@ -1623,6 +1743,7 @@ class KISClient(SafeRequest): ``odno`` 지정 시 해당 주문번호만 조회. 미지정 시 연속조회(모의 15건/페이지)로 output1 을 병합해 반환한다. + ``max_pages`` 미지정 시 DAILY_CCLD_MAX_PAGES(기본 20). """ # 공식 OpenAPI 샘플(inquire_daily_ccld) — env 로만 레거시 TR 오버라이드 default_tr = "VTTC0081R" if self.mock else "TTTC0081R" @@ -1631,14 +1752,15 @@ class KISClient(SafeRequest): ).strip() or default_tr today = dt.now().strftime("%Y%m%d") # 모의 1페이지≈15건 — 장중 체결 누락 방지. 과도 연속조회 방지 상한. - max_pages = max(1, int(get_env_int("DAILY_CCLD_MAX_PAGES", 20))) + _default_max = max(1, int(get_env_int("DAILY_CCLD_MAX_PAGES", 20))) + page_limit = max(1, int(max_pages)) if max_pages is not None else _default_max try: merged_out1: list = [] last_j: Optional[dict] = None fk100 = "" nk100 = "" tr_cont = "" - for _page in range(max_pages): + for _page in range(page_limit): r = self._get( "/uapi/domestic-stock/v1/trading/inquire-daily-ccld", tr_id, @@ -1666,11 +1788,25 @@ class KISClient(SafeRequest): if r is None or r.status_code != 200: if merged_out1 and last_j is not None: break + log_kis_api_response( + "daily_ccld", + None, + http_status=getattr(r, "status_code", None), + tr_id=tr_id, + extra=f"odno={odno or '-'} page={_page + 1}", + ) return None j = r.json() if j.get("rt_cd") != "0": if merged_out1 and last_j is not None: break + log_kis_api_response( + "daily_ccld", + j, + http_status=r.status_code, + tr_id=tr_id, + extra=f"odno={odno or '-'} page={_page + 1}", + ) return None last_j = j chunk = j.get("output1") or [] diff --git a/kis_trader/execution/order_manager.py b/kis_trader/execution/order_manager.py index 285a3c7..6f91528 100644 --- a/kis_trader/execution/order_manager.py +++ b/kis_trader/execution/order_manager.py @@ -17,11 +17,13 @@ kis_trader/execution/order_manager.py — Master Executor * ``REAL_BALANCE_VERIFY_BEFORE_SELL`` (기본 True): 매도 전 실잔고 조회. 보유(`hldg_qty`)와 매도가능(`ord_psbl_qty`)을 분리. 매도가능 0 ≠ 유령. ``orders`` 미체결(pending_sell) 가드와 **역할이 다름** — 중복주문 vs 증권사 진실. - * ``BROKER_HOLDINGS_CACHE_TTL_SEC`` (기본 5): 잔고 캐시 TTL — 매도 **루프** 내 N종목 공유. - 틱매도는 ``prefetch_broker_holdings(force=True)`` 라 TTL 을 안 탐. + * ``BROKER_HOLDINGS_CACHE_TTL_SEC`` (기본 5): 잔고 캐시 TTL — AccountOrderWorker·place 공유. * ``INQUIRE_PSBL_RVSECNCL_BEFORE_GHOST`` (기본 True): 유령정리 전 정정취소가능주문조회. 시장에 남은 매도가 있으면 ghost_purge 금지. - * ``PSBL_RVSECNCL_CACHE_TTL_SEC`` (기본 5): 정정취소가능 조회 캐시 (유량 보호). + * ``PSBL_RVSECNCL_CACHE_TTL_SEC`` (기본 30): 정정취소가능 조회 캐시 (유량 보호). + * ``CANCELABLE_CCLD_MAX_PAGES`` (기본 3): 모의 cancelable daily-ccld 페이지 상한. + * ``SELL_LOCKED_ENQUEUE_COOLDOWN_SEC`` (기본 20): sell_locked 시 전략 enqueue 쿨다운. + * ``CANCELABLE_RECONCILE_ENABLED`` / ``CANCELABLE_RECONCILE_CANCEL_RETRY``: DB↔브로커 정합. * ``GHOST_POSITION_COOLDOWN_SEC`` (기본 300): 유령잔고 정리 후 동일 (전략,종목) 재시도 쿨다운. * ``REAL_BALANCE_VERIFY_BEFORE_BUY`` (기본 False): 매수 전 검증 (기본 OFF — 전략별 복합키가 주 관리). * ``REAL_BALANCE_VERIFY_BEFORE_BUY_MODE`` — ``strategy``(기본): 동일 전략 DB 보유 시만 차단 / @@ -264,12 +266,12 @@ class OrderManager: self._holdings_cache_ts = now return dict(m) - def prefetch_broker_holdings(self) -> bool: + def prefetch_broker_holdings(self, *, force: bool = True) -> bool: """ - 전략 매도 루프 시작 전 1회 호출 (force=True → TTL 무시). - 틱매도 콜백에서도 동일 함수를 써서, 틱마다 잔고 REST 가 날아갈 수 있다. + 매도 place 전 잔고 warm-up (force=True → TTL 무시). + AccountOrderWorker 직렬 환경에서는 force=False 로 연속 SELL TTL 캐시 활용. """ - self.get_broker_holdings(force=True) + self.get_broker_holdings(force=force) return self._holdings_last_fetch_ok def invalidate_holdings_cache(self) -> None: @@ -287,7 +289,7 @@ class OrderManager: if not get_env_bool("INQUIRE_PSBL_RVSECNCL_BEFORE_GHOST", True): return {} now = time.time() - ttl = float(get_env_int("PSBL_RVSECNCL_CACHE_TTL_SEC", 5)) + ttl = float(get_env_int("PSBL_RVSECNCL_CACHE_TTL_SEC", 30)) with self._holdings_lock: if ( not force @@ -336,6 +338,154 @@ class OrderManager: return "cancelable_open" return "" + def _reconcile_cancelable_open( + self, + req: OrderRequest, + cmap: Optional[Dict[str, Dict]], + ) -> str: + """ + DB CANCELLED vs 브로커 cancelable open 불일치 reconcile. + 반환 action: disabled, noop, snapshot_only, cancel_ok, reverted_pending, filled. + """ + if not get_env_bool("CANCELABLE_RECONCILE_ENABLED", True): + return "disabled" + code = str(req.code or "").strip() + if not code: + return "noop" + open_qty = cancelable_remainder_qty(cmap, code) + if open_qty <= 0: + return "noop" + row = (cmap or {}).get(code) or {} + open_odno = str(row.get("odno") or "").strip() + db_row = self.db.get_latest_sell_order_for_code(req.strategy_id, code) + db_status = str((db_row or {}).get("status") or "") + db_ord_no = str((db_row or {}).get("ord_no") or "").strip() + snap_odno = open_odno or db_ord_no + if db_row and snap_odno: + try: + self.db.update_order_broker_snapshot( + ord_no=snap_odno, + strategy_id=req.strategy_id, + code=code, + open_qty=open_qty, + open_odno=open_odno or None, + note="cancelable_open", + ) + except Exception as ex: + logger.debug("broker snapshot 저장 실패 %s: %s", code, ex) + + if open_odno: + try: + fill = self.client.get_execution_by_odno( + open_odno, code=code, wait_sec=0.0, + ) + except Exception: + fill = None + if fill and int(fill.get("filled_qty") or 0) > 0: + if db_row and db_ord_no: + try: + self.db.update_order_broker_snapshot( + ord_no=db_ord_no, + strategy_id=req.strategy_id, + code=code, + open_qty=open_qty, + open_odno=open_odno, + note="filled_on_reconcile", + ) + except Exception: + pass + logger.warning( + "[cancelable_reconcile] %s action=filled open_qty=%d odno=%s " + "db_status=%s db_ord_no=%s", + code, open_qty, open_odno, db_status, db_ord_no or "-", + ) + return "filled" + + cancel_detail: Dict[str, object] = {} + if ( + open_odno + and open_qty > 0 + and get_env_bool("CANCELABLE_RECONCILE_CANCEL_RETRY", True) + ): + try: + cancel_detail = self.client.cancel_order_detail( + open_odno, qty=int(open_qty), + ) + except Exception as ex: + cancel_detail = { + "ok": False, + "rt_cd": "EXC", + "msg_cd": "EXC", + "msg1": str(ex)[:200], + "http_status": 0, + } + if cancel_detail.get("ok"): + note = "cancel_ok" + if db_row and snap_odno: + try: + self.db.update_order_broker_snapshot( + ord_no=snap_odno, + strategy_id=req.strategy_id, + code=code, + open_qty=0, + open_odno=open_odno, + note=note, + ) + except Exception: + pass + logger.warning( + "[cancelable_reconcile] %s action=%s open_qty=%d odno=%s " + "db_status=%s rt_cd=%s msg_cd=%s msg1=%s", + code, note, open_qty, open_odno, db_status, + cancel_detail.get("rt_cd", "-"), + cancel_detail.get("msg_cd", "-"), + cancel_detail.get("msg1", "-"), + ) + self.invalidate_holdings_cache() + return "cancel_ok" + if db_row and db_status == "CANCELLED" and snap_odno: + try: + self.db.update_order_status( + ord_no=snap_odno, + strategy_id=req.strategy_id, + code=code, + status="PENDING_FILL", + broker_reconcile_note="reverted_pending", + ) + self.db.update_order_broker_snapshot( + ord_no=snap_odno, + strategy_id=req.strategy_id, + code=code, + open_qty=open_qty, + open_odno=open_odno or None, + note="reverted_pending", + ) + except Exception as ex: + logger.warning( + "[cancelable_reconcile] %s revert 실패: %s", code, ex, + ) + return "noop" + logger.warning( + "[cancelable_reconcile] %s action=reverted_pending open_qty=%d " + "odno=%s db_status=%s rt_cd=%s msg_cd=%s msg1=%s", + code, open_qty, open_odno or db_ord_no, db_status, + cancel_detail.get("rt_cd", "-"), + cancel_detail.get("msg_cd", "-"), + cancel_detail.get("msg1", "-"), + ) + self.invalidate_holdings_cache() + return "reverted_pending" + + logger.warning( + "[cancelable_reconcile] %s action=snapshot_only open_qty=%d odno=%s " + "db_status=%s db_ord_no=%s rt_cd=%s msg_cd=%s msg1=%s", + code, open_qty, open_odno or "-", db_status, db_ord_no or "-", + cancel_detail.get("rt_cd", "-") if cancel_detail else "-", + cancel_detail.get("msg_cd", "-") if cancel_detail else "-", + cancel_detail.get("msg1", "-") if cancel_detail else "-", + ) + return "snapshot_only" + def _resolve_order_display_name(self, req: OrderRequest) -> str: """MM·DB·로그용 종목명 — code=이름이면 DB/잔고에서 보완.""" fb = str(req.name or req.code or "").strip() @@ -1202,6 +1352,7 @@ class OrderManager: # 만료 — 미체결 또는 부분체결 잔량 정리 remain = max(0, order_qty - prev_filled) + cancel_ok = True if remain > 0: # 매도 IOC면 잔량 주문은 이미 브로커 취소 — cancel REST 생략 skip_cancel = ( @@ -1210,10 +1361,42 @@ class OrderManager: ) if not skip_cancel: try: - self.client.cancel_order(ord_no, qty=remain) + cancel_detail = self.client.cancel_order_detail( + ord_no, qty=remain, + ) + cancel_ok = bool(cancel_detail.get("ok")) + if not cancel_ok: + logger.warning( + "%s⏱ [만료취소실패] %s %s ODNO=%s remain=%d " + "rt_cd=%s msg_cd=%s msg1=%s — CANCELLED 금지%s", + LOG_YELLOW, req.name, req.code, ord_no, remain, + cancel_detail.get("rt_cd", "-"), + cancel_detail.get("msg_cd", "-"), + cancel_detail.get("msg1", "-"), + LOG_RESET, + ) + try: + self.db.update_order_broker_snapshot( + ord_no=ord_no, + strategy_id=req.strategy_id, + code=req.code, + open_qty=remain, + open_odno=ord_no, + note="cancel_failed_at_poll", + ) + except Exception: + pass except Exception as e: - logger.debug("만료 주문 취소 실패 ord_no=%s: %s", ord_no, e) + cancel_ok = False + logger.warning( + "%s⏱ [만료취소예외] %s %s ODNO=%s remain=%d: %s " + "— CANCELLED 금지%s", + LOG_YELLOW, req.name, req.code, ord_no, remain, + e, LOG_RESET, + ) if prev_filled <= 0: + if not cancel_ok and remain > 0: + continue self.db.update_order_status( ord_no=ord_no, strategy_id=req.strategy_id, code=req.code, status="CANCELLED", @@ -2394,12 +2577,67 @@ class OrderManager: ) if not broker_row_still_held(real_row): if block: - logger.warning( - "%s⏸ [매도보류] [%s] %s %s — 잔고맵 0이지만 %s " - "(시장 미체결/API실패) → 유령정리 안 함%s", - LOG_YELLOW, req.strategy_id, req.name, req.code, - block, LOG_RESET, - ) + cmap = None + open_qty = 0 + open_odno = "" + db_status = "" + db_ord_no = "" + rt_cd = msg_cd = msg1 = "-" + if block == "cancelable_open": + cmap = self.get_cancelable_sells(force=False) + open_qty = cancelable_remainder_qty(cmap, req.code) + row_c = (cmap or {}).get(req.code) or {} + open_odno = str(row_c.get("odno") or "") + db_row = self.db.get_latest_sell_order_for_code( + req.strategy_id, req.code, + ) + db_status = str((db_row or {}).get("status") or "") + db_ord_no = str((db_row or {}).get("ord_no") or "") + reconcile_action = self._reconcile_cancelable_open( + req, cmap, + ) + rt_cd = str( + getattr(self.client, "_last_cancel_rt_cd", None) or "-" + ) + msg_cd = str( + getattr(self.client, "_last_cancel_msg_cd", None) or "-" + ) + msg1 = str( + getattr(self.client, "_last_cancel_msg1", None) or "-" + ) + if reconcile_action == "reverted_pending": + pend_no = db_ord_no or open_odno + logger.warning( + "%s⏸ [매도대기중] [%s] %s %s — reconcile " + "PENDING 복구 ODNO=%s%s", + LOG_YELLOW, req.strategy_id, req.name, + req.code, pend_no, LOG_RESET, + ) + return OrderResult( + False, + ord_no=pend_no or None, + reason="sell_order_pending", + request=req, + ) + if reconcile_action == "cancel_ok": + block = self._ghost_purge_block_reason( + req.code, req.strategy_id, real_row, + ) + if not block: + return self._purge_ghost_position( + req, "broker_zero", + ) + if block: + logger.warning( + "%s⏸ [매도보류] [%s] %s %s — 잔고맵 0이지만 %s " + "(시장 미체결/API실패) → 유령정리 안 함 " + "broker_open_qty=%d odno=%s db_last_status=%s db_ord_no=%s " + "rt_cd=%s msg_cd=%s msg1=%s%s", + LOG_YELLOW, req.strategy_id, req.name, req.code, + block, open_qty, open_odno or "-", + db_status or "-", db_ord_no or "-", + rt_cd, msg_cd, msg1, LOG_RESET, + ) return OrderResult( False, reason="sell_locked:%s" % block, diff --git a/kis_trader/execution/order_worker.py b/kis_trader/execution/order_worker.py index 03a575c..27f7a34 100644 --- a/kis_trader/execution/order_worker.py +++ b/kis_trader/execution/order_worker.py @@ -144,7 +144,7 @@ class AccountOrderWorker: return if side == "SELL" and get_env_bool("REAL_BALANCE_VERIFY_BEFORE_SELL", True): try: - self.order_mgr.prefetch_broker_holdings() + self.order_mgr.prefetch_broker_holdings(force=False) except Exception: pass if side == "BUY": diff --git a/kis_trader/strategies/base.py b/kis_trader/strategies/base.py index 034bc9e..798a4c7 100644 --- a/kis_trader/strategies/base.py +++ b/kis_trader/strategies/base.py @@ -207,6 +207,8 @@ class BaseStrategy(ABC, threading.Thread): # 종목당 매도/매수 intent 1장 — enqueue 중복 방지 (Worker 완료 시 해제) self._sell_inflight: set = set() self._buy_inflight: set = set() + # sell_locked/sellable_zero/pending 시 tick enqueue 쿨다운 (SELL_LOCKED_ENQUEUE_COOLDOWN_SEC) + self._sell_locked_until: Dict[str, float] = {} self._order_enqueue_skip = 0 # 루프 숙제별 ms 계측 (LOOP_PROFILE_ENABLED) self._loop_prof_i = 0 @@ -282,6 +284,18 @@ class BaseStrategy(ABC, threading.Thread): code = str((sig or {}).get("code") or "").strip() if not code: return False + cooldown_sec = max( + 0, int(get_env_int("SELL_LOCKED_ENQUEUE_COOLDOWN_SEC", 20) or 0), + ) + if cooldown_sec > 0: + until = self._sell_locked_until.get(code, 0.0) + if time.time() < until: + remain = int(until - time.time()) + self.logger.debug( + "⏸ [매도enqueue쿨다운] %s — %d초 남음 (source=%s)", + code, remain, source, + ) + return False if not self._mark_sell_inflight(code): return False ow = getattr(self, "order_worker", None) @@ -1428,6 +1442,15 @@ class BaseStrategy(ABC, threading.Thread): profit_pct=float(signal.get("profit_pct", 0)), ) result = self.order_mgr.place(req) + reason = str(result.reason or "") + cooldown_sec = max( + 0, int(get_env_int("SELL_LOCKED_ENQUEUE_COOLDOWN_SEC", 20) or 0), + ) + if cooldown_sec > 0 and ( + reason.startswith("sell_locked:") + or reason in ("sellable_zero", "sell_order_pending") + ): + self._sell_locked_until[req.code] = time.time() + float(cooldown_sec) if result.success: self.recently_sold[req.code] = time.time() self._drop_local_position(req.code) diff --git a/kis_trader/web/live_config_schema.py b/kis_trader/web/live_config_schema.py index cf8eebb..30bb3fc 100644 --- a/kis_trader/web/live_config_schema.py +++ b/kis_trader/web/live_config_schema.py @@ -1069,8 +1069,39 @@ def build_live_config_groups() -> List[GroupDef]: "PSBL_RVSECNCL_CACHE_TTL_SEC", "📋 정정취소가능 조회 캐시(초)", "int", - default=5, - hint="같은 초 안에 종목마다 REST 연타 방지. 잔고 캐시 TTL 과 비슷하게.", + default=30, + hint="같은 초 안에 종목마다 REST 연타 방지. sell_locked retry 시 캐시 hit.", + ), + _f( + "CANCELABLE_CCLD_MAX_PAGES", + "📋 모의 cancelable daily-ccld 최대 페이지", + "int", + default=3, + hint="모의투자 cancelable 조회 시 daily-ccld 페이지 상한. 기본 3 (유량·지연 보호).", + ), + _f( + "SELL_LOCKED_ENQUEUE_COOLDOWN_SEC", + "⏸ sell_locked enqueue 쿨다운(초)", + "int", + default=20, + hint=( + "sell_locked/sellable_zero/pending 시 tick·scan 재 enqueue 대기. " + "REST 폭주 방지 (SELL_FAILURE_BACKOFF 와 별개)." + ), + ), + _f( + "CANCELABLE_RECONCILE_ENABLED", + "🔄 cancelable_open DB↔브로커 reconcile", + "bool", + default=True, + hint="ON = DB CANCELLED vs 브로커 미체결 open 불일치 시 자동 조사·복구 시도.", + ), + _f( + "CANCELABLE_RECONCILE_CANCEL_RETRY", + "🔄 reconcile 시 cancel_order 재시도", + "bool", + default=True, + hint="reconcile 중 브로커 미체결 ODNO 에 cancel_order 1회 재시도.", ), _f( "PSBL_RVSECNCL_MAX_PAGES", diff --git a/scripts/adhoc_cancelable_reconcile_smoke.py b/scripts/adhoc_cancelable_reconcile_smoke.py new file mode 100644 index 0000000..ac016fe --- /dev/null +++ b/scripts/adhoc_cancelable_reconcile_smoke.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +""" +scripts/adhoc_cancelable_reconcile_smoke.py +=========================================== +Worker SELL C+D — 주말 구조 스모크 (REST 주문·cancel 호출 없음). + +검증: + · orders broker debug 컬럼 migrate + · update_order_broker_snapshot / get_latest_sell_order_for_code 헬퍼 + · OrderManager._reconcile_cancelable_open disabled 분기 (noop) + · poll 만료 cancel 실패 시 CANCELLED 금지 소스 앵커 + +실행: + python3 -u scripts/adhoc_cancelable_reconcile_smoke.py +""" +from __future__ import annotations + +import inspect +import sys +from pathlib import Path +from unittest.mock import MagicMock, patch + +ROOT = Path(__file__).resolve().parents[1] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + +REQUIRED_ORDER_COLS = ( + "broker_open_qty", + "broker_open_odno", + "broker_reconcile_at", + "broker_reconcile_note", +) + + +def main() -> int: + ok = True + print("=== adhoc_cancelable_reconcile_smoke ===") + + from kis_trader.database.db_manager import get_db + from kis_trader.execution.order_manager import OrderManager, OrderRequest + + db = get_db() + db._ensure_orders_table() + cols = [r["Field"] for r in db.conn.execute( + "SHOW COLUMNS FROM orders", + ).fetchall()] + missing = [c for c in REQUIRED_ORDER_COLS if c not in cols] + if missing: + print(f"FAIL orders 컬럼 누락: {missing}") + ok = False + else: + print(f"OK orders broker debug 컬럼 {len(REQUIRED_ORDER_COLS)}개 확인") + + snap_ok = db.update_order_broker_snapshot( + ord_no="__smoke__", + strategy_id="SMOKE", + code="000000", + open_qty=0, + open_odno=None, + note="adhoc_smoke", + ) + if snap_ok: + print("OK update_order_broker_snapshot (no-row UPDATE 허용)") + else: + print("WARN update_order_broker_snapshot False (no matching row — migrate OK)") + + mock_client = MagicMock() + mock_db = MagicMock() + mock_db.get_latest_sell_order_for_code.return_value = None + om = OrderManager(client=mock_client, db=mock_db) + + req = OrderRequest( + strategy_id="SMOKE", + code="232680", + name="232680", + side="SELL", + qty=1, + price_ref=0.0, + reason="smoke", + ) + + def _env_bool(key, default=True): + if key == "CANCELABLE_RECONCILE_ENABLED": + return False + if key == "CANCELABLE_RECONCILE_CANCEL_RETRY": + return False + return default + + with patch( + "kis_trader.execution.order_manager.get_env_bool", + side_effect=_env_bool, + ): + action = om._reconcile_cancelable_open( + req, {"232680": {"psbl_qty": 10, "odno": "X"}}, + ) + if action != "disabled": + print(f"FAIL reconcile disabled 기대=disabled 실제={action}") + ok = False + else: + print("OK _reconcile_cancelable_open disabled → noop 경로") + + from kis_trader.execution import order_manager as om_mod + src = inspect.getsource(om_mod.OrderManager.poll_pending_fills) + if "cancel_failed_at_poll" in src and "CANCELLED 금지" in src: + print("OK poll_pending_fills cancel 실패 → CANCELLED 금지 앵커") + else: + print("FAIL poll_pending_fills cancel-fail 가드 소스 누락") + ok = False + + from kis_trader.strategies import base as base_mod + base_src = inspect.getsource(base_mod.BaseStrategy._enqueue_sell) + if "SELL_LOCKED_ENQUEUE_COOLDOWN_SEC" in base_src: + print("OK base._enqueue_sell sell_locked cooldown 앵커") + else: + print("FAIL base enqueue cooldown 소스 누락") + ok = False + + ow_src = Path(ROOT / "kis_trader/execution/order_worker.py").read_text(encoding="utf-8") + if "prefetch_broker_holdings(force=False)" in ow_src: + print("OK order_worker prefetch force=False") + else: + print("FAIL order_worker prefetch force=False 누락") + ok = False + + print("=== 최종:", "통과" if ok else "실패", "===") + return 0 if ok else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_optuna_4strat_tpe_seq.sh b/scripts/run_optuna_4strat_tpe_seq.sh index 63e8bb1..7b76b46 100755 --- a/scripts/run_optuna_4strat_tpe_seq.sh +++ b/scripts/run_optuna_4strat_tpe_seq.sh @@ -38,6 +38,9 @@ BREAKOUT_OPTUNA_SL_MODES="${BREAKOUT_OPTUNA_SL_MODES:-fixed}" BREAKOUT_OPTUNA_OB_MODES="${BREAKOUT_OPTUNA_OB_MODES:-off}" # kiwoom|ls — 웹 Optuna 이력소스 / CLI UNIVERSE_HISTORY_SOURCE UNIVERSE_HISTORY_SOURCE="${UNIVERSE_HISTORY_SOURCE:-${BACKTEST_UNIVERSE_HISTORY_SOURCE:-kiwoom}}" +# 웹 순차 잡 ID (전략/조합별 1·2차 state 파일 prefix) +OPTUNA_SEQ_JOB_ID="${OPTUNA_SEQ_JOB_ID:-seq}" +SORT_BY="${SORT_BY:-score}" PY="${PY:-.venv/bin/python}" TS0="$(date +%Y%m%d_%H%M%S)" MASTER="logs/optuna_4strat_tpe_${START}_${END}_${TS0}_master.log" @@ -51,7 +54,7 @@ MASTER="logs/optuna_4strat_tpe_${START}_${END}_${TS0}_master.log" echo "BREAKOUT_OPTUNA_OB_MODES=$BREAKOUT_OPTUNA_OB_MODES" echo "UNIVERSE_HISTORY_SOURCE=$UNIVERSE_HISTORY_SOURCE" echo "min_wr=$MIN_WIN_RATE min_pf=$MIN_PF min_trades=$MIN_TRADES" - echo "apply-best=OFF breakout-orderbook=스위치(스터디별) n_jobs=1 (사후 results_gated + briefing.md)" + echo "apply-best=OFF · 전략/조합마다 1·2차 TPE 연쇄 · n_jobs=1 (사후 results_gated + briefing.md)" echo "master_log=$MASTER" free -h | sed -n '1,2p' df -h / | tail -1 @@ -63,97 +66,106 @@ run_one() { local entry_mode="${2:-}" local sl_mode="${3:-}" local ob_mode="${4:-off}" - local ts study log sort_by bo_extra + local ts step_job_id refine_log state_file sort_by bo_extra extra min_trades_arg ts="$(date +%Y%m%d_%H%M%S)" - study="${strat}_tpe_${START//-/}_${END//-/}_${ts}" - log="logs/optuna_${strat}_tpe_${ts}.log" - if [[ "$strat" == "tail" && -n "$entry_mode" ]]; then - study="${strat}_${entry_mode}_tpe_${START//-/}_${END//-/}_${ts}" - log="logs/optuna_${strat}_${entry_mode}_tpe_${ts}.log" - fi - if [[ "$strat" == "breakout" && -n "$sl_mode" ]]; then - bo_extra="${sl_mode}_ob_${ob_mode}" - study="${strat}_${bo_extra}_tpe_${START//-/}_${END//-/}_${ts}" - log="logs/optuna_${strat}_${bo_extra}_tpe_${ts}.log" - fi sort_by="${SORT_BY:-score}" case "$sort_by" in score|score_legacy|pnl|daily_avg|win_rate) ;; *) sort_by="score" ;; esac - local min_trades_arg + if [[ "$strat" == "breakout" && -n "$sl_mode" ]]; then + bo_extra="${sl_mode}_ob_${ob_mode}" + extra="$bo_extra" + elif [[ "$strat" == "tail" && -n "$entry_mode" ]]; then + extra="$entry_mode" + else + extra="" + fi + + step_job_id="${OPTUNA_SEQ_JOB_ID}_${strat}${extra:+_${extra}}_${ts}" + refine_log="logs/optuna_seq_refine_${step_job_id}.log" + state_file="logs/${step_job_id}_refine_state.json" + min_trades_arg=$("$PY" -c "from kis_trader.backtest.optuna_common import resolve_optuna_min_trades; print(resolve_optuna_min_trades('${START}', '${END}', '${strat}')['min_trades'])") { echo "" - echo "-------- [$strat${entry_mode:+/$entry_mode}${sl_mode:+/$sl_mode}${bo_extra:+/$bo_extra}] START $(date -Is) study=$study univ=$UNIVERSE_HISTORY_SOURCE min_trades=$min_trades_arg --------" + echo "-------- [$strat${entry_mode:+/$entry_mode}${sl_mode:+/$sl_mode}${bo_extra:+/$bo_extra}] START $(date -Is) 1·2차 step=$step_job_id --------" } | tee -a "$MASTER" - echo "$log" > "logs/optuna_${strat}_tpe_latest.logpath" - echo "$study" > "logs/optuna_${strat}_tpe_latest.study" - # 웹 진행률: 전역 latest.study 가 이전 전략에 남으면 바가 1번에서 멈춤 → 잡별 파일 + if [[ -n "${OPTUNA_SEQ_ACTIVE_FILE:-}" ]]; then - if [[ "$strat" == "breakout" && -n "$bo_extra" ]]; then - extra="$bo_extra" - else - extra="${entry_mode:-${sl_mode:-}}" - fi { echo "strategy=$strat" - echo "study=$study" echo "entry_mode=${entry_mode:-}" echo "sl_mode=${sl_mode:-}" echo "ob_mode=${ob_mode:-}" echo "extra=${extra}" + echo "refine_state_path=$state_file" } > "$OPTUNA_SEQ_ACTIVE_FILE" fi set +e - set +e - local cmd_args=( + local refine_args=( + --job-id "$step_job_id" --strategy "$strat" --mode "$MODE" --start "$START" --end "$END" --trials "$TRIALS" - --min_trades "$min_trades_arg" - --min_win_rate "$MIN_WIN_RATE" - --min_pf "$MIN_PF" - --orderbook-filter "$([[ "$strat" == "breakout" ]] && echo "$ob_mode" || echo off)" - --no-progress - --study-name "$study" --sort-by "$sort_by" + --min-trades "$min_trades_arg" --universe-history-source "$UNIVERSE_HISTORY_SOURCE" ) if [[ -n "${STUDY_TRIALS:-}" && "${STUDY_TRIALS}" != "0" ]]; then - cmd_args+=(--study-trials "$STUDY_TRIALS") + refine_args+=(--study-trials "$STUDY_TRIALS") fi if [[ "$strat" == "tail" && -n "$entry_mode" ]]; then - cmd_args+=(--entry-mode "$entry_mode") + refine_args+=(--entry-mode "$entry_mode") fi if [[ "$strat" == "breakout" && -n "$sl_mode" ]]; then - cmd_args+=(--sl-mode "$sl_mode") + refine_args+=(--sl-mode "$sl_mode" --ob-mode "$ob_mode") + fi + if [[ -n "${CANDLE_SOURCE:-}" ]]; then + refine_args+=(--candle-source "$CANDLE_SOURCE") fi - if [[ -n "${TICK_SOURCE:-}" ]]; then - cmd_args+=("--tick-source" "$TICK_SOURCE") + refine_args+=(--tick-source "$TICK_SOURCE") fi if [[ -n "${OB_SOURCE:-}" ]]; then - cmd_args+=("--ob-source" "$OB_SOURCE") + refine_args+=(--ob-source "$OB_SOURCE") fi - "$PY" -u kis_trader/backtest/param_search_optuna.py "${cmd_args[@]}" >"$log" 2>&1 + "$PY" -u kis_trader/backtest/optuna_mode_refine_runner.py "${refine_args[@]}" >"$refine_log" 2>&1 local rc=$? set -e + local result_json briefing_md + result_json="" + briefing_md="" + if [[ -f "$state_file" ]]; then + result_json=$("$PY" -c "import json; print(json.load(open('${state_file}')).get('result_json') or '')" 2>/dev/null || true) + if [[ -n "$result_json" && -f "$result_json" ]]; then + echo "OPTUNA_RESULT_JSON=$result_json" | tee -a "$MASTER" + briefing_md="${result_json%.json}.briefing.md" + if [[ -f "$briefing_md" ]]; then + echo "OPTUNA_BRIEFING_MD=$briefing_md" | tee -a "$MASTER" + fi + fi + fi + if [[ -z "$result_json" ]]; then + grep -E 'OPTUNA_RESULT_JSON=' "$refine_log" | tail -n 1 | tee -a "$MASTER" || true + fi + { echo "-------- [$strat${entry_mode:+/$entry_mode}${sl_mode:+/$sl_mode}${bo_extra:+/$bo_extra}] END rc=$rc $(date -Is) --------" - echo "LOG=$log" - grep -E 'OPTUNA_RESULT_JSON=|OPTUNA_BRIEFING_MD=|Best trial|optuna_best|❌|KeyError|Traceback' "$log" | tail -n 24 || true + echo "REFINE_LOG=$refine_log" + echo "STATE=$state_file" + grep -E 'OPTUNA_RESULT_JSON=|OPTUNA_BRIEFING_MD=|✅ 1·2차|❌ 1·2차|phase1|phase2|Traceback' "$refine_log" | tail -n 24 || true } | tee -a "$MASTER" if [[ "$rc" -ne 0 ]]; then - echo "⚠️ [$strat] 실패 rc=$rc — 다음 전략 계속" | tee -a "$MASTER" + echo "⚠️ [$strat] 1·2차 실패 rc=$rc — 다음 전략 계속" | tee -a "$MASTER" fi return 0 } diff --git a/static/css/backtest.css b/static/css/backtest.css index 62941a1..16632d0 100644 --- a/static/css/backtest.css +++ b/static/css/backtest.css @@ -382,6 +382,39 @@ overflow: auto; } + .opt-period-banner { + border: 1px solid var(--border); + font-size: 12px; + } + .opt-period-banner.opt-period-ok { + background: rgba(63, 185, 80, 0.08); + border-color: rgba(63, 185, 80, 0.35); + } + .opt-period-banner.opt-period-warn { + background: rgba(210, 153, 34, 0.12); + border-color: rgba(210, 153, 34, 0.55); + } + .opt-period-badge { + display: inline-block; + padding: 2px 8px; + border-radius: 4px; + border: 1px solid var(--border); + background: rgba(255, 255, 255, 0.04); + font-size: 11px; + white-space: nowrap; + } + .opt-period-badge b { font-weight: 600; color: var(--text); } + .opt-period-badge-form { border-color: #58a6ff; } + .opt-period-badge-master { border-color: #8b949e; } + .opt-period-badge-p1 { border-color: #3fb950; } + .opt-period-badge-p2 { border-color: #d29922; } + .opt-period-badge-active { border-color: #bc8cff; } + .opt-period-badge-warn { + background: rgba(248, 81, 73, 0.12); + border-color: rgba(248, 81, 73, 0.55); + } + .opt-period-cell-warn { color: #d29922; } + /* 오늘 운영 — 조건검색 이력(키움/LS) */ .dash-univ-table .dash-univ-src-kiwoom { color: #58a6ff; font-weight: 600; } .dash-univ-table .dash-univ-src-kis { color: #3fb950; font-weight: 600; } diff --git a/static/js/backtest.js b/static/js/backtest.js index 4f335d2..371b96d 100644 --- a/static/js/backtest.js +++ b/static/js/backtest.js @@ -8339,6 +8339,7 @@ function optunaSetNav(job) { const postBusy = phase === 'postprocess' || (st === 'running' && pct >= 99.9 && !post.ready && post.expect_ob !== false); const finalizeBusy = phase === 'finalize'; const label = (job.label || job.strategy || 'Optuna') + + (prog.refine_phase === 'phase1' ? ' · 1차' : (prog.refine_phase === 'phase2' ? ' · 2차' : '')) + (postBusy ? ' 후처리' : (finalizeBusy ? ' 저장중' : (st === 'running' ? ' 실행중' : (st === 'done' ? ' 완료' : (st === 'error' ? ' 오류' : ''))))); const done = prog.trials_done != null ? prog.trials_done : '?'; const tot = prog.trials_total != null ? prog.trials_total : '?'; @@ -8421,12 +8422,16 @@ function optunaRenderCompare(sum) { } } optunaRenderModeCombo(sum); + optunaRenderModeTop10(sum); optunaRenderTrailRec(sum); optunaRenderPostprocess(sum); optunaRenderOverfitBadges(sum); optunaRenderOverfit(sum); + optunaRenderParamDistChart(sum); } +const OPTUNA_CARD_TOP_N = 5; + function optunaRenderModeCombo(sum) { const el = $('opt_mode_combo_body'); const m = sum && sum.mode_combo_summary; @@ -8456,17 +8461,106 @@ function optunaRenderModeCombo(sum) { el.innerHTML = `
` + `Top${m.top_n != null ? m.top_n : 'N'} 축최빈 조립` + - (m.pool_size != null ? ` · pool ${m.pool_size}` : '') + + (m.pool_kind ? ` · pool=${m.pool_kind}` : '') + + (m.pool_size != null ? ` · ${m.pool_size}건` : '') + (vsLine ? ` · ${vsLine}` : '') + ` · 아래 표=사후합격과 같은 열(안정·과적합 포함). trial 번호는 조립이라 없음.` + `
` + (m.note ? `
${m.note}
` : '') + `
` + - `` + - ` ` + + `` + `
`; } +function optunaRenderModeTop10(sum) { + const metaEl = $('opt_mode_top_meta'); + const meta = (sum && sum.mode_consensus_meta) || {}; + if (metaEl) { + if (!sum) { + metaEl.textContent = '—'; + } else if (meta.mode_pool_size != null || meta.note) { + metaEl.innerHTML = + `pool ${meta.mode_pool_kind || 'positive'}` + + (meta.mode_pool_size != null ? ` · ${meta.mode_pool_size}건` : '') + + (meta.mode_params_keys != null ? ` · 축 ${meta.mode_params_keys}개` : '') + + (meta.note ? ` · ${meta.note}` : '') + + (meta.scoring ? ` · ${meta.scoring}` : ''); + } else { + metaEl.textContent = 'mode Top10 — 완료 후 표시'; + } + } + optunaFillModeTopTbody( + 'opt_top5_mode_top_tbody', + (sum && sum.top5_consensus) || [], + 'consensus', + sum ? 'mode Top10 없음 (pool 0 · grid/results 확인)' : '완료 후 표시', + ); + optunaRenderModeBandTop3(sum, window._optunaLastJob || null); +} + +function optunaDailyAvgPnlCell(r) { + const v = (r.period_daily_avg_pnl != null) ? r.period_daily_avg_pnl : r.daily_pnl_mean; + if (v == null) return '—'; + const n = Number(v); + const cls = n >= 0 ? 'text-pnl-pos' : 'text-pnl-neg'; + return `${optunaFmtNum(v)}`; +} + +function optunaDailyAvgPctCell(r) { + let p = r.period_daily_avg_pct; + if (p == null && r.daily_avg_pct != null) p = r.daily_avg_pct; + if (p == null && r.bot_pct != null && r.n_period_trading_days > 0) { + p = Number(r.bot_pct) / Number(r.n_period_trading_days); + } + if (p == null) return '—'; + const n = Number(p); + const cls = n >= 0 ? 'text-pnl-pos' : 'text-pnl-neg'; + const sign = n > 0 ? '+' : ''; + return `${sign}${optunaFmtNum(n, 3)}%`; +} + +function optunaModeTopTableRow(r, source) { + const rank = r.rank || 1; + const src = source; + const applyCls = 'btn-outline-success'; + const cm = r.consensus_match_pct; + const cmN = r.consensus_match_n; + const cmOf = r.consensus_match_of; + const cmCell = (cm != null && cmOf != null) + ? `${optunaFmtNum(cm, 1)}%
${cmN}/${cmOf}축 밴드
` + : '—'; + return ` + ${rank} + #${r.optuna_trial_number ?? '—'} + ${cmCell} + ${optunaFmtNum(r.total_trades, 0)} + ${optunaFmtNum(r.win_rate, 1)}% + ${optunaFmtNum(r.pf, 2)} + ${optunaFmtNum(r.total_pnl)} + ${optunaDailyAvgPnlCell(r)} + ${optunaDailyAvgPctCell(r)} + ${optunaObWhipCell(r)} + ${r.n_losing_days != null ? optunaFmtNum(r.n_losing_days, 0) : '—'} + ${r.worst_day_pnl != null ? optunaFmtNum(r.worst_day_pnl) : '—'} + ${optunaStableScoreCell(r)} + ${optunaOverfitCell(r)} + + + + + `; +} + +function optunaFillModeTopTbody(tbodyId, rows, source, emptyMsg) { + const tb = $(tbodyId); + if (!tb) return; + if (!rows || !rows.length) { + tb.innerHTML = `${emptyMsg}`; + return; + } + tb.innerHTML = rows.map((r) => optunaModeTopTableRow(r, source)).join(''); +} + function optunaRenderOverfit(sum) { const scoreEl = $('opt_overfit_score'); const facTb = $('opt_overfit_factors_tbody'); @@ -8535,6 +8629,184 @@ function optunaRenderOverfit(sum) { if (noteEl) noteEl.textContent = d.note || '—'; } +function optunaRefinePhaseLabel(job) { + const sy = String((job && job.study_name) || ''); + const sh = String((job && job.study_short) || ''); + if (sy.includes('refine2') || sh === 'refine2') return '2차 TPE'; + if (sy.includes('refine1') || sh === 'refine1') return '1차 TPE'; + const ph = (job && job.progress && job.progress.refine_phase) || ''; + if (ph === 'phase2') return '2차 TPE'; + if (ph === 'phase1') return '1차 TPE'; + return ''; +} + +function optunaFormatGatedTopRowHtml(t, i, showBorder) { + const dAvg = (t.period_daily_avg_pnl != null) ? t.period_daily_avg_pnl : t.daily_pnl_mean; + const dPct = (t.period_daily_avg_pct != null) ? t.period_daily_avg_pct : t.daily_avg_pct; + const rank = t.rank || i; + const bdr = showBorder !== false ? ' border-bottom border-secondary' : ''; + return `
+ #${i} trial #${t.optuna_trial_number ?? '—'} + PnL ${Number(t.total_pnl || 0).toLocaleString()} + ${dAvg != null ? `일평균 ${Number(dAvg).toLocaleString()}원` : ''} + ${dPct != null ? `일평균 ${Number(dPct) >= 0 ? '+' : ''}${Number(dPct).toFixed(3)}%` : ''} + WR ${Number(t.win_rate || 0).toFixed(1)}% · PF ${Number(t.pf || 0).toFixed(2)} · ${Number(t.total_trades || 0)}건 + ${optunaObWhipCell(t)} + + + +
`; +} + +function optunaBuildGatedTop3Html(rows, opts) { + const o = opts || {}; + const n = o.topN || OPTUNA_CARD_TOP_N; + const list = (rows || []).slice(0, n); + if (!list.length) return ''; + const pr = o.periodRange ? ` · 기간 ${o.periodRange}` : ''; + const phase = o.phaseLabel || '사후합격'; + const note = o.running + ? ' (진행 중 · 사후합격 미확정)' + : ''; + let html = `
${phase} Top${n}${pr}${note}
`; + list.forEach((t, i) => { + html += optunaFormatGatedTopRowHtml(t, i + 1, i < list.length - 1); + }); + return html; +} + +function optunaRenderGatedTop3Card(sum, job) { + const body = $('opt_gated_top3_body'); + const title = $('opt_gated_top3_title'); + if (!body) return; + const phase = optunaRefinePhaseLabel(job); + const phaseTxt = phase ? `${phase} 사후합격 Top${OPTUNA_CARD_TOP_N}` : `사후합격 Top${OPTUNA_CARD_TOP_N}`; + if (title) { + title.innerHTML = `${phaseTxt} DB 적용 후보 · 일평균(원/%) · 익절/손절 비교`; + } + const pr = (job && job.start && job.end) + ? optunaFmtPeriodShort(job.start, job.end) + : ''; + if (job && job.status === 'running' && job.live_summary) { + const ls = job.live_summary; + const ph = ls.refine_phase === 'phase2' ? '2차 TPE' : (ls.refine_phase === 'phase1' ? '1차 TPE' : phase); + const rows = (ls.top3_gated && ls.top3_gated.length) + ? ls.top3_gated + : (ls.top3_learn || []); + if (rows.length) { + body.innerHTML = optunaBuildGatedTop3Html(rows, { + phaseLabel: ph ? `${ph} learn` : 'learn', + periodRange: ls.period_range || pr, + running: !ls.top3_gated || !ls.top3_gated.length, + }); + return; + } + } + const gated = (sum && sum.top5_gated) || []; + if (!gated.length) { + const done = job && job.status === 'done'; + body.innerHTML = done + ? '사후합격 후보 없음' + : '완료 후 표시'; + return; + } + body.innerHTML = optunaBuildGatedTop3Html(gated, { + phaseLabel: phase ? `${phase} 사후합격` : '사후합격', + periodRange: pr, + running: false, + }); +} + +function optunaRenderModeBandTop3(sum, job) { + const el = $('opt_mode_band_top3'); + if (!el) return; + const rows = (sum && sum.top5_consensus) || []; + if (!rows.length) { + el.innerHTML = `mode 밴드 근접 Top${OPTUNA_CARD_TOP_N} 없음`; + return; + } + const pr = (job && job.start && job.end) + ? optunaFmtPeriodShort(job.start, job.end) + : ''; + const pool = (sum.mode_consensus_meta && sum.mode_consensus_meta.mode_pool_size) || null; + el.innerHTML = + optunaBuildModeTop3Html(rows.slice(0, OPTUNA_CARD_TOP_N), { + periodRange: pr, + poolSize: pool, + topN: OPTUNA_CARD_TOP_N, + title: `mode 밴드 근접 Top${OPTUNA_CARD_TOP_N} (분포 참고 · DB 1순위 아님)`, + }); +} + +function optunaRenderParamDistChart(sum) { + const el = $('opt_param_dist_chart'); + if (!el) return; + const d = sum && sum.overfit_diagnostics; + const dist = (d && d.threshold_distribution) || []; + if (!dist.length) { + const done = window._optunaLastJob && window._optunaLastJob.status === 'done'; + el.innerHTML = done + ? '분포 데이터 없음' + : '완료 후 표시'; + return; + } + const rows = dist.filter((r) => { + if (r.p25 == null || r.p75 == null) return false; + const p = String(r.param || ''); + return p && !p.endsWith('_mode') && p !== 'sl_mode'; + }); + const pri = ['tp_pct', 'sl_pct', 'trail_pct', 'trail_arm_pct', 'max_daily_chg', 'vol_mult']; + rows.sort((a, b) => { + const ia = pri.indexOf(String(a.param || '')); + const ib = pri.indexOf(String(b.param || '')); + const pa = ia >= 0 ? ia : 999; + const pb = ib >= 0 ? ib : 999; + if (pa !== pb) return pa - pb; + return String(a.param || '').localeCompare(String(b.param || '')); + }); + if (!rows.length) { + el.innerHTML = '수치형 분포 없음'; + return; + } + const poolNote = d.threshold_pool + ? `
표본: ${d.threshold_pool} · n=${d.threshold_pool_n || 0}
` + : ''; + const bars = rows.map((r) => { + const p25 = Number(r.p25); + const p75 = Number(r.p75); + const med = r.median != null ? Number(r.median) : null; + const mode = r.mode != null ? Number(r.mode) : null; + const vals = [p25, p75, med, mode].filter((v) => v != null && !Number.isNaN(v)); + const lo = Math.min(...vals) * 0.92; + const hi = Math.max(...vals) * 1.08; + const span = hi - lo || 1; + const pct = (v) => Math.max(0, Math.min(100, ((v - lo) / span) * 100)); + const share = r.mode_share != null ? `${(Number(r.mode_share) * 100).toFixed(0)}%` : '—'; + const fmt = (v) => (v == null || Number.isNaN(v)) ? '—' : optunaFmtNum(v, 3); + let marks = ''; + if (med != null && !Number.isNaN(med)) { + marks += `
`; + } + if (mode != null && !Number.isNaN(mode)) { + marks += `
`; + } + const l = pct(p25).toFixed(1); + const w = Math.max(1, pct(p75) - pct(p25)).toFixed(1); + return `
+
+ ${r.param} + mode ${fmt(r.mode)} (${share}) +
+
+
+ ${marks} +
+
p25 ${fmt(p25)} · med ${fmt(med)} · p75 ${fmt(p75)}
+
`; + }).join(''); + el.innerHTML = poolNote + bars; +} + function optunaRenderTrailRec(sum) { const body = $('opt_daily_trail_rec_body'); if (!body) return; @@ -8647,15 +8919,7 @@ function optunaObWhipCell(r) { function optunaRankTableRow(r, source) { const rank = r.rank || 1; const src = source; - const busy = optunaPostBusy(window._optunaLastJob); - const anchors = (((window._optunaLastSummary || {}).postprocess_topn || {}).postprocess_by_anchor) || []; - const hasLearnAnchor = anchors.some((a) => a && a.role === 'learn'); - // 학습 Top「상세」: learn 후처리 앵커가 있을 때만 (gated 비면 learn 폴백) - const detailBtn = (src === 'learn' && !hasLearnAnchor) - ? '' - : (busy - ? '' - : ``); + const detailBtn = ''; const applyCls = src === 'stable' ? 'btn-outline-info' : 'btn-outline-success'; return ` ${rank} @@ -8664,7 +8928,8 @@ function optunaRankTableRow(r, source) { ${optunaFmtNum(r.win_rate, 1)}% ${optunaFmtNum(r.pf, 2)} ${optunaFmtNum(r.total_pnl)} - ${(r.period_daily_avg_pnl != null || r.daily_pnl_mean != null) ? optunaFmtNum(r.period_daily_avg_pnl != null ? r.period_daily_avg_pnl : r.daily_pnl_mean) : '—'} + ${optunaDailyAvgPnlCell(r)} + ${optunaDailyAvgPctCell(r)} ${optunaObWhipCell(r)} ${r.n_losing_days != null ? optunaFmtNum(r.n_losing_days, 0) : '—'} ${r.worst_day_pnl != null ? optunaFmtNum(r.worst_day_pnl) : '—'} @@ -8672,6 +8937,7 @@ function optunaRankTableRow(r, source) { ${optunaOverfitCell(r)} + ${src !== 'mode' ? `` : ''} ${detailBtn} @@ -8682,7 +8948,7 @@ function optunaFillRankTbody(tbodyId, rows, source, emptyMsg) { const tb = $(tbodyId); if (!tb) return; if (!rows || !rows.length) { - tb.innerHTML = `${emptyMsg}`; + tb.innerHTML = `${emptyMsg}`; return; } tb.innerHTML = rows.map((r) => optunaRankTableRow(r, source)).join(''); @@ -9062,12 +9328,8 @@ function _optunaBaseCell(a) { + (bits.length ? `
${bits.join(' · ')}
` : ''); } -function optunaSelectPostprocess(source, rank) { - window._optunaPostSel = { source: source || 'gated', rank: rank || 1 }; - _optunaPostFp = ''; // 선택 바뀌면 표 다시 그림 - optunaRenderPostprocess(window._optunaLastSummary || null); - const box = $('opt_postprocess_rec'); - if (box) box.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); +function optunaSelectPostprocess(_source, _rank) { + // 호가 8방 UI 제거 — no-op } function optunaPostprocessFingerprint(sum) { @@ -9087,223 +9349,8 @@ function optunaPostprocessFingerprint(sum) { ].join('|'); } -function optunaRenderPostprocess(sum) { - const cards = $('opt_postprocess_cards'); - const consEl = $('opt_postprocess_consensus'); - const job = window._optunaLastJob || null; - const finish = () => optunaSyncRerunButton(sum, job); - if (!cards) { finish(); return; } - if (!sum) { - cards.innerHTML = '
사후합격 또는 안정 Top5에서 「상세」를 누르면 이 표가 바뀝니다.
'; - if (consEl) consEl.textContent = '합의: 완료 후 표시'; - _optunaPostFp = ''; - finish(); - return; - } - // 폴링 재진입: 내용·선택이 같으면 DOM 유지 (스크롤 점프 방지) - const fp = optunaPostprocessFingerprint(sum); - if (fp && fp === _optunaPostFp && cards.innerHTML && cards.innerHTML.length > 80) { - finish(); - return; - } - const yWin = window.scrollY || window.pageYOffset || 0; - const box = $('opt_postprocess_rec'); - const zone = $('opt_postprocess_cards'); - const yBox = box ? box.scrollTop : 0; - const yZone = zone ? zone.scrollTop : 0; - try { - const ob8prev = zone && zone.querySelector('.opt-ob8-zone'); - if (ob8prev) window._optunaOb8Scroll = ob8prev.scrollTop; - } catch (e0) { /* ignore */ } - const topn = sum.postprocess_topn; - const anchors = (topn && topn.postprocess_by_anchor) || []; - const cons = (topn && topn.postprocess_consensus) || {}; - if (consEl) { - const cc = cons.combo || {}; - const cws = cons.whipsaw || {}; - const ctr = cons.trail || {}; - if (cc.ok) { - const rs = cc.recommended_stats || {}; - const pnl = rs.pnl != null ? Number(rs.pnl).toLocaleString() : '—'; - const wr = rs.win_rate != null ? Number(rs.win_rate).toFixed(1) + '%' : '—'; - consEl.innerHTML = - '합의(gated+mode, 실매 제외)' - + `
호가 방: ${optunaObComboLabel(cc.combo_id, cc.label)}` - + ` · ${cc.n || 0}앵커 · PnL ${pnl} · WR ${wr}` - + ` · 휩쏘: ${cws.ok ? '있음' : '없음'}` - + ` · 트레일: ${ctr.ok ? ('ARM ' + Number(ctr.arm_krw || 0).toLocaleString()) : '없음'}
` - + (cons.note ? `
${cons.note}
` : ''); - } else { - const ce = cons.entry || {}; - const cx = cons.exit || {}; - const cs = cons.stop || {}; - const kind = optunaPool8wayKind(anchors); - const st0 = optunaOb8wayState(((anchors.find((x) => x.role === 'gated') || {}).orderbook)); - const ran = !!(topn && topn.run_ob_whipsaw); - const head = (kind === 'legacy' && !ran) - ? '호가: 8방 미산출(구JSON)' - : ('호가: 8방 미산출(' + (optunaObReasonKo(st0) || 'TPE 없음') + ')'); - const note = (kind === 'failed' || (kind === 'legacy' && ran)) - ? ('합의=8방 유효 방 없음(' + (optunaObReasonKo(st0) || '') + '). 축분리 median 폴백. 호가스냅이 늘지 않으면 재실행해도 동일.') - : (cons.note || ''); - consEl.innerHTML = - '합의(gated+mode, 실매 제외)' - + `
${head} · 진입 ${ce.ok ? '있음' : '없음'} · 익절 ${cx.ok ? '있음' : '없음'} · 손절 ${cs.ok ? '있음' : '없음'}` - + ` · 휩쏘: ${cws.ok ? '있음' : '없음'} · 트레일: ${ctr.ok ? ('ARM ' + Number(ctr.arm_krw || 0).toLocaleString()) : '없음'}
` - + (note ? `
${note}
` : ''); - } - } - const strat = String((sum && sum.strategy) || (job && job.strategy) || '').toLowerCase(); - const canExitStop = strat === 'momentum' || strat === 'breakout'; - const whipSkip = strat === 'tail' || strat === 'breakout' || strat === 'short'; - if (!anchors.length) { - cards.innerHTML = '
TopN 후처리 없음(구 JSON). 「이 잡 후처리 재실행」으로 축별 숫자를 채우세요.
' - + '
'; - finish(); - return; - } - const sel = window._optunaPostSel || { source: 'gated', rank: 1 }; - let src = sel.source || 'gated'; - let rk = sel.rank || 1; - let a = null; - if (src === 'mode') { - a = anchors.find((x) => x.role === 'mode') || null; - } else if (src === 'stable') { - a = anchors.find((x) => x.role === 'stable' && Number(x.rank) === Number(rk)) || null; - } else if (src === 'learn') { - a = anchors.find((x) => x.role === 'learn' && Number(x.rank) === Number(rk)) || null; - } else { - a = anchors.find((x) => x.role === 'gated' && Number(x.rank) === Number(rk)) || null; - } - // gated 비면 learn# → mode 순으로 폴백 (호가방이 학습 후보에서 보이게) - let fallbackNote = ''; - if (!a) { - const learnA = anchors.find((x) => x.role === 'learn' && Number(x.rank) === 1) - || anchors.find((x) => x.role === 'learn') || null; - const modeA = anchors.find((x) => x.role === 'mode') || null; - if (learnA) { - a = learnA; - src = 'learn'; - rk = Number(learnA.rank) || 1; - fallbackNote = - '
' - + '사후합격(gated) 0건 → 학습 Top 후처리(learn#) 표시. ' - + 'WR/PF 사후게이트와 무관하게 호가 8방을 돌린 결과입니다. DB적용은 여전히 gated 우선.' - + '
'; - window._optunaPostSel = { source: 'learn', rank: rk }; - } else if (modeA) { - a = modeA; - src = 'mode'; - rk = 1; - fallbackNote = - '
' - + '사후합격·학습 앵커 없음 → mode/live만 표시.' - + '
'; - window._optunaPostSel = { source: 'mode', rank: 1 }; - } - } - const live = anchors.find((x) => x.role === 'live') || null; - const ofKey = src === 'stable' ? 'top5_stable' : (src === 'learn' ? 'top5_learn' : 'top5_gated'); - const ofRow = ((sum && sum[ofKey]) || []).find((x) => Number(x.rank) === Number(rk)) || null; - const ofPct = (ofRow && ofRow.overfit_risk_pct != null) - ? ofRow.overfit_risk_pct - : ((topn && topn.apply_overfit_pct != null) ? topn.apply_overfit_pct : sum.apply_overfit_pct); - const verd = (ofRow && (ofRow.overfit_verdict_ui || ofRow.overfit_verdict)) - || (topn && (topn.apply_overfit_verdict_ui || topn.apply_overfit_verdict)) || ''; - if (!a) { - const gatedN = ((sum && sum.top5_gated) || []).length; - const stableN = ((sum && sum.top5_stable) || []).length; - cards.innerHTML = - '
이 순위 후처리 앵커 없음.
' - + `
gated=${gatedN} · stable=${stableN} · ` - + `앵커=${anchors.map((x) => x.id || x.role).join(',') || '없음'}. ` - + '사후합격·학습·안정 Top5「상세」또는 mode 를 선택하세요.
'; - finish(); - return; - } - if (fallbackNote) { - cards.dataset.fallbackNote = '1'; - } else { - delete cards.dataset.fallbackNote; - } - const renderOne = (row, opts) => { - const isLive = !!(opts && opts.live); - const rowSrc = isLive ? '' : (row.role === 'mode' ? 'mode' : (row.role === 'stable' ? 'stable' : (row.role === 'learn' ? 'learn' : 'gated'))); - const rowRk = row.rank || 1; - const ob = row.orderbook || {}; - const whipBtn = () => { - if (isLive || !rowSrc) return '참고'; - if (whipSkip) return '해당없음'; - // 구버튼: base+휩쏘 DB적용 (탐색은 후처리 재실행) - const ws = row.whipsaw || {}; - if (ws && ws.ok === true) { - return ``; - } - return ``; - }; - const baseBtn = () => { - if (isLive || !rowSrc) return '참고'; - return ``; - }; - const wr = row.win_rate != null ? row.win_rate - : (isLive - ? (((row.orderbook || {}).orig_stats || {}).win_rate) - : (ofRow && ofRow.win_rate)); - const rowForBase = Object.assign({}, row, { win_rate: wr }); - const title = isLive - ? `${row.id} · 실매 참고 (적용 없음)` - : `${row.id} · trial ${row.optuna_trial_number != null ? '#' + row.optuna_trial_number : '—'}` - + ` · 과적합 가능도 ${ofPct != null ? ofPct : '—'}% · ${verd || ''}`; - return `
-
${title}
-
과적합%=추정. ${row.note || ''} · 호가 방마다 휩쏘 TPE(통과 체결 기준)
-
차트 타점: ${_optunaBaseCell(rowForBase)} · ${baseBtn()}
- ${_optunaObComboTable(ob, canExitStop, rowSrc, rowRk, isLive, row, whipSkip)} -
- - - - - - - - - -
참고성적적용
000 방 휩쏘(앵커)${_optunaAxisStatsHtml(row.whipsaw, row.whipsaw)}${whipBtn()}
-
-
`; - }; - cards.innerHTML = - (fallbackNote || '') - + renderOne(a, { live: false }) - + (live ? renderOne(live, { live: true }) : ''); - document.querySelectorAll('#opt_top5_tbody tr').forEach((tr, i) => { - tr.style.outline = (src === 'gated' && i + 1 === Number(rk)) ? '1px solid var(--accent)' : ''; - }); - document.querySelectorAll('#opt_top5_learn_tbody tr').forEach((tr, i) => { - tr.style.outline = (src === 'learn' && i + 1 === Number(rk)) ? '1px solid var(--accent)' : ''; - }); - document.querySelectorAll('#opt_top5_stable_tbody tr').forEach((tr, i) => { - tr.style.outline = (src === 'stable' && i + 1 === Number(rk)) ? '1px solid var(--accent)' : ''; - }); - // 폴링 재렌더 후 스크롤 복원 (호가8방 구역·창) - try { - if (box) box.scrollTop = yBox; - if (zone) zone.scrollTop = yZone; - const ob8 = zone && zone.querySelector('.opt-ob8-zone'); - if (ob8 && window._optunaOb8Scroll != null) ob8.scrollTop = window._optunaOb8Scroll; - window.scrollTo(0, yWin); - } catch (e) { /* ignore */ } - _optunaPostFp = optunaPostprocessFingerprint(sum); - // 다음 폴링용 — 구역 안 스크롤 기억 (fingerprint skip 시에도 유지) - try { - const ob8 = zone && zone.querySelector('.opt-ob8-zone'); - if (ob8) { - if (window._optunaOb8Scroll != null) ob8.scrollTop = window._optunaOb8Scroll; - ob8.onscroll = function () { window._optunaOb8Scroll = ob8.scrollTop; }; - } - } catch (e2) { /* ignore */ } - finish(); +function optunaRenderPostprocess(_sum) { + // 호가 8방·후처리 비교표 UI 제거 — no-op (백엔드 postprocess_topn 은 유지) } function optunaRenderTop5(sum) { @@ -9412,6 +9459,7 @@ async function optunaShowCandidate(source, rank) { (j.start && j.end ? ` · 기간 ${j.start}~${j.end}` : '') + (daily ? `
일별 PnL: ${daily}
` : '') + ` ` + + ` ` + ` `; } if (pre) { @@ -9435,6 +9483,86 @@ async function optunaShowCandidate(source, rank) { } } +function optunaFmtPeriodShort(start, end) { + const s = String(start || '').slice(0, 10); + const e = String(end || '').slice(0, 10); + return (s && e) ? `${s}~${e}` : (s || e || '—'); +} + +function optunaPeriodBadge(label, start, end, extraCls) { + const r = optunaFmtPeriodShort(start, end); + return `${label} ${r}`; +} + +function optunaRenderPeriodBanner(job) { + const el = $('opt_period_banner'); + if (!el) return; + const pi = job && job.period_info; + const formS = ($('opt_start')?.value || '').trim().slice(0, 10); + const formE = ($('opt_end')?.value || '').trim().slice(0, 10); + const jobS = String((job && job.start) || '').slice(0, 10); + const jobE = String((job && job.end) || '').slice(0, 10); + if (!pi && !jobS) { + el.innerHTML = ''; + el.className = 'opt-period-banner d-none mb-2 p-2 rounded small'; + return; + } + const formMismatch = !!(formS && formE && jobS && jobE && (formS !== jobS || formE !== jobE)); + const hasRefine = !!(pi && pi.has_refine); + const mismatch = !!(pi && pi.mismatch) || formMismatch; + el.className = 'opt-period-banner mb-2 p-2 rounded small ' + (mismatch ? 'opt-period-warn' : 'opt-period-ok'); + let html = mismatch + ? '
⚠ 기간이 서로 다릅니다 — Top5·일평균·백테 비교 시 study별 기간을 확인하세요
' + : '
📅 백테 기간 (전 단계 동일)
'; + html += '
'; + if (formS && formE) { + const fm = formMismatch ? ' opt-period-badge-warn' : ''; + html += optunaPeriodBadge('상단 폼', formS, formE, 'opt-period-badge-form' + fm); + } + if (jobS && jobE) { + html += optunaPeriodBadge('잡(메인)', jobS, jobE, 'opt-period-badge-master'); + } + if (hasRefine && pi) { + if (pi.phase1 && pi.phase1.start) { + const c = (pi.phase1.start !== jobS || pi.phase1.end !== jobE) ? ' opt-period-badge-warn' : ''; + html += optunaPeriodBadge('1차 TPE', pi.phase1.start, pi.phase1.end, 'opt-period-badge-p1' + c); + } + if (pi.phase2 && pi.phase2.study && pi.phase2.start) { + const c = (pi.phase2.start !== jobS || pi.phase2.end !== jobE) ? ' opt-period-badge-warn' : ''; + html += optunaPeriodBadge('2차 TPE', pi.phase2.start, pi.phase2.end, 'opt-period-badge-p2' + c); + } + if (pi.active && pi.active.study && pi.active.start) { + html += optunaPeriodBadge('활성 study', pi.active.start, pi.active.end, 'opt-period-badge-active'); + } + } else if (pi && pi.active && pi.active.study && pi.active.start) { + html += optunaPeriodBadge('study', pi.active.start, pi.active.end, 'opt-period-badge-active'); + } + html += '
'; + if (formMismatch) { + html += `
상단 폼(${formS}~${formE}) ≠ 선택 잡(${jobS}~${jobE}) — 새 잡 시작 시 폼 기간이 적용됩니다
`; + } + if (pi && pi.mismatch_notes && pi.mismatch_notes.length) { + html += `
${pi.mismatch_notes.join(' · ')}
`; + } + if (pi && pi.refine_note) { + html += `
${pi.refine_note}
`; + } + el.innerHTML = html; +} + +function optunaFormatJobPeriodCell(job) { + const pi = job.period_info; + const base = optunaFmtPeriodShort(job.start, job.end); + if (!pi || !pi.has_refine) return base; + if (!pi.mismatch) return base; + const p1 = pi.phase1 && pi.phase1.range ? pi.phase1.range : ''; + const p2 = (pi.phase2 && pi.phase2.study && pi.phase2.range) ? pi.phase2.range : ''; + let inner = ' ' + base; + if (p1 && p1 !== base) inner += `
1차 ${p1}`; + if (p2 && p2 !== base && p2 !== p1) inner += `
2차 ${p2}`; + return inner; +} + function optunaRenderJob(job) { if (!job) return; if (job.job_id) { @@ -9460,6 +9588,25 @@ function optunaRenderJob(job) { if ($('opt_st_study')) { $('opt_st_study').textContent = job.active_study_name || job.study_name || '—'; } + const refineWrap = $('opt_st_refine_wrap'); + const refineEl = $('opt_st_refine'); + const piRef = job.period_info; + if (refineWrap && refineEl) { + if (piRef && piRef.has_refine && piRef.phase1 && piRef.phase2) { + const p1r = piRef.phase1.range || optunaFmtPeriodShort(piRef.phase1.start, piRef.phase1.end); + const p2r = piRef.phase2.range || optunaFmtPeriodShort(piRef.phase2.start, piRef.phase2.end); + const p1s = (piRef.phase1.study || '').split('_').slice(-3, -1).join('_') || '1차'; + refineEl.innerHTML = + `1차 ${(piRef.phase1.study || '—').slice(0, 48)}` + + ` ${p1r}` + + ` · 2차 ${(piRef.phase2.study || '—').slice(0, 48)}` + + ` ${p2r}`; + refineWrap.classList.remove('d-none'); + } else { + refineEl.textContent = '—'; + refineWrap.classList.add('d-none'); + } + } if ($('opt_st_trials')) { const met = optunaBestMetricsText(prog, job); const nowBits = [ @@ -9500,7 +9647,7 @@ function optunaRenderJob(job) { if ($('opt_st_post')) { const busy = optunaPostBusy(job); $('opt_st_post').textContent = post.hint - || (busy ? '후처리 중 · 「상세」는 끝난 뒤' : (job.status === 'done' ? '후처리 — 표가 있으면 「상세」' : '후처리 — trial 끝난 뒤 시작')); + || (busy ? '후처리 중' : (job.status === 'done' ? '후처리 —' : '후처리 — trial 끝난 뒤 시작')); } if ($('opt_btn_mode_detail')) $('opt_btn_mode_detail').disabled = optunaPostBusy(job); if ($('opt_btn_pp_rerun')) $('opt_btn_pp_rerun').disabled = optunaPostBusy(job); @@ -9530,11 +9677,32 @@ function optunaRenderJob(job) { const lab = k.label || k.strategy || jid; return ``; }).join(''); + } else if (job.status === 'running' && job.live_summary && (job.live_summary.top3_learn || []).length) { + const ls = job.live_summary; + const phase = ls.refine_phase === 'phase2' ? '2차' : '1차'; + const pr = ls.period_range || optunaFmtPeriodShort(ls.period_start, ls.period_end); + let html = `
${phase} learn Top3 · 기간 ${pr} (진행 중 · 축 분포 확인용)
`; + (ls.top3_learn || []).forEach((t, i) => { + html += `
${optunaFormatTopRow(t, i + 1)}
`; + }); + if (ls.refine_phase === 'phase2' && (ls.phase1_top3 || []).length) { + const p1pr = ls.phase1_period_range || ''; + html += `
1차 Top3 (참고${p1pr ? ' · 기간 ' + p1pr : ''})
`; + ls.phase1_top3.forEach((t, i) => { + html += `
${optunaFormatTopRow(t, i + 1)}
`; + }); + } + $('opt_st_top').innerHTML = html; + window._optunaLastSummary = { top5_learn: ls.top3_learn || [], n_all: ls.n_complete }; + optunaRenderTop5(window._optunaLastSummary); } else if (sum && sum.top) { const t = sum.top; const dAvg = (t.period_daily_avg_pnl != null) ? t.period_daily_avg_pnl : t.daily_pnl_mean; + const dPct = (t.period_daily_avg_pct != null) + ? t.period_daily_avg_pct + : t.daily_avg_pct; const mtBit = (sum.min_trades != null) ? ` · min≥${sum.min_trades}` + (sum.n_trading_days != null @@ -9551,6 +9719,7 @@ function optunaRenderJob(job) { `거래 ${t.total_trades} · WR ${Number(t.win_rate || 0).toFixed(1)}% · ` + `PF ${Number(t.pf || 0).toFixed(2)} · PnL ${Number(t.total_pnl || 0).toLocaleString()}원` + (dAvg != null ? ` · 일평균 ${Number(dAvg).toLocaleString()}원` : '') + + (dPct != null ? ` · 일평균 ${Number(dPct) >= 0 ? '+' : ''}${Number(dPct).toFixed(3)}%` : '') + ` · gated ${sum.n_gated}/${sum.n_all}` + (sum.n_stable != null ? ` · stable ${sum.n_stable}` : '') + mtBit; @@ -9583,11 +9752,175 @@ function optunaRenderJob(job) { } optunaRenderCompare(sum); optunaRenderTop5(sum); + optunaRenderGatedTop3Card(sum, job); if (!window._optunaPostSel) window._optunaPostSel = { source: 'gated', rank: 1 }; optunaRenderPostprocess(sum); + optunaRenderPeriodBanner(job); optunaSetNav(job); } +async function optunaBacktestFromCandidate(source, rank) { + if (!_optunaJobId) { + alert('선택된 job 없음 — 최근 잡에서「보기」로 job을 먼저 고르세요'); + return; + } + const src = source || 'gated'; + const rk = rank || 1; + if (src === 'mode') { + alert('mode_combo는 trial 번호가 없어 백테탭 자동 채우기를 지원하지 않습니다.'); + return; + } + const curJob = window._optunaLastJob; + if (curJob && (curJob.kind === 'seq' || curJob.kind === 'seq4') && (curJob.child_jobs || []).length) { + alert('순차 묶음 잡입니다. 전략별 import 잡에서「백테」를 누르세요.'); + return; + } + try { + const qs = new URLSearchParams({ + job_id: _optunaJobId, + source: src, + rank: String(rk), + }); + const r = await fetch('/api/optuna/candidate?' + qs.toString()); + const j = await r.json(); + if (!j.ok) { + alert('❌ ' + (j.error || '후보 없음')); + return; + } + window._optunaCandidateCache = { + strategy: j.strategy, + start: j.start, + end: j.end, + params: j.params_full || j.params_preview || {}, + metrics: j.metrics || {}, + }; + const cache = window._optunaCandidateCache; + const strat = String(cache.strategy || curJob?.strategy || '').toLowerCase(); + const p = cache.params || {}; + const job = curJob || {}; + const trialNo = (j.metrics && j.metrics.optuna_trial_number != null) + ? j.metrics.optuna_trial_number + : '—'; + + const applyJobSources = (cfg) => { + if (cache.start && cfg.startId) setDateVal(cfg.startId, cache.start); + if (cache.end && cfg.endId) setDateVal(cfg.endId, cache.end); + const hist = job.universe_history_source + || $('opt_univ_history_source')?.value + || 'kiwoom'; + if (cfg.univSrcId && $(cfg.univSrcId)) $(cfg.univSrcId).value = hist; + if (cfg.univId && $(cfg.univId)) $(cfg.univId).checked = true; + if (job.candle_source && cfg.candleId && $(cfg.candleId)) { + $(cfg.candleId).value = job.candle_source; + } + if (job.tick_source && cfg.tickId && $(cfg.tickId)) { + $(cfg.tickId).value = job.tick_source; + } + if (job.ob_source && cfg.obId && $(cfg.obId)) { + $(cfg.obId).value = job.ob_source; + } + }; + + const tabMap = { + breakout: 'breakout', + tail: 'tail', + scalp: 'backtest', + momentum: 'momentum', + us_momentum: 'us_momentum', + }; + const tabLabel = { + breakout: '돌파매매', + tail: '꼬리잡기', + scalp: '스캘핑', + momentum: '모멘텀', + us_momentum: '해외 모멘텀', + }; + + if (strat === 'momentum' || strat === 'us_momentum') { + await optunaFillFormFromCandidate(src, rk); + return; + } + + if (strat === 'breakout') { + applyJobSources({ + startId: 'bo_start', + endId: 'bo_end', + univId: 'bo_use_univ_history', + univSrcId: 'bo_univ_history_source', + candleId: 'bo_candle_source', + tickId: 'bo_tick_source', + obId: 'bo_ob_source', + }); + fillBreakoutFormFromOptunaParams(p); + } else if (strat === 'tail') { + applyJobSources({ + startId: 'tl_start', + endId: 'tl_end', + univId: 'tl_use_univ_history', + univSrcId: 'tl_univ_history_source', + tickId: 'tl_tick_source', + obId: 'tl_ob_source', + }); + fillTailFormFromOptunaParams(p); + } else if (strat === 'scalp') { + applyJobSources({ + startId: 'bt_start', + endId: 'bt_end', + univId: 'bt_use_univ_history', + univSrcId: 'bt_univ_history_source', + tickId: 'bt_tick_source', + obId: 'bt_ob_source', + }); + fillScalpFormFromOptunaParams(p); + } else { + alert('백테탭 자동 채우기 미지원 전략: ' + strat); + return; + } + + const wantTab = tabMap[strat] || strat; + const tab = document.querySelector(`[data-tab="${wantTab}"]`); + if (tab && !tab.classList.contains('active')) tab.click(); + + const panelId = wantTab === 'backtest' ? 'tab-backtest' : ('tab-' + wantTab); + const panel = $(panelId); + if (panel) panel.scrollIntoView({ behavior: 'smooth', block: 'start' }); + + alert( + `${tabLabel[strat] || strat} 백테 탭에 반영했습니다.\n` + + `trial #${trialNo} · ${src} #${rk}\n` + + `기간 ${cache.start || ''}~${cache.end || ''}\n` + + '「백테 실행」으로 Optuna 숫자와 비교하세요.\n(DB 저장 아님)' + ); + } catch (e) { + alert('백테 폼 반영 오류: ' + e); + } +} + +function fillBreakoutFormFromOptunaParams(p) { + if (!p || typeof p !== 'object') return; + const b = Object.assign({}, p); + if (b.ob_filter_enabled == null && b._orderbook_filter_enabled != null) { + b.ob_filter_enabled = !!b._orderbook_filter_enabled; + } + if (b.pg_filter_enabled == null && b._program_filter_enabled != null) { + b.pg_filter_enabled = !!b._program_filter_enabled; + } + fillBreakoutFormFromApi(b); + if (typeof fillObWhipReadonly === 'function') fillObWhipReadonly('bo', b); +} + +function fillTailFormFromOptunaParams(p) { + if (!p || typeof p !== 'object') return; + fillTailFormFromApi(p); + if (typeof fillObWhipReadonly === 'function') fillObWhipReadonly('tl', p); +} + +function fillScalpFormFromOptunaParams(p) { + if (!p || typeof p !== 'object') return; + fillScalpFormFromApi(p); + if (typeof fillObWhipReadonly === 'function') fillObWhipReadonly('bt', p); +} + async function optunaFillFormFromCandidate(source, rank) { // 캐시 없으면 다시 fetch let cache = window._optunaCandidateCache; @@ -9763,7 +10096,7 @@ async function optunaApplyUpto(source, rank, upto) { } } } catch (e) { /* ignore */ } - const who = (src === 'mode' ? 'mode_combo(축최빈조립·trial없음)' : (src === 'stable' ? `안정 #${rk}` : (src === 'learn' ? `학습 #${rk}` : `사후합격 #${rk}`))); + const who = (src === 'mode' ? 'mode_combo(축최빈조립·trial없음)' : (src === 'consensus' ? `mode Top10 #${rk}` : (src === 'stable' ? `안정 #${rk}` : (src === 'learn' ? `학습 #${rk}` : `사후합격 #${rk}`)))); // 적용 직전: 익절/손절 숫자를 confirm에 보여 혼동(mode vs gated) 방지 try { const sum2 = window._optunaLastSummary || null; @@ -9772,7 +10105,7 @@ async function optunaApplyUpto(source, rank, upto) { const mr = ((sum2 && sum2.compare_rows) || []).find((r) => r.source === 'mode'); pr = (mr && (mr.params || mr)) || (sum2 && sum2.mode_combo_summary && sum2.mode_combo_summary.params) || null; } else { - const ofKey2 = src === 'stable' ? 'top5_stable' : (src === 'learn' ? 'top5_learn' : 'top5_gated'); + const ofKey2 = src === 'stable' ? 'top5_stable' : (src === 'consensus' ? 'top5_consensus' : (src === 'learn' ? 'top5_learn' : 'top5_gated')); const row2 = ((sum2 && sum2[ofKey2]) || []).find((x) => Number(x.rank) === Number(rk)); pr = row2 && (row2.params || row2); } @@ -10023,9 +10356,17 @@ async function optunaStart() { const entryModes = optunaTailEntryModes(); const slModes = optunaBreakoutSlModes(); const obModes = strategies.includes('breakout') ? optunaBreakoutObModes() : []; + const boCombos = strategies.includes('breakout') + ? slModes.flatMap(sm => obModes.map(om => `${sm}×${om}`)) + : []; if (!strategies.length) { alert('전략을 1개 이상 체크하세요'); return; } if (!start || !end) { alert('시작·종료일을 입력하세요'); return; } - const how = strategies.length >= 2 ? `순차 ${strategies.length}개` : strategies[0]; + const needSeq = strategies.length >= 2 + || (strategies.includes('tail') && entryModes.length >= 2) + || (strategies.includes('breakout') && boCombos.length >= 2); + const how = needSeq + ? `순차1·2차 (${[...strategies, ...(entryModes.length >= 2 ? ['꼬리×2'] : []), ...(boCombos.length >= 2 ? [`돌파×${boCombos.length}`] : [])].join('+')})` + : strategies[0]; const srcLabel = univSrc === 'ls' ? 'LS' : '키움'; const sortLabel = ({ score: '수익·낙폭·표본', @@ -10034,17 +10375,17 @@ async function optunaStart() { pnl: '총손익', })[sortBy] || sortBy; const emLabel = strategies.includes('tail') - ? `\n꼬리진입=${entryModes.join('+')}` + (entryModes.length >= 2 ? ' (순차 2스터디)' : '') + ? `\n꼬리진입=${entryModes.join('+')}` + (entryModes.length >= 2 ? ' (순차·각 1·2차)' : '') : ''; - const boCombos = strategies.includes('breakout') - ? slModes.flatMap(sm => obModes.map(om => `${sm}×${om}`)) - : []; const slLabel = strategies.includes('breakout') - ? `\n돌파=${boCombos.join('+')}` + (boCombos.length >= 2 ? ` (순차 ${boCombos.length}스터디)` : '') + ? `\n돌파=${boCombos.join('+')}` + (boCombos.length >= 2 ? ` (순차·각 1·2차)` : '') : ''; + const chainLabel = needSeq + ? `\n각 전략/조합: 1차(넓은 Grid) → 2차(밴드 축소) 후 다음` + : `\n1·2차 TPE 연쇄 (1차 넓은 Grid → 2차 밴드 축소)`; if (!confirm(`Optuna 시작?\n${how}: ${strategies.join(', ')}\n${start}~${end} trials=${trials}` + (studyTrials ? `\n스터디 총 횟수=${studyTrials}` : '') + - `\n승리식=${sortLabel}\n이력소스=${srcLabel}${emLabel}${slLabel}\n(DB 미적용)`)) return; + `${chainLabel}\n승리식=${sortLabel}\n이력소스=${srcLabel}${emLabel}${slLabel}\n(DB 미적용)`)) return; try { const r = await fetch('/api/optuna/start', { method: 'POST', @@ -10124,6 +10465,48 @@ async function optunaStop() { } catch (e) { alert('오류: ' + e); } } +function optunaFormatModeTopRow(t, i) { + const dAvg = (t.period_daily_avg_pnl != null) ? t.period_daily_avg_pnl : t.daily_pnl_mean; + const dPct = (t.period_daily_avg_pct != null) ? t.period_daily_avg_pct : t.daily_avg_pct; + const prox = t.consensus_match_pct; + const proxN = t.consensus_match_n; + const proxOf = t.consensus_match_of; + let proxBit = ''; + if (prox != null) { + proxBit = ` · 근접 ${Number(prox).toFixed(1)}%`; + if (proxN != null && proxOf != null) proxBit += ` (${proxN}/${proxOf}축)`; + } + return `#${i} trial ${t.optuna_trial_number ?? '—'} · PnL ${Number(t.total_pnl || 0).toLocaleString()}원` + + (dAvg != null ? ` · 일평균 ${Number(dAvg).toLocaleString()}원` : '') + + (dPct != null ? ` · 일평균 ${Number(dPct) >= 0 ? '+' : ''}${Number(dPct).toFixed(3)}%` : '') + + proxBit + + ` · WR ${Number(t.win_rate || 0).toFixed(1)}% · PF ${Number(t.pf || 0).toFixed(2)}`; +} + +function optunaBuildModeTop3Html(rows, opts) { + const o = opts || {}; + const n = o.topN || OPTUNA_CARD_TOP_N; + const list = (rows || []).slice(0, n); + if (!list.length) return ''; + const pr = o.periodRange ? ` · 기간 ${o.periodRange}` : ''; + const pool = o.poolSize != null ? ` · pool ${o.poolSize}건` : ''; + const head = o.title || `mode Top${n} (밴드 근접)`; + let html = `
${head}${pr}${pool}
`; + list.forEach((t, i) => { + html += `
${optunaFormatModeTopRow(t, i + 1)}
`; + }); + return html; +} + +function optunaFormatTopRow(t, i) { + const dAvg = (t.period_daily_avg_pnl != null) ? t.period_daily_avg_pnl : t.daily_pnl_mean; + const dPct = (t.period_daily_avg_pct != null) ? t.period_daily_avg_pct : t.daily_avg_pct; + return `#${i} trial ${t.optuna_trial_number ?? '—'} · PnL ${Number(t.total_pnl || 0).toLocaleString()}원` + + (dAvg != null ? ` · 일평균 ${Number(dAvg).toLocaleString()}원` : '') + + (dPct != null ? ` · 일평균 ${Number(dPct) >= 0 ? '+' : ''}${Number(dPct).toFixed(3)}%` : '') + + ` · WR ${Number(t.win_rate || 0).toFixed(1)}% · PF ${Number(t.pf || 0).toFixed(2)} · score ${Number(t.score || 0).toFixed(3)}`; +} + async function optunaRefreshJobs() { try { const sort = $('opt_jobs_sort')?.value || 'started'; @@ -10139,20 +10522,30 @@ async function optunaRefreshJobs() { const prog = job.progress || {}; const jid = job.job_id; const label = job.label || job.strategy || ''; + const studyNote = (job.study_short && job.kind === 'import') + ? `
${job.study_short}` + : (job.source === 'cli' && job.study_short + ? `
${job.study_short}` + : ''); const sel = jid === _optunaJobId ? 'outline:1px solid var(--accent)' : ''; const st = String(job.status || ''); + const refinePh = prog.refine_phase || ''; + const phaseTag = refinePh === 'phase2' ? '2차' : (refinePh === 'phase1' ? '1차' : ''); const stCell = (st === 'running') - ? `` + ? `` : st; + const statusNote = (st === 'running') + ? (job.leftover_note ? ` ·${job.leftover_note}` : (phaseTag ? ` ·${phaseTag} TPE` : '')) + : (job.leftover_note ? ` ·${job.leftover_note}` : ''); const extraBtns = (st === 'done' && job.can_continue) ? `` + `` : ''; return ` ${jid} - ${label} - ${job.start || ''}~${job.end || ''} - ${stCell}${job.phase === 'postprocess' ? ' ·후처리' : ''}${job.leftover_note ? ' ·' + job.leftover_note : ''} + ${label}${studyNote} + ${optunaFormatJobPeriodCell(job)} + ${stCell}${job.phase === 'postprocess' ? ' ·후처리' : ''}${statusNote} ${(() => { const done = prog.trials_done ?? '—'; const batch = prog.trials_total ?? job.trials ?? '—'; @@ -10183,13 +10576,68 @@ function optunaCloseJoinCmd() { function optunaFillJoinOverlay(job) { if (!job) return; - if ($('opt_join_hint')) $('opt_join_hint').textContent = job.join_hint || ''; + const pi = job.period_info; + let periodHint = ''; + if (pi && (pi.has_refine || pi.active && pi.active.study)) { + const bits = []; + if (job.start && job.end) bits.push(`잡 ${optunaFmtPeriodShort(job.start, job.end)}`); + if (pi.phase1 && pi.phase1.start) bits.push(`1차 ${pi.phase1.range || optunaFmtPeriodShort(pi.phase1.start, pi.phase1.end)}`); + if (pi.phase2 && pi.phase2.study && pi.phase2.start) bits.push(`2차 ${pi.phase2.range || optunaFmtPeriodShort(pi.phase2.start, pi.phase2.end)}`); + if (bits.length) { + periodHint = (pi.mismatch ? '⚠ 기간 불일치 · ' : '📅 ') + bits.join(' · '); + } + } + if ($('opt_join_hint')) { + const base = job.join_hint || ''; + $('opt_join_hint').textContent = periodHint ? (periodHint + (base ? '\n' + base : '')) : base; + } if ($('opt_join_study')) { $('opt_join_study').textContent = job.join_study || job.active_study_name || job.study_name || '—'; } if ($('opt_join_cmd')) $('opt_join_cmd').textContent = job.join_cmd || '(아직 study 없음 · 첫 START 후)'; if ($('opt_join_cmd_ps')) $('opt_join_cmd_ps').textContent = job.join_cmd_ps || '(아직 study 없음 · 첫 START 후)'; + if ($('opt_web_cmd_full')) { + $('opt_web_cmd_full').textContent = job.web_cmd_full || job.web_cmd || job.cmd || '—'; + } if ($('opt_web_cmd')) $('opt_web_cmd').textContent = job.web_cmd || job.cmd || '—'; + const refineBox = $('opt_join_refine'); + if (refineBox) { + const rows = job.seq_refine_cmds || []; + window._optunaSeqRefine = rows; + if (!rows.length) { + refineBox.innerHTML = ''; + } else { + refineBox.innerHTML = + '
PC별 병렬 — 전략/조합마다 1·2차 (refine runner)
' + + '
MariaDB 공유 · PC마다 한 줄 · 2차만은 --phase1-study (DB payload)
' + + rows.map((row, i) => { + const done = row.phase1_db + ? ' (1차 DB OK)' + : (row.done ? ' (1차 OK)' : ''); + const note = row.phase2_note ? `
${row.phase2_note}
` : ''; + return `
+
+ ${row.step}. ${row.label || row.strategy}${done} + + + + + + +
+ ${note} +

+            

+          
`; + }).join(''); + rows.forEach((row, i) => { + const pf = document.getElementById('opt_refine_full_' + i); + if (pf) pf.textContent = row.cmd || ''; + const p2 = document.getElementById('opt_refine_p2_' + i); + if (p2) p2.textContent = row.cmd_phase2 || ''; + }); + } + } const box = $('opt_join_all'); if (box) { const all = job.join_cmds_all || []; @@ -10198,11 +10646,11 @@ function optunaFillJoinOverlay(job) { return; } window._optunaJoinAll = all; - box.innerHTML = '
순차 스터디별 (이미 START 된 것만)
' + + box.innerHTML = '
스터디별 trial 추가 (1·2차 study 이름)
' + all.map((row, i) => { const lab = [row.strategy, row.extra].filter(Boolean).join('/'); return `
- ${i + 1}. ${lab} + ${i + 1}. ${lab} · ${row.study || ''} @@ -10220,6 +10668,16 @@ function optunaFillJoinOverlay(job) { } } +function optunaCopySeqRefine(i, which) { + const row = (window._optunaSeqRefine || [])[i]; + if (!row) return; + let t = row.cmd || ''; + if (which === 'full_ps') t = row.cmd_ps || ''; + if (which === 'p2') t = row.cmd_phase2 || ''; + if (which === 'p2_ps') t = row.cmd_phase2_ps || ''; + optunaCopyText(t); +} + function optunaCopyText(text) { const t = String(text || ''); if (!t) return; @@ -10239,6 +10697,7 @@ function optunaCopyJoinAll(i, which) { function optunaCopyJoin(which) { let el = $('opt_join_cmd'); if (which === 'web') el = $('opt_web_cmd'); + if (which === 'web_full') el = $('opt_web_cmd_full'); if (which === 'ps') el = $('opt_join_cmd_ps'); optunaCopyText(el ? el.textContent : ''); } diff --git a/templates/backtest.html b/templates/backtest.html index 501926a..2b3ef77 100644 --- a/templates/backtest.html +++ b/templates/backtest.html @@ -4166,11 +4166,12 @@
- 승리식 기본=수익·낙폭·표본 · (구)=PnL/max(MDD,하한) · min_trades: 꼬리=1 고정 · 그 외=거래일×2(기본) · 탐색 WR/PF=0 · 사후=results_gated · DB 자동적용 안 함 · 2개 이상=순차 · 이력소스=후보 테이블 · 꼬리: align/limit_atr · 돌파: fixed/atr×호가 off/on + 전략 1개만=1·2차 연쇄 · 2개 이상·꼬리2모드·돌파多=순차(조합마다 1·2차) · 승리식=수익·낙폭·표본 · min_trades: 꼬리=1 · 그 외=거래일×2 · WR/PF=0 · DB 미적용
진행
+
학습 trial
@@ -4187,6 +4188,7 @@
study
+
1·2차
trial
@@ -4213,6 +4215,28 @@
+
+
사후합격 Top5 DB 적용 후보 · 일평균(원/%) · 익절/손절 비교
+
완료 후 표시
+
+ +
+
사후합격 pool 파라미터 분포 분포 참고용 · DB 적용 1순위 아님
+
+
범례 (막대 1줄 = 파라미터 1개)
+
+ + 파란 띠 = p25~p75 · 사후합격 후보들의 중간 50% 구간 (너무 극단값 제외한 흔한 범위) + + 녹색 세로줄 = median · 중앙값 (위·아래 절반이 이 값 기준) + + 주황 동그라미 = mode · 최빈값 (후보 중 가장 많이 나온 숫자 · 괄호 %는 비율) +
+
아래 숫자(p25·med·p75)는 막대와 동일. 익절·손절이 Top5마다 다르게 보이면 이 밴드가 넓다는 뜻입니다.
+
+
완료 후 표시
+
+
@@ -4272,7 +4296,8 @@ #trial거래 승률PFPnL - 일평균 + 일평균(원) + 일평균% 호가·익절·손절·휩쏘 손실일최악일 안정점수 ↑ @@ -4281,7 +4306,7 @@ - 완료 후 표시 + 완료 후 표시
@@ -4295,7 +4320,8 @@ #trial거래 승률PFPnL - 일평균 + 일평균(원) + 일평균% 호가·익절·손절·휩쏘 손실일최악일 안정점수 ↑ @@ -4304,14 +4330,14 @@ - 완료 후 표시 + 완료 후 표시
-
mode_combo 축별 최빈 조립 · trial 번호 없음 · 탐색 TopN 합의 1회 실측 — 표 컬럼=사후합격 Top10과 동일
+
mode_combo 축별 최빈 조립 · PnL 양수 pool · trial 번호 없음 · 2차 완료 후 표시
완료 후 표시
@@ -4319,7 +4345,8 @@ - + + @@ -4328,7 +4355,33 @@ - + + +
#trial거래 승률PFPnL일평균일평균(원)일평균% 호가·익절·손절·휩쏘 손실일최악일 안정점수 ↑
완료 후 표시 (mode 실측 1행 · 사후합격과 같은 열)
완료 후 표시 (mode 실측 1행 · 사후합격과 같은 열)
+
+
+ +
+
mode Top10 results_mode · PnL 양수 pool p25~p75 밴드 근접도 ↑ · trial 있음 · 적용 1순위 아님
+
+
+
+ + + + + + + + + + + + + + + +
#trial근접% ↑거래승률PFPnL일평균(원)일평균%호가·익절·손절·휩쏘손실일최악일안정점수 ↑과적합% ↓
완료 후 표시 (mode Top10 · 구 JSON은 웹에서 즉시 재계산)
@@ -4343,7 +4396,8 @@ #trial거래 승률PFPnL - 일평균 + 일평균(원) + 일평균% 호가·익절·손절·휩쏘 손실일최악일 안정점수 ↑ @@ -4352,24 +4406,10 @@ - 완료 후 표시 (구 JSON은 재실행 필요) + 완료 후 표시 (구 JSON은 재실행 필요)
-
-
후처리 비교표 - (사후합격「상세」· gated 없으면 학습Top learn# 폴백 · mode/live) -
-
- - - 끝난 잡은 이 버튼이 켜집니다. 8방이 비면 누르세요(호가스냅 부족이면 재실행해도 동일). -
-
합의: 완료 후 표시
-
-
사후합격 또는 안정 Top에서 「상세」를 누르면 이 표가 바뀝니다.
-
-
@@ -4412,24 +4452,30 @@
- + \ No newline at end of file