diff --git a/__pycache__/database.cpython-312.pyc b/__pycache__/database.cpython-312.pyc index dd33620..2e20352 100644 Binary files a/__pycache__/database.cpython-312.pyc and b/__pycache__/database.cpython-312.pyc differ diff --git a/database.py b/database.py index 6fd9cab..8ea69c5 100644 --- a/database.py +++ b/database.py @@ -652,10 +652,13 @@ ENV_CONFIG_KEYS = ( "MOMENTUM_BACKTEST_SCAN_SEC", "MOMENTUM_BACKTEST_UNIVERSE_SCAN_AT", "MOMENTUM_BACKTEST_CANDLE_WARMUP_BARS", - # 백테 REST 전일봉 웜업: 1차 봉수 · 전일 장시작 미확보 시만 재시도 봉수 + # 백테 REST 전일봉 웜업: 1차→실패시만 사다리 증량(최대 4단) · DB 캐시 공유 "MOMENTUM_BACKTEST_REST_WARMUP", "MOMENTUM_BACKTEST_REST_WARMUP_BARS", "MOMENTUM_BACKTEST_REST_WARMUP_BARS_RETRY", + "MOMENTUM_BACKTEST_REST_WARMUP_BARS_3", + "MOMENTUM_BACKTEST_REST_WARMUP_BARS_4", + "MOMENTUM_BACKTEST_REST_WARMUP_DB_CACHE", "MOMENTUM_BACKTEST_REST_MAX_CODES", "MOMENTUM_BACKTEST_REST_SLEEP_SEC", "MOMENTUM_BACKTEST_REST_KIWOOM_MOCK", diff --git a/kis_trader/backtest/bt_rest_warmup_cache.py b/kis_trader/backtest/bt_rest_warmup_cache.py new file mode 100644 index 0000000..a947810 --- /dev/null +++ b/kis_trader/backtest/bt_rest_warmup_cache.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- +"""백테 REST 전일봉 웜업 DB 캐시 — 다른 PC/동일 study 재사용. + +성공분만 저장. 실패는 저장하지 않아 다른 머신·코드 개선 후 재시도 가능. +""" +from __future__ import annotations + +import json +import logging +from datetime import datetime +from typing import Any, Dict, List, Optional, Tuple + +logger = logging.getLogger("kis_trader.bt_rest_warmup_cache") + +_TABLE = "bt_rest_warmup_cache" +_ENSURED = False + +_DDL = """ +CREATE TABLE IF NOT EXISTS bt_rest_warmup_cache ( + code VARCHAR(16) NOT NULL, + period_day CHAR(8) NOT NULL COMMENT '백테 구간 시작일 YYYYMMDD', + market VARCHAR(8) NOT NULL DEFAULT 'KR', + n_bars INT NOT NULL DEFAULT 0 COMMENT '확보에 쓴 REST 봉수', + ok TINYINT NOT NULL DEFAULT 0, + prefix_json LONGTEXT NULL COMMENT '[{datetime,open,high,low,close,volume},...]', + updated_at DATETIME NOT NULL, + PRIMARY KEY (code, period_day, market), + KEY idx_bt_rest_warm_day (period_day, market) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 +COMMENT='모멘텀 백테 REST 전일봉 웜업 공유 캐시(성공만)' +""" + + +def ensure_bt_rest_warmup_cache_table() -> 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