feat: 새로운 안전 규칙 및 최적화 적용을 통한 트레이딩 시스템 개선
변경 사항 (Changes): 구문 오류(Syntax error) 및 토큰 낭비를 방지하기 위해 에이전트 쉘(Agent shell)과 파이썬 코드 스니펫에 다수의 신규 안전 규칙(Safety rules)을 추가함. 스키마 검증 및 적절한 SQL 포맷팅을 보장하기 위해 임시(Ad-hoc) 데이터베이스 쿼리 작성 가이드라인을 도입함. 코드 수정 후 UI 기능이 정상 작동하는지 확인하기 위해, 백테스트 웹 서비스 재시작 및 브라우저 검증에 대한 새로운 규칙을 구현함. 시스템 전반의 무결성(Integrity)을 유지하기 위해 실전 매매(Live trading), 웹 백테스팅, 파라미터 탐색(Parameter searches) 간의 일관성 검사(Consistency checks) 체계를 확립함. 기대 효과 (Impact): 이러한 개선 사항들은 트레이딩 시스템의 견고성(Robustness)과 신뢰성을 향상시키며, 에러 발생을 최소화하고 다양한 시스템 컴포넌트 간의 원활한 상호작용을 보장함.
This commit is contained in:
@@ -4,7 +4,7 @@ kis_trader/backtest/param_search_optuna.py — Optuna TPE 파라미터 탐색 (
|
||||
==============================================================================
|
||||
기존 Grid CLI(tail_param_search.py 등)는 그대로 두고, Bayesian(TPE) add-on.
|
||||
|
||||
현재 구현: --strategy tail | momentum | breakout
|
||||
현재 구현: --strategy tail | momentum | breakout | scalp
|
||||
|
||||
실행 예:
|
||||
# 꼬리
|
||||
@@ -13,6 +13,8 @@ kis_trader/backtest/param_search_optuna.py — Optuna TPE 파라미터 탐색 (
|
||||
python3 kis_trader/backtest/param_search_optuna.py --strategy momentum --mode fast --trials 200
|
||||
# 돌파
|
||||
python3 kis_trader/backtest/param_search_optuna.py --strategy breakout --mode fast --trials 200
|
||||
# 스캘핑 RSI V자 (trigger=진입 / exit=청산)
|
||||
python3 kis_trader/backtest/param_search_optuna.py --strategy scalp --mode trigger --trials 100
|
||||
|
||||
Win11 + VM 동시 분산: 같은 study-name · 같은 storage(141/kis_optuna) 로 각각 --trials 실행.
|
||||
|
||||
@@ -51,10 +53,13 @@ from database import TradeDB
|
||||
from kis_trader.backtest import tail_backtest_common as tbc
|
||||
from kis_trader.backtest.optuna_common import (
|
||||
OPTUNA_STRATEGIES,
|
||||
announce_optuna_json_path,
|
||||
optuna_run_lock_name,
|
||||
release_shared_tick_store,
|
||||
resolve_optuna_storage_url,
|
||||
resolve_study_name,
|
||||
)
|
||||
from kis_trader.backtest.optuna_mode_combo import enrich_out_data_with_mode_combo
|
||||
from kis_trader.backtest.optuna_breakout import (
|
||||
apply_best_breakout_trial,
|
||||
prepare_breakout_search_context,
|
||||
@@ -65,6 +70,11 @@ from kis_trader.backtest.optuna_momentum import (
|
||||
prepare_momentum_search_context,
|
||||
run_momentum_optuna,
|
||||
)
|
||||
from kis_trader.backtest.optuna_scalping import (
|
||||
apply_best_scalp_trial,
|
||||
prepare_scalp_search_context,
|
||||
run_scalp_optuna,
|
||||
)
|
||||
from kis_trader.backtest.optuna_search_space import suggest_tail_params, tail_grid_axis_keys
|
||||
from kis_trader.backtest.param_search_cli_common import (
|
||||
add_portfolio_cli_args,
|
||||
@@ -92,9 +102,10 @@ _FAIL_OBJECTIVE = -1e18
|
||||
|
||||
# 전략별 --mode 허용값 (Grid CLI 와 동일)
|
||||
STRATEGY_MODES: Dict[str, List[str]] = {
|
||||
"tail": ["fast", "coarse", "fine", "full", "massive"],
|
||||
"momentum": ["fast", "rr", "coarse", "fine", "full"],
|
||||
"breakout": ["fast", "coarse", "fine", "full"],
|
||||
"tail": ["fast", "coarse", "fine", "wide", "full", "massive"],
|
||||
"momentum": ["fast", "exit", "rr", "coarse", "fine", "wide", "full"],
|
||||
"breakout": ["fast", "coarse", "fine", "wide", "full"],
|
||||
"scalp": ["fast", "trigger", "exit", "coarse", "fine", "full", "wide"],
|
||||
}
|
||||
|
||||
|
||||
@@ -171,8 +182,8 @@ def prepare_tail_search_context(
|
||||
"적용" if ob_filter_on else "스킵 — 코어 파라미터 순수 탐색",
|
||||
)
|
||||
|
||||
row = db.conn.execute("SELECT * FROM env_config ORDER BY id DESC LIMIT 1").fetchone()
|
||||
r = dict(row) if row else {}
|
||||
from kis_trader.backtest.backtest_portfolio_common import load_portfolio_env_row
|
||||
r = load_portfolio_env_row(db)
|
||||
fee_rate, sell_tax, _slot_from_fee = tbc.fee_and_slot_from_env_row(r)
|
||||
portfolio = tbc.resolve_tail_portfolio_params(
|
||||
r,
|
||||
@@ -221,10 +232,15 @@ def prepare_tail_search_context(
|
||||
base_params = dict(base_params)
|
||||
base_params["scan_interval_min"] = scan_interval_min
|
||||
base_params["timeframe"] = tail_tf
|
||||
base_params.setdefault("backtest_tick_fallback_ohlc", True)
|
||||
from kis_trader.engine.tail_tick_replay import tail_backtest_use_tick_db as _tail_use_tick
|
||||
from kis_trader.engine.tail_tick_replay import (
|
||||
tail_backtest_tick_fallback_ohlc as _tail_tick_fallback,
|
||||
tail_backtest_use_tick_db as _tail_use_tick,
|
||||
tail_backtest_use_tick_exit as _tail_use_tick_exit,
|
||||
)
|
||||
base_params.setdefault("backtest_tick_fallback_ohlc", _tail_tick_fallback(None))
|
||||
base_params.setdefault("backtest_use_tick_db", _tail_use_tick(None))
|
||||
if base_params.get("backtest_use_tick_db"):
|
||||
base_params.setdefault("backtest_use_tick_exit", _tail_use_tick_exit(None))
|
||||
if base_params.get("backtest_use_tick_db") or base_params.get("backtest_use_tick_exit"):
|
||||
logger.info("📌 틱재생(ws_ticks): ON — 실매 체결 정합 모드 (OHLC 낙관편향 제거)")
|
||||
|
||||
logger.info(
|
||||
@@ -452,111 +468,155 @@ def run_tail_optuna(
|
||||
n_jobs=n_jobs,
|
||||
show_progress_bar=show_progress,
|
||||
)
|
||||
finally:
|
||||
# 탐색 종료(또는 예외) 시 공유메모리 즉시 해제 (atexit 는 크래시 대비 이중 안전장치).
|
||||
_store = getattr(ctx, "shared_tick_store", None)
|
||||
if _store is not None:
|
||||
elapsed = time.time() - t0
|
||||
logger.info("✅ Optuna 완료 | %.1f초 | 완료 trial %d", elapsed, len(study.trials))
|
||||
|
||||
# JSON export — study.user_attrs 기준 (n_jobs>1 에도 안전)
|
||||
passing: List[Dict[str, Any]] = []
|
||||
for trial in study.trials:
|
||||
if trial.state != optuna.trial.TrialState.COMPLETE:
|
||||
continue
|
||||
if not trial.user_attrs.get("gates_ok"):
|
||||
continue
|
||||
params_raw = trial.user_attrs.get("params_json") or "{}"
|
||||
try:
|
||||
_store.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
ctx.shared_tick_store = None
|
||||
elapsed = time.time() - t0
|
||||
logger.info("✅ Optuna 완료 | %.1f초 | 완료 trial %d", elapsed, len(study.trials))
|
||||
combo = json.loads(params_raw)
|
||||
except json.JSONDecodeError:
|
||||
combo = dict(trial.params)
|
||||
passing.append({
|
||||
"params": combo,
|
||||
"apply_cfg": {**ctx.base_params, **combo},
|
||||
"total_trades": int(trial.user_attrs.get("total_trades") or 0),
|
||||
"win_rate": float(trial.user_attrs.get("win_rate") or 0),
|
||||
"total_pnl": float(trial.user_attrs.get("total_pnl") or 0),
|
||||
"pf": float(trial.user_attrs.get("pf") or 0),
|
||||
"optuna_trial_number": trial.number,
|
||||
})
|
||||
if sort_by == "pnl":
|
||||
passing.sort(key=lambda r: (-float(r["total_pnl"]), -float(r["win_rate"])))
|
||||
else:
|
||||
passing.sort(key=lambda r: (-float(r["win_rate"]), -float(r["total_pnl"])))
|
||||
|
||||
# JSON export — study.user_attrs 기준 (n_jobs>1 에도 안전)
|
||||
passing: List[Dict[str, Any]] = []
|
||||
for trial in study.trials:
|
||||
if trial.state != optuna.trial.TrialState.COMPLETE:
|
||||
continue
|
||||
if not trial.user_attrs.get("gates_ok"):
|
||||
continue
|
||||
params_raw = trial.user_attrs.get("params_json") or "{}"
|
||||
profitable = [r for r in passing if float(r.get("total_pnl") or 0) > 0]
|
||||
if profitable:
|
||||
passing = profitable
|
||||
|
||||
out_data = {
|
||||
"engine": "optuna",
|
||||
"mode": ctx.mode,
|
||||
"start": ctx.start,
|
||||
"end": ctx.end,
|
||||
"timeframe": ctx.tail_tf,
|
||||
"universe_source": ctx.universe_source,
|
||||
"universe_history_slots": ctx.universe_history_slots,
|
||||
"slot_money": int(ctx.slot_money),
|
||||
"max_stocks": ctx.max_stocks,
|
||||
"total_budget_krw": int(ctx.total_budget_krw),
|
||||
"portfolio_mode": True,
|
||||
"budget_warning": ctx.portfolio.get("budget_warning"),
|
||||
"backtest_days": ctx.period_days,
|
||||
"min_trades": min_trades,
|
||||
"min_win_rate": min_win_rate,
|
||||
"min_pf": min_pf,
|
||||
"sort_by": sort_by,
|
||||
"grid_keys": ctx.grid_keys,
|
||||
"grid_axis_hints": {k: TAIL_GRID_AXIS_HINTS_KO[k] for k in ctx.grid_keys if k in TAIL_GRID_AXIS_HINTS_KO},
|
||||
"holding_peak_in_candles": ctx.has_holding_peak,
|
||||
"optuna_study_name": study_name,
|
||||
"optuna_storage": storage_url,
|
||||
"optuna_n_trials_requested": n_trials,
|
||||
"optuna_trials_completed": len(study.trials),
|
||||
"optuna_best_value": study.best_value if study.best_trial else None,
|
||||
"optuna_best_trial_number": study.best_trial.number if study.best_trial else None,
|
||||
"elapsed_sec": round(elapsed, 1),
|
||||
"results": passing[:5000],
|
||||
}
|
||||
|
||||
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
out_name = f"optuna_tail_{ctx.mode}_{ts}.json"
|
||||
out_dir = _results_dir_for_write()
|
||||
out_path = os.path.join(out_dir, out_name)
|
||||
try:
|
||||
combo = json.loads(params_raw)
|
||||
except json.JSONDecodeError:
|
||||
combo = dict(trial.params)
|
||||
passing.append({
|
||||
"params": combo,
|
||||
"apply_cfg": {**ctx.base_params, **combo},
|
||||
"total_trades": int(trial.user_attrs.get("total_trades") or 0),
|
||||
"win_rate": float(trial.user_attrs.get("win_rate") or 0),
|
||||
"total_pnl": float(trial.user_attrs.get("total_pnl") or 0),
|
||||
"pf": float(trial.user_attrs.get("pf") or 0),
|
||||
"optuna_trial_number": trial.number,
|
||||
})
|
||||
if sort_by == "pnl":
|
||||
passing.sort(key=lambda r: (-float(r["total_pnl"]), -float(r["win_rate"])))
|
||||
else:
|
||||
passing.sort(key=lambda r: (-float(r["win_rate"]), -float(r["total_pnl"])))
|
||||
|
||||
profitable = [r for r in passing if float(r.get("total_pnl") or 0) > 0]
|
||||
if profitable:
|
||||
passing = profitable
|
||||
|
||||
out_data = {
|
||||
"engine": "optuna",
|
||||
"mode": ctx.mode,
|
||||
"start": ctx.start,
|
||||
"end": ctx.end,
|
||||
"timeframe": ctx.tail_tf,
|
||||
"universe_source": ctx.universe_source,
|
||||
"universe_history_slots": ctx.universe_history_slots,
|
||||
"slot_money": int(ctx.slot_money),
|
||||
"max_stocks": ctx.max_stocks,
|
||||
"total_budget_krw": int(ctx.total_budget_krw),
|
||||
"portfolio_mode": True,
|
||||
"budget_warning": ctx.portfolio.get("budget_warning"),
|
||||
"backtest_days": ctx.period_days,
|
||||
"min_trades": min_trades,
|
||||
"min_win_rate": min_win_rate,
|
||||
"min_pf": min_pf,
|
||||
"sort_by": sort_by,
|
||||
"grid_keys": ctx.grid_keys,
|
||||
"grid_axis_hints": {k: TAIL_GRID_AXIS_HINTS_KO[k] for k in ctx.grid_keys if k in TAIL_GRID_AXIS_HINTS_KO},
|
||||
"holding_peak_in_candles": ctx.has_holding_peak,
|
||||
"optuna_study_name": study_name,
|
||||
"optuna_storage": storage_url,
|
||||
"optuna_n_trials_requested": n_trials,
|
||||
"optuna_trials_completed": len(study.trials),
|
||||
"optuna_best_value": study.best_value if study.best_trial else None,
|
||||
"optuna_best_trial_number": study.best_trial.number if study.best_trial else None,
|
||||
"elapsed_sec": round(elapsed, 1),
|
||||
"results": passing[:5000],
|
||||
}
|
||||
|
||||
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
out_name = f"optuna_tail_{ctx.mode}_{ts}.json"
|
||||
out_dir = _results_dir_for_write()
|
||||
out_path = os.path.join(out_dir, out_name)
|
||||
try:
|
||||
with open(out_path, "w", encoding="utf-8") as f:
|
||||
json.dump(out_data, f, indent=2, ensure_ascii=False)
|
||||
except OSError:
|
||||
fb = os.path.join(os.path.expanduser("~"), ".kis_bot_search_results")
|
||||
os.makedirs(fb, exist_ok=True)
|
||||
out_path = os.path.join(fb, out_name)
|
||||
with open(out_path, "w", encoding="utf-8") as f:
|
||||
json.dump(out_data, f, indent=2, ensure_ascii=False)
|
||||
logger.warning("⚠️ results/ 쓰기 권한 없음 → 폴백 저장: %s", out_path)
|
||||
|
||||
logger.info("💾 Optuna 결과 저장: %s", out_path)
|
||||
|
||||
if study.best_trial and study.best_value > _FAIL_OBJECTIVE + 1:
|
||||
bt = study.best_trial
|
||||
logger.info(
|
||||
"🏆 Best trial #%d | objective=%.4g | pnl=%s | wr=%.1f%% | trades=%s",
|
||||
bt.number,
|
||||
study.best_value,
|
||||
bt.user_attrs.get("total_pnl"),
|
||||
float(bt.user_attrs.get("win_rate") or 0),
|
||||
bt.user_attrs.get("total_trades"),
|
||||
with open(out_path, "w", encoding="utf-8") as f:
|
||||
json.dump(out_data, f, indent=2, ensure_ascii=False)
|
||||
except OSError:
|
||||
fb = os.path.join(os.path.expanduser("~"), ".kis_bot_search_results")
|
||||
os.makedirs(fb, exist_ok=True)
|
||||
out_path = os.path.join(fb, out_name)
|
||||
with open(out_path, "w", encoding="utf-8") as f:
|
||||
json.dump(out_data, f, indent=2, ensure_ascii=False)
|
||||
logger.warning("⚠️ results/ 쓰기 권한 없음 → 폴백 저장: %s", out_path)
|
||||
announce_optuna_json_path(
|
||||
out_path, strategy="tail", mode=ctx.mode, note="중간저장(mode 전)", log=logger,
|
||||
)
|
||||
else:
|
||||
logger.info("⚠️ 조건 만족 trial 없음 (min_trades·승률·PF 게이트 확인)")
|
||||
|
||||
study._kis_export_path = out_path # type: ignore[attr-defined]
|
||||
return study
|
||||
def _eval_mode(combo: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
return evaluate_tail_param_combo(
|
||||
combo,
|
||||
base_params=ctx.base_params,
|
||||
candles_by_code=ctx.candles_by_code,
|
||||
fee_rate=ctx.fee_rate,
|
||||
sell_tax=ctx.sell_tax,
|
||||
min_trades=1,
|
||||
min_win_rate=0.0,
|
||||
min_pf=0.0,
|
||||
universe_by_slot=ctx.universe_by_slot,
|
||||
slot_money=ctx.slot_money,
|
||||
max_stocks=ctx.max_stocks,
|
||||
total_budget_krw=ctx.total_budget_krw,
|
||||
period_days=ctx.period_days,
|
||||
cache_holder=ctx.cache_holder,
|
||||
ticks_by_code=ctx.ticks_by_code,
|
||||
orderbook_by_code=ctx.orderbook_by_code,
|
||||
program_by_code=ctx.program_by_code,
|
||||
log_verdict_by_code=ctx.log_verdict_by_code,
|
||||
)
|
||||
|
||||
def _save_partial(_data: Dict[str, Any]) -> None:
|
||||
try:
|
||||
with open(out_path, "w", encoding="utf-8") as f:
|
||||
json.dump(_data, f, indent=2, ensure_ascii=False)
|
||||
except OSError as exc:
|
||||
logger.warning("⚠️ mode_combo 부분저장 실패: %s", exc)
|
||||
return
|
||||
announce_optuna_json_path(
|
||||
out_path, strategy="tail", mode=ctx.mode, note="mode_combo params 저장(실측 전)", log=logger,
|
||||
)
|
||||
|
||||
enrich_out_data_with_mode_combo(
|
||||
out_data,
|
||||
evaluate_fn=_eval_mode,
|
||||
grid_keys=ctx.grid_keys,
|
||||
log=logger,
|
||||
on_partial_save=_save_partial,
|
||||
)
|
||||
try:
|
||||
with open(out_path, "w", encoding="utf-8") as f:
|
||||
json.dump(out_data, f, indent=2, ensure_ascii=False)
|
||||
except OSError as exc:
|
||||
logger.warning("⚠️ mode_combo 반영 재저장 실패: %s", exc)
|
||||
announce_optuna_json_path(
|
||||
out_path, strategy="tail", mode=ctx.mode, note="최종 JSON", log=logger,
|
||||
)
|
||||
|
||||
if study.best_trial and study.best_value > _FAIL_OBJECTIVE + 1:
|
||||
bt = study.best_trial
|
||||
logger.info(
|
||||
"🏆 Best trial #%d | objective=%.4g | pnl=%s | wr=%.1f%% | trades=%s",
|
||||
bt.number,
|
||||
study.best_value,
|
||||
bt.user_attrs.get("total_pnl"),
|
||||
float(bt.user_attrs.get("win_rate") or 0),
|
||||
bt.user_attrs.get("total_trades"),
|
||||
)
|
||||
else:
|
||||
logger.info("⚠️ 조건 만족 trial 없음 (min_trades·승률·PF 게이트 확인)")
|
||||
|
||||
study._kis_export_path = out_path # type: ignore[attr-defined]
|
||||
return study
|
||||
finally:
|
||||
# mode_combo 실측이 ticks 공유뷰를 쓰므로 optimize 직후 unlink 금지
|
||||
release_shared_tick_store(ctx, log=logger)
|
||||
|
||||
|
||||
def apply_best_trial(study: optuna.Study, ctx: TailSearchContext) -> bool:
|
||||
@@ -579,18 +639,18 @@ def apply_best_trial(study: optuna.Study, ctx: TailSearchContext) -> bool:
|
||||
|
||||
|
||||
def main() -> None:
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
week_ago = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d")
|
||||
from kis_trader.backtest.param_search_dates import resolve_param_search_range
|
||||
week_ago, today = resolve_param_search_range("TAIL", lookback_days=7)
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Optuna TPE 파라미터 탐색 (Grid CLI add-on, storage=MariaDB 141 기본)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--strategy", default="tail", choices=list(OPTUNA_STRATEGIES),
|
||||
help="전략: tail | momentum | breakout",
|
||||
help="전략: tail | momentum | breakout | scalp",
|
||||
)
|
||||
parser.add_argument("--start", default=week_ago, help="시작일 YYYY-MM-DD")
|
||||
parser.add_argument("--end", default=today, help="종료일 YYYY-MM-DD")
|
||||
parser.add_argument("--start", default=week_ago, help="시작일 YYYY-MM-DD (거래일 보정)")
|
||||
parser.add_argument("--end", default=today, help="종료일 YYYY-MM-DD (주말·휴장이면 이전 장운영일)")
|
||||
parser.add_argument("--timeframe", "--tf", default=3, type=int, dest="timeframe",
|
||||
help="ws_candles 분봉 3·5·15·60")
|
||||
add_portfolio_cli_args(parser)
|
||||
@@ -666,7 +726,7 @@ def main() -> None:
|
||||
|
||||
strategy = (args.strategy or "tail").strip().lower()
|
||||
if strategy not in OPTUNA_STRATEGIES:
|
||||
logger.error("❌ --strategy 는 tail/momentum/breakout 중 하나")
|
||||
logger.error("❌ --strategy 는 tail/momentum/breakout/scalp 중 하나")
|
||||
sys.exit(2)
|
||||
|
||||
allowed_modes = STRATEGY_MODES.get(strategy, [])
|
||||
@@ -677,11 +737,11 @@ def main() -> None:
|
||||
|
||||
sort_by = (args.sort_by or "").strip().lower()
|
||||
if not sort_by:
|
||||
sort_by = "score" if strategy == "momentum" else "pnl"
|
||||
sort_by = "score" if strategy in ("momentum", "scalp") else "pnl"
|
||||
momentum_sort = {"score", "pnl", "win_rate"}
|
||||
basic_sort = {"pnl", "win_rate"}
|
||||
if strategy == "momentum" and sort_by not in momentum_sort:
|
||||
logger.error("❌ momentum --sort-by 는 score|pnl|win_rate")
|
||||
if strategy in ("momentum", "scalp") and sort_by not in momentum_sort:
|
||||
logger.error("❌ %s --sort-by 는 score|pnl|win_rate", strategy)
|
||||
sys.exit(2)
|
||||
if strategy in ("tail", "breakout") and sort_by not in basic_sort:
|
||||
logger.error("❌ %s --sort-by 는 pnl|win_rate", strategy)
|
||||
@@ -711,6 +771,7 @@ def main() -> None:
|
||||
cli_override=args.study_name,
|
||||
)
|
||||
|
||||
study = None
|
||||
try:
|
||||
if strategy == "tail":
|
||||
ctx = prepare_tail_search_context(
|
||||
@@ -773,6 +834,36 @@ def main() -> None:
|
||||
if args.apply_best:
|
||||
apply_best_momentum_trial(study)
|
||||
|
||||
elif strategy == "scalp":
|
||||
ctx_s = prepare_scalp_search_context(
|
||||
args.start, args.end, mode,
|
||||
use_fallback_universe=use_fallback,
|
||||
time_start_hm=args.time_start,
|
||||
time_end_hm=args.time_end,
|
||||
slot_money=args.slot_money,
|
||||
max_stocks=args.max_stocks,
|
||||
total_budget_krw=args.total_budget,
|
||||
orderbook_filter=args.orderbook_filter,
|
||||
)
|
||||
if ctx_s is None:
|
||||
sys.exit(1)
|
||||
study = run_scalp_optuna(
|
||||
ctx_s,
|
||||
n_trials=n_trials,
|
||||
storage_url=storage_url,
|
||||
study_name=study_name,
|
||||
min_trades=args.min_trades,
|
||||
min_win_rate=args.min_win_rate,
|
||||
min_pf=args.min_pf,
|
||||
sort_by=sort_by,
|
||||
sampler_name=sampler_name,
|
||||
seed=args.seed,
|
||||
n_jobs=n_jobs,
|
||||
show_progress=not args.no_progress,
|
||||
)
|
||||
if args.apply_best:
|
||||
apply_best_scalp_trial(study)
|
||||
|
||||
else:
|
||||
ctx_b = prepare_breakout_search_context(
|
||||
args.start, args.end, mode,
|
||||
@@ -803,6 +894,17 @@ def main() -> None:
|
||||
if args.apply_best:
|
||||
apply_best_breakout_trial(study)
|
||||
|
||||
# 종료 직전 절대경로 한 번 더 (로그 끝에서 바로 복사)
|
||||
export = getattr(study, "_kis_export_path", None) if study is not None else None
|
||||
if export:
|
||||
announce_optuna_json_path(
|
||||
str(export),
|
||||
strategy=strategy,
|
||||
mode=mode,
|
||||
note="CLI 종료·열기용 경로",
|
||||
log=logger,
|
||||
)
|
||||
|
||||
except KeyboardInterrupt as e:
|
||||
print(f"\n⛔ {e} — 중단", flush=True)
|
||||
sys.exit(130)
|
||||
|
||||
Reference in New Issue
Block a user