From 3eaa3b61dfe6e9a726b0a6af7658c01897238470 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 21 Aug 2026 20:18:24 +0900 Subject: [PATCH] =?UTF-8?q?=EC=98=B5=ED=88=AC=EB=82=98=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=EC=88=98=EC=A0=95=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __pycache__/database.cpython-312.pyc | Bin 245323 -> 245439 bytes database.py | 5 +- kis_trader/backtest/bt_rest_warmup_cache.py | 125 ++++++++++++++++ .../backtest/momentum_backtest_common.py | 134 ++++++++++-------- kis_trader/backtest/optuna_breakout.py | 19 ++- kis_trader/backtest/optuna_momentum.py | 19 ++- kis_trader/backtest/optuna_scalping.py | 19 ++- kis_trader/backtest/optuna_study_store.py | 63 ++++++++ kis_trader/backtest/optuna_web_jobs.py | 60 ++++---- kis_trader/backtest/param_search_optuna.py | 24 +++- logs/optuna_latest.jsonpath | 2 +- logs/optuna_momentum_latest.jsonpath | 2 +- logs/optuna_momentum_tpe_latest.jsonpath | 2 +- logs/optuna_web_latest_job.txt | 2 +- 14 files changed, 370 insertions(+), 106 deletions(-) create mode 100644 kis_trader/backtest/bt_rest_warmup_cache.py diff --git a/__pycache__/database.cpython-312.pyc b/__pycache__/database.cpython-312.pyc index dd336205b267f36dd331c3a39418b02368037b33..2e203528ca7c8f989a91d884383fcbf1703b8056 100644 GIT binary patch delta 13938 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 C`E$Vl delta 14150 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 v)AbbdzyWobs&& None: + global _ENSURED + if _ENSURED: + return + try: + from database import TradeDB + + db = TradeDB() + db.conn.execute(_DDL) + _ENSURED = True + except Exception as exc: + logger.warning("⚠️ bt_rest_warmup_cache 테이블 생성 실패: %s", exc) + + +def _day_key(start_key: str) -> str: + s = str(start_key or "").strip().replace("-", "")[:8] + return s if len(s) == 8 and s.isdigit() else "" + + +def load_rest_warmup_prefix( + code: str, + *, + period_day: str, + market: str = "KR", +) -> Optional[Tuple[List[Dict[str, Any]], int]]: + """성공 캐시면 (prefix_rows, n_bars), 없으면 None.""" + c = str(code or "").strip() + day = _day_key(period_day) + mkt = str(market or "KR").strip().upper() or "KR" + if not c or not day: + return None + try: + ensure_bt_rest_warmup_cache_table() + from database import TradeDB + + cur = TradeDB().conn.execute( + "SELECT ok, n_bars, prefix_json FROM bt_rest_warmup_cache " + "WHERE code=%s AND period_day=%s AND market=%s LIMIT 1", + (c, day, mkt), + ) + row = cur.fetchone() + if not row: + return None + if int(row.get("ok") or 0) != 1: + return None + raw = row.get("prefix_json") or "[]" + rows = json.loads(raw) if isinstance(raw, str) else (raw or []) + if not isinstance(rows, list) or not rows: + return None + return rows, int(row.get("n_bars") or 0) + except Exception as exc: + logger.debug("bt_rest_warmup load miss %s: %s", c, exc) + return None + + +def save_rest_warmup_prefix( + code: str, + *, + period_day: str, + market: str, + n_bars: int, + prefix_rows: List[Dict[str, Any]], +) -> bool: + """성공 접두만 upsert. 실패는 저장하지 않음.""" + c = str(code or "").strip() + day = _day_key(period_day) + mkt = str(market or "KR").strip().upper() or "KR" + if not c or not day or not prefix_rows: + return False + try: + ensure_bt_rest_warmup_cache_table() + from database import TradeDB + + payload = json.dumps(prefix_rows, ensure_ascii=False, separators=(",", ":")) + now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + TradeDB().conn.execute( + """ + INSERT INTO bt_rest_warmup_cache + (code, period_day, market, n_bars, ok, prefix_json, updated_at) + VALUES (%s, %s, %s, %s, 1, %s, %s) + ON DUPLICATE KEY UPDATE + n_bars=VALUES(n_bars), + ok=1, + prefix_json=VALUES(prefix_json), + updated_at=VALUES(updated_at) + """, + (c, day, mkt, int(n_bars or 0), payload, now), + ) + return True + except Exception as exc: + logger.warning("⚠️ bt_rest_warmup 저장 실패 %s: %s", c, exc) + return False diff --git a/kis_trader/backtest/momentum_backtest_common.py b/kis_trader/backtest/momentum_backtest_common.py index 379277f..ec057e5 100644 --- a/kis_trader/backtest/momentum_backtest_common.py +++ b/kis_trader/backtest/momentum_backtest_common.py @@ -194,9 +194,9 @@ def inject_momentum_rest_warmup_memory( """ DB 전일봉이 없을 때 키움 ka10080 REST를 종목당 호출해 **메모리에만** prepend. - - 1차: MOMENTUM_BACKTEST_REST_WARMUP_BARS (기본 700) - - 전일 종가 미확보 시에만 2차: MOMENTUM_BACKTEST_REST_WARMUP_BARS_RETRY (기본 1500) - - DB INSERT 없음. 성공 prefix·영구실패는 프로세스 캐시 (Optuna trial 재조회·로그 스팸 방지). + - 사다리 최대 4단(실패 종목만 다음 단): BARS → RETRY → BARS_3 → BARS_4 + - 성공 prefix: 프로세스 RAM + `bt_rest_warmup_cache` (다른 PC 공유) + - 실패는 DB 미저장(재시도 가능). 프로세스 영구실패는 동일 런 내 재호출 방지. """ from kis_trader.utils.env import get_env_bool, get_env_float, get_env_int from kis_trader.utils.logger import get_logger @@ -213,6 +213,7 @@ def inject_momentum_rest_warmup_memory( if len(ps) < 8 or not candles_by_code: return stats period_day = ps[:8] + use_db_cache = get_env_bool("MOMENTUM_BACKTEST_REST_WARMUP_DB_CACHE", True) target: Set[str] if universe_by_slot: @@ -238,15 +239,17 @@ def inject_momentum_rest_warmup_memory( if max_codes > 0: need_codes = need_codes[:max_codes] - n_bars = max( - 50, - int(get_env_int("MOMENTUM_BACKTEST_REST_WARMUP_BARS", 700)), - ) - # 전일(직전 세션) 종가가 1차에 안 잡힐 때만 — 평소엔 700만 - n_retry = max( - n_bars, - int(get_env_int("MOMENTUM_BACKTEST_REST_WARMUP_BARS_RETRY", 1500)), - ) + n1 = max(50, int(get_env_int("MOMENTUM_BACKTEST_REST_WARMUP_BARS", 700))) + n2 = max(n1, int(get_env_int("MOMENTUM_BACKTEST_REST_WARMUP_BARS_RETRY", 1500))) + n3 = max(n2, int(get_env_int("MOMENTUM_BACKTEST_REST_WARMUP_BARS_3", 2500))) + n4 = max(n3, int(get_env_int("MOMENTUM_BACKTEST_REST_WARMUP_BARS_4", 4000))) + ladder: List[int] = [] + seen_n: Set[int] = set() + for n in (n1, n2, n3, n4): + if n not in seen_n: + seen_n.add(n) + ladder.append(n) + ladder = ladder[:4] sleep_sec = float(get_env_float("MOMENTUM_BACKTEST_REST_SLEEP_SEC", 0.25)) kw_key, kw_secret, is_mock = _kiwoom_gap_credentials() if not kw_key or not kw_secret: @@ -256,15 +259,14 @@ def inject_momentum_rest_warmup_memory( from kis_trader.ws.kis_ws import get_kiwoom_candles_df - # 전부 캐시 hit면 Optuna trial 경로에서 INFO 스팸 금지 will_fetch = any( (c, period_day) not in _REST_WARMUP_PREFIX_CACHE for c in need_codes ) if will_fetch: log.info( - "📡 모멘텀 REST 웜업(메모리): 전일봉 부족 %d종목 · ka10080 n=%d" - " (실패 시 n=%d 1회 재시도, DB 미기록)", - len(need_codes), n_bars, n_retry, + "📡 모멘텀 REST 웜업(메모리): 전일봉 부족 %d종목 · 사다리 n=%s" + " (실패 시만 다음 단, DB캐시=%s)", + len(need_codes), ladder, "ON" if use_db_cache else "OFF", ) for i, code in enumerate(need_codes): @@ -274,58 +276,70 @@ def inject_momentum_rest_warmup_memory( stats["fail"] += 1 continue cache_key = (code, period_day) + prefix: List[Dict[str, Any]] = [] + cached = _REST_WARMUP_PREFIX_CACHE.get(cache_key) - did_network = False if cached is not None: stats["cache_hit"] += 1 prefix = [dict(r) for r in cached] - else: - did_network = True + elif use_db_cache: try: - df = get_kiwoom_candles_df( - code, 1, kw_key, kw_secret, is_mock=is_mock, n=n_bars, - ) - except Exception as e: - log.warning("⚠️ REST 웜업 실패 %s: %s", code, e) - _REST_WARMUP_PERM_FAIL.add(cache_key) - stats["fail"] += 1 - continue - if df is None or getattr(df, "empty", True): - _REST_WARMUP_PERM_FAIL.add(cache_key) - stats["fail"] += 1 - continue - try: - prefix = _rest_df_to_prefix(df, rows, ps) - except Exception as e: - log.warning("⚠️ REST 웜업 파싱 실패 %s: %s", code, e) - _REST_WARMUP_PERM_FAIL.add(cache_key) - stats["fail"] += 1 - continue + from kis_trader.backtest.bt_rest_warmup_cache import load_rest_warmup_prefix + hit = load_rest_warmup_prefix(code, period_day=period_day, market="KR") + except Exception: + hit = None + if hit: + db_rows, _n_used = hit + prefix = [dict(r) for r in db_rows] + _REST_WARMUP_PREFIX_CACHE[cache_key] = [dict(r) for r in prefix] + stats["cache_hit"] += 1 - # 1차로 전일 종가 미확보 → 봉 수 늘려 1회만 재시도 (중간 거래일 0봉 등) - if (not prefix or not _momentum_rows_have_prev_day(prefix, period_day)) and n_retry > n_bars: - stats["retry"] += 1 - log.info( - "📡 REST 웜업 재시도 %s: n=%d → n=%d (전일 종가 미확보)", - code, n_bars, n_retry, - ) + if not prefix: + for tier_i, n_try in enumerate(ladder): + if tier_i > 0: + stats["retry"] += 1 + log.info( + "📡 REST 웜업 재시도 %s: n=%d → n=%d (전일 종가 미확보 · %d/%d단)", + code, ladder[tier_i - 1], n_try, tier_i + 1, len(ladder), + ) try: - df2 = get_kiwoom_candles_df( - code, 1, kw_key, kw_secret, is_mock=is_mock, n=n_retry, + df = get_kiwoom_candles_df( + code, 1, kw_key, kw_secret, is_mock=is_mock, n=int(n_try), ) except Exception as e: - log.warning("⚠️ REST 웜업 재시도 실패 %s: %s", code, e) - df2 = None - if df2 is not None and not getattr(df2, "empty", True): - try: - prefix = _rest_df_to_prefix(df2, rows, ps) - except Exception as e: - log.warning("⚠️ REST 웜업 재시도 파싱 실패 %s: %s", code, e) - prefix = [] - - _REST_WARMUP_PREFIX_CACHE[cache_key] = [dict(r) for r in prefix] - if sleep_sec > 0 and did_network and i + 1 < len(need_codes): - time.sleep(sleep_sec) + log.warning("⚠️ REST 웜업 실패 %s n=%s: %s", code, n_try, e) + df = None + if sleep_sec > 0 and i + 1 < len(need_codes): + time.sleep(sleep_sec) + if df is None or getattr(df, "empty", True): + continue + try: + prefix = _rest_df_to_prefix(df, rows, ps) + except Exception as e: + log.warning("⚠️ REST 웜업 파싱 실패 %s: %s", code, e) + prefix = [] + if prefix and _momentum_rows_have_prev_day(prefix, period_day): + _REST_WARMUP_PREFIX_CACHE[cache_key] = [dict(r) for r in prefix] + if use_db_cache: + try: + from kis_trader.backtest.bt_rest_warmup_cache import ( + save_rest_warmup_prefix, + ) + save_rest_warmup_prefix( + code, + period_day=period_day, + market="KR", + n_bars=int(n_try), + prefix_rows=prefix, + ) + except Exception: + pass + break + else: + # 사다리 전부 실패 + _REST_WARMUP_PERM_FAIL.add(cache_key) + stats["fail"] += 1 + continue if not prefix or not _momentum_rows_have_prev_day(prefix, period_day): _REST_WARMUP_PERM_FAIL.add(cache_key) @@ -335,7 +349,7 @@ def inject_momentum_rest_warmup_memory( stats["ok"] += 1 stats["bars"] += len(prefix) - if will_fetch: + if will_fetch or stats["cache_hit"] > 0: log.info( "✅ 모멘텀 REST 웜업 완료: ok=%d fail=%d cache=%d retry=%d bars=%d", stats["ok"], stats["fail"], stats["cache_hit"], stats["retry"], stats["bars"], diff --git a/kis_trader/backtest/optuna_breakout.py b/kis_trader/backtest/optuna_breakout.py index 59dd768..d0dbba8 100644 --- a/kis_trader/backtest/optuna_breakout.py +++ b/kis_trader/backtest/optuna_breakout.py @@ -354,8 +354,14 @@ def run_breakout_optuna( direction="maximize", sampler=_make_sampler(sampler_name, seed), ) - from kis_trader.backtest.optuna_study_store import bind_study_trials, finalize_optuna_export + from kis_trader.backtest.optuna_study_store import ( + bind_study_trials, + clamp_optimize_n_trials, + finalize_optuna_export, + make_study_goal_stop_callback, + ) bind_study_trials(study, n_trials=n_trials, log=logger) + n_trials = clamp_optimize_n_trials(study, n_trials, log=logger) def objective(trial: optuna.Trial) -> float: if ctx.mode == "tpe": @@ -400,7 +406,16 @@ def run_breakout_optuna( logger.info("🔬 Optuna BREAKOUT | study=%s | trials=%d", study_name, n_trials) t0 = time.time() try: - study.optimize(objective, n_trials=n_trials, n_jobs=n_jobs, show_progress_bar=show_progress) + if n_trials <= 0: + logger.info("📌 추가 trial 없음 — 기존 study 결과만 정리") + else: + study.optimize( + objective, + n_trials=n_trials, + n_jobs=n_jobs, + show_progress_bar=show_progress, + callbacks=[make_study_goal_stop_callback(logger)], + ) elapsed = time.time() - t0 passing: List[Dict[str, Any]] = [] diff --git a/kis_trader/backtest/optuna_momentum.py b/kis_trader/backtest/optuna_momentum.py index 51fd4a0..9cfb0b8 100644 --- a/kis_trader/backtest/optuna_momentum.py +++ b/kis_trader/backtest/optuna_momentum.py @@ -431,8 +431,14 @@ def run_momentum_optuna( direction="maximize", sampler=_make_sampler(sampler_name, seed), ) - from kis_trader.backtest.optuna_study_store import bind_study_trials, finalize_optuna_export + from kis_trader.backtest.optuna_study_store import ( + bind_study_trials, + clamp_optimize_n_trials, + finalize_optuna_export, + make_study_goal_stop_callback, + ) bind_study_trials(study, n_trials=n_trials, log=logger) + n_trials = clamp_optimize_n_trials(study, n_trials, log=logger) def objective(trial: optuna.Trial) -> float: if ctx.mode == "tpe": @@ -485,7 +491,16 @@ def run_momentum_optuna( ) t0 = time.time() try: - study.optimize(objective, n_trials=n_trials, n_jobs=n_jobs, show_progress_bar=show_progress) + if n_trials <= 0: + logger.info("📌 추가 trial 없음 — 기존 study 결과만 정리") + else: + study.optimize( + objective, + n_trials=n_trials, + n_jobs=n_jobs, + show_progress_bar=show_progress, + callbacks=[make_study_goal_stop_callback(logger)], + ) elapsed = time.time() - t0 passing: List[Dict[str, Any]] = [] diff --git a/kis_trader/backtest/optuna_scalping.py b/kis_trader/backtest/optuna_scalping.py index 80d4e4a..72259c2 100644 --- a/kis_trader/backtest/optuna_scalping.py +++ b/kis_trader/backtest/optuna_scalping.py @@ -394,8 +394,14 @@ def run_scalp_optuna( direction="maximize", sampler=_make_sampler(sampler_name, seed), ) - from kis_trader.backtest.optuna_study_store import bind_study_trials, finalize_optuna_export + from kis_trader.backtest.optuna_study_store import ( + bind_study_trials, + clamp_optimize_n_trials, + finalize_optuna_export, + make_study_goal_stop_callback, + ) bind_study_trials(study, n_trials=n_trials, log=logger) + n_trials = clamp_optimize_n_trials(study, n_trials, log=logger) def objective(trial: optuna.Trial) -> float: if ctx.mode == "tpe": @@ -452,7 +458,16 @@ def run_scalp_optuna( ) t0 = time.time() try: - study.optimize(objective, n_trials=n_trials, n_jobs=n_jobs, show_progress_bar=show_progress) + if n_trials <= 0: + logger.info("📌 추가 trial 없음 — 기존 study 결과만 정리") + else: + study.optimize( + objective, + n_trials=n_trials, + n_jobs=n_jobs, + show_progress_bar=show_progress, + callbacks=[make_study_goal_stop_callback(logger)], + ) elapsed = time.time() - t0 passing: List[Dict[str, Any]] = [] diff --git a/kis_trader/backtest/optuna_study_store.py b/kis_trader/backtest/optuna_study_store.py index 1d0e86f..80fd5f3 100644 --- a/kis_trader/backtest/optuna_study_store.py +++ b/kis_trader/backtest/optuna_study_store.py @@ -173,6 +173,69 @@ def leftover_trials(study_trials: int, n_finished: int) -> int: return max(0, t - c) +def clamp_optimize_n_trials( + study: Any, + n_trials: int, + *, + log: Optional[logging.Logger] = None, +) -> int: + """ + Optuna study.optimize(n_trials=N) 은 **추가** N회. + study-trials 목표가 있으면 남은 횟수로 잘라서, 목표 초과 연타를 막는다. + """ + lg = log or logger + try: + n = max(0, int(n_trials or 0)) + except (TypeError, ValueError): + n = 0 + try: + goal = parse_study_trials_value((study.user_attrs or {}).get(KIS_STUDY_TRIALS_ATTR)) + except Exception: + goal = 0 + if goal <= 0: + return max(1, n) if n > 0 else 0 + _c, _r, n_f = count_study_states(study) + left = leftover_trials(goal, n_f) + if left <= 0: + lg.info( + "📌 study-trials 목표 도달 — 추가 trial 생략 (끝난시도=%s / 목표=%s)", + n_f, goal, + ) + return 0 + if n <= 0 or n > left: + lg.warning( + "⚠️ --trials=%s → leftover %s 로 축소 (끝난시도=%s / study-trials=%s)", + n, left, n_f, goal, + ) + return left + return n + + +def make_study_goal_stop_callback(log: Optional[logging.Logger] = None): + """trial 끝날 때마다 study-trials 도달이면 study.stop().""" + lg = log or logger + + def _cb(study: Any, _trial: Any) -> None: + try: + goal = parse_study_trials_value((study.user_attrs or {}).get(KIS_STUDY_TRIALS_ATTR)) + except Exception: + return + if goal <= 0: + return + _c, _r, n_f = count_study_states(study) + if n_f >= goal: + lg.info( + "📌 study-trials 도달 → optimize 중지 (끝난시도=%s / 목표=%s)", + n_f, goal, + ) + try: + study.stop() + except Exception: + pass + + return _cb + + def leftover_note( study_trials: int, n_finished: int, diff --git a/kis_trader/backtest/optuna_web_jobs.py b/kis_trader/backtest/optuna_web_jobs.py index eb16f29..d986b3b 100644 --- a/kis_trader/backtest/optuna_web_jobs.py +++ b/kis_trader/backtest/optuna_web_jobs.py @@ -605,7 +605,7 @@ def _join_argv_for_study( extra: str = "", py_bin: str = "python3", ) -> List[str]: - """다른 PC용 — 상대경로. --trials 는 추가분. py_bin=python3|python.""" + """다른 PC용 — 상대경로. --trials 는 추가분(leftover). py_bin=python3|python|venv경로.""" strat = str(strategy or "").strip().lower() mode = str(meta.get("mode") or "tpe").strip() or "tpe" trials = str(int(meta.get("trials") or 200)) @@ -630,32 +630,30 @@ def _join_argv_for_study( "--universe-history-source", hist, ] st_goal = 0 - try: - st_goal = int(meta.get("study_trials") or 0) - except (TypeError, ValueError): - st_goal = 0 leftover = 0 + # 항상 DB 플래그로 leftover 재계산 (meta 캐시가 오래된 200을 붙이지 않게) try: - leftover = int(meta.get("leftover_trials") or 0) - except (TypeError, ValueError): - leftover = 0 - if st_goal <= 0: + from kis_trader.backtest.optuna_study_store import flags_for_web + fl = flags_for_web(study) + st_goal = int(fl.get("study_trials") or 0) + leftover = int(fl.get("leftover_trials") or 0) + except Exception: try: - from kis_trader.backtest.optuna_study_store import flags_for_web - fl = flags_for_web(study) - st_goal = int(fl.get("study_trials") or 0) - leftover = int(fl.get("leftover_trials") or leftover) - except Exception: - pass + st_goal = int(meta.get("study_trials") or 0) + except (TypeError, ValueError): + st_goal = 0 + try: + leftover = int(meta.get("leftover_trials") or 0) + except (TypeError, ValueError): + leftover = 0 if st_goal > 0: argv.extend(["--study-trials", str(st_goal)]) - if leftover > 0: - # --trials 는 이 프로세스 추가분 = 남은 횟수 - try: - idx = argv.index("--trials") - argv[idx + 1] = str(leftover) - except (ValueError, IndexError): - argv.extend(["--trials", str(leftover)]) + # 목표 도달이면 trials=0 (명령 복사해도 추가 연타 금지) + try: + idx = argv.index("--trials") + argv[idx + 1] = str(max(0, leftover)) + except (ValueError, IndexError): + argv.extend(["--trials", str(max(0, leftover))]) extra = str(extra or "").strip() if strat == "tail": ems = list(meta.get("tail_entry_modes") or ["align"]) @@ -679,13 +677,17 @@ def _join_argv_for_study( def _ps_join_script(argv: List[str]) -> str: - """Win11 PowerShell 한 덩어리. 레포 루트에서 실행.""" + """Win11 PowerShell 한 덩어리. 레포 루트에서 실행 · .venv python 고정.""" from kis_trader.backtest.optuna_common import mariadb_creds host = str(mariadb_creds().get("host") or "192.168.0.141") - body = _quote_ps(argv) + body_argv = list(argv or []) + if body_argv and str(body_argv[0]).strip().lower() in ("python", "python3"): + body_argv[0] = r".\.venv\Scripts\python.exe" + body = _quote_ps(body_argv) return ( "# 레포 루트로 이동한 뒤 붙여넣기 (git 커밋 = 웹 VM 과 동일)\n" + "# --trials = 남은 횟수(leftover). study-trials 목표 도달이면 0.\n" "$env:PYTHONUNBUFFERED = '1'\n" "$env:PYTHONPATH = (Get-Location).Path\n" f"$env:DB_HOST = '{host}'\n" @@ -721,8 +723,8 @@ def build_optuna_join_payload(meta: Dict[str, Any]) -> Dict[str, Any]: web_cmd = str(m.get("cmd") or "").strip() hints = [ "레포 루트 · 웹과 같은 git 커밋 · MariaDB 141/kis_optuna.", - "PowerShell 은 python (python3 아님). --trials 는 이 PC 추가분, --study-trials 는 스터디 총 완료 목표.", - "자잘한 VM 10대는 틱 RAM 이 10번 복제됩니다. Win PC 1대 + 기존 VM 이 현실적입니다.", + "PowerShell: .\\.venv\\Scripts\\python.exe 사용. --trials=이 PC 추가분(남은 횟수), --study-trials=스터디 총 목표.", + "목표가 이미 찼으면 --trials 0 (추가 연타 없음). 자잘한 VM 다수보다 Win PC 1대가 현실적.", ] if is_seq: hints.append( @@ -732,7 +734,8 @@ def build_optuna_join_payload(meta: Dict[str, Any]) -> Dict[str, Any]: join_cmd_ps = "" if strat and study: argv_sh = _join_argv_for_study( - m, strategy=strat, study=study, extra=extra, py_bin="python3" + m, strategy=strat, study=study, extra=extra, + py_bin=".venv/bin/python", ) argv_ps = _join_argv_for_study( m, strategy=strat, study=study, extra=extra, py_bin="python" @@ -758,7 +761,8 @@ def build_optuna_join_payload(meta: Dict[str, Any]) -> Dict[str, Any]: continue seen.add(sy) a_sh = _join_argv_for_study( - m, strategy=st, study=sy, extra=ex, py_bin="python3" + m, strategy=st, study=sy, extra=ex, + py_bin=".venv/bin/python", ) a_ps = _join_argv_for_study( m, strategy=st, study=sy, extra=ex, py_bin="python" diff --git a/kis_trader/backtest/param_search_optuna.py b/kis_trader/backtest/param_search_optuna.py index db76f24..a7afcfb 100644 --- a/kis_trader/backtest/param_search_optuna.py +++ b/kis_trader/backtest/param_search_optuna.py @@ -481,8 +481,14 @@ def run_tail_optuna( direction=direction, sampler=sampler, ) - from kis_trader.backtest.optuna_study_store import bind_study_trials, finalize_optuna_export + from kis_trader.backtest.optuna_study_store import ( + bind_study_trials, + clamp_optimize_n_trials, + finalize_optuna_export, + make_study_goal_stop_callback, + ) bind_study_trials(study, n_trials=n_trials, log=logger) + n_trials = clamp_optimize_n_trials(study, n_trials, log=logger) def objective(trial: optuna.Trial) -> float: if ctx.mode == "tpe": @@ -531,12 +537,16 @@ def run_tail_optuna( ) t0 = time.time() try: - study.optimize( - objective, - n_trials=n_trials, - n_jobs=n_jobs, - show_progress_bar=show_progress, - ) + if n_trials <= 0: + logger.info("📌 추가 trial 없음 — 기존 study 결과만 정리") + else: + study.optimize( + objective, + n_trials=n_trials, + n_jobs=n_jobs, + show_progress_bar=show_progress, + callbacks=[make_study_goal_stop_callback(logger)], + ) elapsed = time.time() - t0 logger.info("✅ Optuna 완료 | %.1f초 | 완료 trial %d", elapsed, len(study.trials)) diff --git a/logs/optuna_latest.jsonpath b/logs/optuna_latest.jsonpath index f6a86f5..ee5affe 100644 --- a/logs/optuna_latest.jsonpath +++ b/logs/optuna_latest.jsonpath @@ -1 +1 @@ -/home/hoon/kis_bot/kis_trader/backtest/results/optuna_momentum_tpe_20260821_151059.json +/home/hoon/kis_bot/kis_trader/backtest/results/optuna_momentum_tpe_20260821_194013.json diff --git a/logs/optuna_momentum_latest.jsonpath b/logs/optuna_momentum_latest.jsonpath index f6a86f5..ee5affe 100644 --- a/logs/optuna_momentum_latest.jsonpath +++ b/logs/optuna_momentum_latest.jsonpath @@ -1 +1 @@ -/home/hoon/kis_bot/kis_trader/backtest/results/optuna_momentum_tpe_20260821_151059.json +/home/hoon/kis_bot/kis_trader/backtest/results/optuna_momentum_tpe_20260821_194013.json diff --git a/logs/optuna_momentum_tpe_latest.jsonpath b/logs/optuna_momentum_tpe_latest.jsonpath index f6a86f5..ee5affe 100644 --- a/logs/optuna_momentum_tpe_latest.jsonpath +++ b/logs/optuna_momentum_tpe_latest.jsonpath @@ -1 +1 @@ -/home/hoon/kis_bot/kis_trader/backtest/results/optuna_momentum_tpe_20260821_151059.json +/home/hoon/kis_bot/kis_trader/backtest/results/optuna_momentum_tpe_20260821_194013.json diff --git a/logs/optuna_web_latest_job.txt b/logs/optuna_web_latest_job.txt index b9e94d2..d7685df 100644 --- a/logs/optuna_web_latest_job.txt +++ b/logs/optuna_web_latest_job.txt @@ -1 +1 @@ -opt_20260821_145331_mome +opt_20260821_193005_mome