ls증권 히스토리 구독 넣음
This commit is contained in:
@@ -10,7 +10,9 @@ kis_trader/backtest/param_search_optuna.py — Optuna TPE 파라미터 탐색 (
|
||||
# 꼬리
|
||||
python3 kis_trader/backtest/param_search_optuna.py --strategy tail --mode fast --trials 200
|
||||
# 모멘텀 (1위 정렬 기본 score=순익/MDD)
|
||||
python3 kis_trader/backtest/param_search_optuna.py --strategy momentum --mode fast --trials 200
|
||||
# --mode fine → 기존 Grid 이산 메뉴 + categorical
|
||||
# --mode tpe → 연속 float/int (TPE 가 구간 축소, Grid 메뉴 미사용)
|
||||
python3 kis_trader/backtest/param_search_optuna.py --strategy momentum --mode tpe --trials 200
|
||||
# 돌파
|
||||
python3 kis_trader/backtest/param_search_optuna.py --strategy breakout --mode fast --trials 200
|
||||
# 스캘핑 RSI V자 (trigger=진입 / exit=청산)
|
||||
@@ -19,12 +21,15 @@ kis_trader/backtest/param_search_optuna.py — Optuna TPE 파라미터 탐색 (
|
||||
Win11 + VM 동시 분산: 같은 study-name · 같은 storage(141/kis_optuna) 로 각각 --trials 실행.
|
||||
|
||||
DB 적용:
|
||||
--apply-best (1위 trial → env_config, 총손익≤0 이면 스킵)
|
||||
--apply-best (사후게이트 results_gated 통과 trial → env_config, 총손익≤0 이면 스킵)
|
||||
|
||||
Env (선택):
|
||||
OPTUNA_DB_NAME=kis_optuna # 기본. 변경 시에만 설정
|
||||
OPTUNA_STORAGE_URL=... # 전체 URL 직접 지정 시 위보다 우선
|
||||
OPTUNA_TAIL_STUDY_NAME=... # study 이름 고정
|
||||
PARAM_SEARCH_OPTUNA_MIN_WIN_RATE / MIN_PF # 탐색 게이트 기본 0 (TPE 학습)
|
||||
PARAM_SEARCH_OPTUNA_REPORT_MIN_WIN_RATE / MIN_PF # 사후 후보·apply (기본 40 / 1.0)
|
||||
PARAM_SEARCH_OPTUNA_BRIEFING_AI=1 # 최종 JSON 시 Claude 보강(키 없으면 규칙만)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -54,10 +59,16 @@ from kis_trader.backtest import tail_backtest_common as tbc
|
||||
from kis_trader.backtest.optuna_common import (
|
||||
OPTUNA_STRATEGIES,
|
||||
announce_optuna_json_path,
|
||||
build_optuna_result_tiers,
|
||||
ensure_optuna_gate_env_defaults,
|
||||
optuna_run_lock_name,
|
||||
optuna_search_gate_defaults,
|
||||
pick_gated_apply_trial,
|
||||
release_shared_tick_store,
|
||||
resolve_optuna_storage_url,
|
||||
resolve_study_name,
|
||||
set_optuna_trial_stability_attrs,
|
||||
stability_fields_from_trial_attrs,
|
||||
)
|
||||
from kis_trader.backtest.optuna_mode_combo import enrich_out_data_with_mode_combo
|
||||
from kis_trader.backtest.optuna_breakout import (
|
||||
@@ -81,6 +92,7 @@ from kis_trader.backtest.optuna_dart import (
|
||||
run_dart_optuna,
|
||||
)
|
||||
from kis_trader.backtest.optuna_search_space import suggest_tail_params, tail_grid_axis_keys
|
||||
from kis_trader.backtest.optuna_tail_tpe_space import suggest_tail_params_tpe, tail_tpe_axis_keys
|
||||
from kis_trader.backtest.param_search_cli_common import (
|
||||
add_portfolio_cli_args,
|
||||
add_search_filter_cli_args,
|
||||
@@ -107,10 +119,11 @@ _FAIL_OBJECTIVE = -1e18
|
||||
|
||||
# 전략별 --mode 허용값 (Grid CLI 와 동일)
|
||||
STRATEGY_MODES: Dict[str, List[str]] = {
|
||||
"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"],
|
||||
"tail": ["fast", "coarse", "fine", "wide", "full", "massive", "tpe"],
|
||||
"momentum": ["fast", "exit", "rr", "coarse", "fine", "wide", "full", "tpe"],
|
||||
"us_momentum": ["fast", "exit", "rr", "coarse", "fine", "wide", "full", "tpe"],
|
||||
"breakout": ["fast", "coarse", "fine", "wide", "full", "tpe"],
|
||||
"scalp": ["fast", "trigger", "exit", "coarse", "fine", "full", "wide", "tpe"],
|
||||
"dart": ["fast", "coarse", "fine"],
|
||||
}
|
||||
|
||||
@@ -163,6 +176,7 @@ def prepare_tail_search_context(
|
||||
max_stocks: Optional[int] = None,
|
||||
total_budget_krw: Optional[float] = None,
|
||||
orderbook_filter: str = "off",
|
||||
history_source: Optional[str] = None,
|
||||
) -> Optional[TailSearchContext]:
|
||||
"""
|
||||
run_search 와 동일한 데이터·base_params 1회 로드 (Grid 중복 최소화).
|
||||
@@ -218,18 +232,29 @@ def prepare_tail_search_context(
|
||||
start_key, end_key, start_ymd, end_ymd = tbc.date_keys(start, end)
|
||||
|
||||
use_saved_history = not use_fallback_universe
|
||||
universe_by_slot, universe_source, universe_history_slots, scan_interval_min = (
|
||||
tbc.resolve_tail_universe(start_ymd, end_ymd, use_saved_history=use_saved_history)
|
||||
from kis_trader.backtest.universe_history_source import (
|
||||
resolve_backtest_universe_history_source,
|
||||
)
|
||||
|
||||
_hs = resolve_backtest_universe_history_source(history_source)
|
||||
universe_by_slot, universe_source, universe_history_slots, scan_interval_min = (
|
||||
tbc.resolve_tail_universe(
|
||||
start_ymd, end_ymd,
|
||||
use_saved_history=use_saved_history,
|
||||
history_source=_hs,
|
||||
)
|
||||
)
|
||||
# scan_at 타임라인과 슬롯 dict 동일 소스 스태시
|
||||
base_params["_universe_history_source"] = _hs
|
||||
if use_fallback_universe:
|
||||
print("📌 [유니버스] --fallback-universe: 저장 이력 무시 → ws_candles 전 종목")
|
||||
elif universe_source == "history":
|
||||
elif str(universe_source or "").startswith("history"):
|
||||
avg = (
|
||||
sum(len(v) for v in universe_by_slot.values()) / max(1, universe_history_slots)
|
||||
if universe_by_slot else 0
|
||||
)
|
||||
print(
|
||||
f"✅ 유니버스: SHORT 저장 이력 (웹백테 동일) | "
|
||||
f"✅ 유니버스: SHORT 저장 이력 src={universe_source} | "
|
||||
f"{universe_history_slots:,}슬롯 · 평균 {avg:.1f}종목"
|
||||
)
|
||||
else:
|
||||
@@ -239,15 +264,15 @@ def prepare_tail_search_context(
|
||||
base_params["scan_interval_min"] = scan_interval_min
|
||||
base_params["timeframe"] = tail_tf
|
||||
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))
|
||||
base_params.setdefault("backtest_use_tick_exit", _tail_use_tick_exit(None))
|
||||
# 절대규칙: Optuna/파람은 OHLC 폴백으로 숫자 변조 금지 (DB에 ON이어도 강제 OFF)
|
||||
base_params["backtest_tick_fallback_ohlc"] = False
|
||||
if base_params.get("backtest_use_tick_db") or base_params.get("backtest_use_tick_exit"):
|
||||
logger.info("📌 틱재생(ws_ticks): ON — 실매 체결 정합 모드 (OHLC 낙관편향 제거)")
|
||||
logger.info("📌 틱재생(ws_ticks): ON — OHLC 폴백 강제 OFF (정합 절대규칙)")
|
||||
|
||||
logger.info(
|
||||
f"📅 데이터 로드: {start} ~ {end} | TF={tail_tf} | "
|
||||
@@ -310,7 +335,15 @@ def prepare_tail_search_context(
|
||||
pass
|
||||
|
||||
from kis_trader.backtest.tail_param_search import _tail_grids
|
||||
pre_grid = _tail_grids(mode)
|
||||
# tpe = 연속 Optuna (Grid 미사용). 알 수 없는 mode 가 fast 로 폴백되면 안 됨.
|
||||
if mode == "tpe":
|
||||
pre_grid: Dict[str, Any] = {}
|
||||
base_params["skip_hts_scan_dupes"] = False
|
||||
logger.info(
|
||||
"📌 mode=tpe — 연속(float/int) 탐색 (Grid categorical 미사용, TPE 가 구간 축소)"
|
||||
)
|
||||
else:
|
||||
pre_grid = _tail_grids(mode)
|
||||
_ob_axes = ("max_spread_pct", "min_bid_ask_ratio")
|
||||
_ob_sweeping = any(len(set(pre_grid.get(k) or [])) > 1 for k in _ob_axes)
|
||||
if ob_filter_on and _ob_sweeping:
|
||||
@@ -382,7 +415,7 @@ def prepare_tail_search_context(
|
||||
total_budget_krw=total_budget_f,
|
||||
period_days=period_days,
|
||||
portfolio=portfolio,
|
||||
grid_keys=tail_grid_axis_keys(mode),
|
||||
grid_keys=tail_tpe_axis_keys() if mode == "tpe" else tail_grid_axis_keys(mode),
|
||||
ob_filter_on=ob_filter_on,
|
||||
cache_holder=cache_holder,
|
||||
shared_tick_store=shared_tick_store,
|
||||
@@ -395,7 +428,8 @@ def _make_sampler(name: str, seed: Optional[int]):
|
||||
n = (name or "tpe").strip().lower()
|
||||
if n == "random":
|
||||
return RandomSampler(seed=seed)
|
||||
return TPESampler(seed=seed, multivariate=True)
|
||||
# multivariate TPE + 조건부 suggest 시 independent sampling 경고가 trial마다 폭주 → 억제
|
||||
return TPESampler(seed=seed, multivariate=True, warn_independent_sampling=False)
|
||||
|
||||
|
||||
def run_tail_optuna(
|
||||
@@ -426,7 +460,10 @@ def run_tail_optuna(
|
||||
)
|
||||
|
||||
def objective(trial: optuna.Trial) -> float:
|
||||
combo = suggest_tail_params(trial, ctx.mode)
|
||||
if ctx.mode == "tpe":
|
||||
combo = suggest_tail_params_tpe(trial)
|
||||
else:
|
||||
combo = suggest_tail_params(trial, ctx.mode)
|
||||
result = evaluate_tail_param_combo(
|
||||
combo,
|
||||
base_params=ctx.base_params,
|
||||
@@ -457,6 +494,7 @@ def run_tail_optuna(
|
||||
trial.set_user_attr("pf", float(result.get("pf") or 0))
|
||||
trial.set_user_attr("total_trades", int(result["total_trades"]))
|
||||
trial.set_user_attr("params_json", json.dumps(result["params"], ensure_ascii=False))
|
||||
set_optuna_trial_stability_attrs(trial, result)
|
||||
|
||||
if sort_by == "win_rate":
|
||||
return float(result["win_rate"])
|
||||
@@ -489,7 +527,7 @@ def run_tail_optuna(
|
||||
combo = json.loads(params_raw)
|
||||
except json.JSONDecodeError:
|
||||
combo = dict(trial.params)
|
||||
passing.append({
|
||||
row = {
|
||||
"params": combo,
|
||||
"apply_cfg": {**ctx.base_params, **combo},
|
||||
"total_trades": int(trial.user_attrs.get("total_trades") or 0),
|
||||
@@ -497,18 +535,19 @@ def run_tail_optuna(
|
||||
"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,
|
||||
})
|
||||
}
|
||||
row.update(stability_fields_from_trial_attrs(trial))
|
||||
passing.append(row)
|
||||
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
|
||||
tiers = build_optuna_result_tiers(passing, sort_by=sort_by)
|
||||
|
||||
out_data = {
|
||||
"engine": "optuna",
|
||||
"strategy": "tail",
|
||||
"mode": ctx.mode,
|
||||
"start": ctx.start,
|
||||
"end": ctx.end,
|
||||
@@ -535,7 +574,7 @@ def run_tail_optuna(
|
||||
"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],
|
||||
**tiers,
|
||||
}
|
||||
|
||||
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
@@ -626,21 +665,35 @@ def run_tail_optuna(
|
||||
|
||||
|
||||
def apply_best_trial(study: optuna.Study, ctx: TailSearchContext) -> bool:
|
||||
"""Best trial → env_config (총손익≤0 스킵)."""
|
||||
if not study.best_trial or study.best_value <= _FAIL_OBJECTIVE + 1:
|
||||
logger.warning("⚠️ 적용할 best trial 없음 — DB 미적용")
|
||||
"""사후게이트 통과 trial → env_config (총손익≤0 스킵)."""
|
||||
trial = pick_gated_apply_trial(study, sort_by="pnl", fail_objective=_FAIL_OBJECTIVE)
|
||||
if trial is None:
|
||||
logger.warning(
|
||||
"⚠️ 사후게이트(results_gated) 통과 trial 없음 — DB 미적용"
|
||||
)
|
||||
return False
|
||||
pnl = float(study.best_trial.user_attrs.get("total_pnl") or 0)
|
||||
pnl = float(trial.user_attrs.get("total_pnl") or 0)
|
||||
if pnl <= 0:
|
||||
logger.warning("⚠️ Best trial 총손익 ≤ 0 — DB 미적용. 기존 설정 유지.")
|
||||
logger.warning("⚠️ gated trial 총손익 ≤ 0 — DB 미적용. 기존 설정 유지.")
|
||||
return False
|
||||
params_raw = study.best_trial.user_attrs.get("params_json") or "{}"
|
||||
params_raw = trial.user_attrs.get("params_json") or "{}"
|
||||
combo = json.loads(params_raw)
|
||||
merged = {**ctx.base_params, **combo}
|
||||
apply_params_to_db(merged)
|
||||
env_map = _tail_params_to_env_map(merged)
|
||||
logger.info("🚀 [Optuna apply-best] trial #%d → env_config", study.best_trial.number)
|
||||
logger.info("🚀 [Optuna apply-best] gated trial #%d → env_config", trial.number)
|
||||
logger.info("적용된 값: %s", json.dumps(env_map, indent=2, ensure_ascii=False))
|
||||
try:
|
||||
from kis_trader.backtest.optuna_daily_trail_recommend import (
|
||||
apply_daily_trail_recommend_from_optuna_json,
|
||||
)
|
||||
apply_daily_trail_recommend_from_optuna_json(
|
||||
getattr(study, "_kis_export_path", None),
|
||||
strategy="tail",
|
||||
log=logger,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning("⚠️ 다단트레일 추천 반영 스킵: %s", exc)
|
||||
return True
|
||||
|
||||
|
||||
@@ -648,6 +701,12 @@ def main() -> None:
|
||||
from kis_trader.backtest.param_search_dates import resolve_param_search_range
|
||||
week_ago, today = resolve_param_search_range("TAIL", lookback_days=7)
|
||||
|
||||
# Optuna 게이트·브리핑 키 DB 기본값 (없으면 삽입)
|
||||
try:
|
||||
ensure_optuna_gate_env_defaults()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Optuna TPE 파라미터 탐색 (Grid CLI add-on, storage=MariaDB 141 기본)",
|
||||
)
|
||||
@@ -691,9 +750,25 @@ def main() -> None:
|
||||
help="목적함수: tail/breakout pnl|win_rate, momentum score|pnl|win_rate (미지정=전략 기본)",
|
||||
)
|
||||
add_search_filter_cli_args(parser)
|
||||
parser.add_argument("--min_trades", default=3, type=int, help="최소 거래 건수")
|
||||
# Optuna: 탐색 중 승률·PF 게이트 OFF(0) — TPE가 PnL 차이를 학습. 사후 results_gated 로 후보 분리.
|
||||
_sw, _sp, _st = optuna_search_gate_defaults()
|
||||
parser.set_defaults(min_win_rate=_sw, min_pf=_sp, min_trades=_st)
|
||||
parser.add_argument(
|
||||
"--min_trades",
|
||||
default=_st,
|
||||
type=int,
|
||||
help=f"최소 거래 건수 (Optuna 기본 {_st}, Grid CLI 와 별개)",
|
||||
)
|
||||
parser.add_argument("--fallback-universe", action="store_true", dest="fallback_universe")
|
||||
parser.add_argument("--use-universe-history", action="store_true", dest="use_universe_history")
|
||||
parser.add_argument(
|
||||
"--universe-history-source",
|
||||
default=None,
|
||||
choices=["kiwoom", "ls"],
|
||||
dest="universe_history_source",
|
||||
help="이력 테이블: kiwoom=target_candidates_history, ls=ls_candidates_history "
|
||||
"(기본 env BACKTEST_UNIVERSE_HISTORY_SOURCE 또는 kiwoom)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--orderbook-filter", default="off", choices=["off", "on", "auto"],
|
||||
dest="orderbook_filter",
|
||||
@@ -706,6 +781,10 @@ def main() -> None:
|
||||
"--no-progress", action="store_true", dest="no_progress",
|
||||
help="Optuna progress bar 끄기",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--symbol", default="",
|
||||
help="us_momentum 전용: 1종목 유니버스(종목 cfg Optuna). 예: TSLA",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
n_trials = args.trials
|
||||
@@ -732,7 +811,7 @@ def main() -> None:
|
||||
|
||||
strategy = (args.strategy or "tail").strip().lower()
|
||||
if strategy not in OPTUNA_STRATEGIES:
|
||||
logger.error("❌ --strategy 는 tail/momentum/breakout/scalp 중 하나")
|
||||
logger.error("❌ --strategy 는 tail/momentum/us_momentum/breakout/scalp 중 하나")
|
||||
sys.exit(2)
|
||||
|
||||
allowed_modes = STRATEGY_MODES.get(strategy, [])
|
||||
@@ -743,10 +822,10 @@ def main() -> None:
|
||||
|
||||
sort_by = (args.sort_by or "").strip().lower()
|
||||
if not sort_by:
|
||||
sort_by = "score" if strategy in ("momentum", "scalp") else "pnl"
|
||||
sort_by = "score" if strategy in ("momentum", "us_momentum", "scalp") else "pnl"
|
||||
momentum_sort = {"score", "pnl", "win_rate"}
|
||||
basic_sort = {"pnl", "win_rate"}
|
||||
if strategy in ("momentum", "scalp") and sort_by not in momentum_sort:
|
||||
if strategy in ("momentum", "us_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:
|
||||
@@ -790,6 +869,7 @@ def main() -> None:
|
||||
max_stocks=args.max_stocks,
|
||||
total_budget_krw=args.total_budget,
|
||||
orderbook_filter=args.orderbook_filter,
|
||||
history_source=args.universe_history_source,
|
||||
)
|
||||
if ctx is None:
|
||||
sys.exit(1)
|
||||
@@ -810,16 +890,24 @@ def main() -> None:
|
||||
if args.apply_best:
|
||||
apply_best_trial(study, ctx)
|
||||
|
||||
elif strategy == "momentum":
|
||||
elif strategy in ("momentum", "us_momentum"):
|
||||
_mom_market = "US" if strategy == "us_momentum" else "KR"
|
||||
_sym = str(getattr(args, "symbol", "") or "").strip().upper()
|
||||
if _sym and strategy != "us_momentum":
|
||||
logger.error("❌ --symbol 은 us_momentum 전용")
|
||||
sys.exit(2)
|
||||
ctx_m = prepare_momentum_search_context(
|
||||
args.start, args.end, mode,
|
||||
use_fallback_universe=use_fallback,
|
||||
use_fallback_universe=use_fallback or (_mom_market == "US"),
|
||||
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,
|
||||
orderbook_filter="off" if _mom_market == "US" else args.orderbook_filter,
|
||||
market=_mom_market,
|
||||
symbol=_sym if strategy == "us_momentum" else "",
|
||||
history_source=args.universe_history_source,
|
||||
)
|
||||
if ctx_m is None:
|
||||
sys.exit(1)
|
||||
@@ -838,7 +926,11 @@ def main() -> None:
|
||||
show_progress=not args.no_progress,
|
||||
)
|
||||
if args.apply_best:
|
||||
apply_best_momentum_trial(study)
|
||||
if strategy == "us_momentum":
|
||||
from kis_trader.backtest.optuna_momentum import apply_best_us_momentum_trial
|
||||
apply_best_us_momentum_trial(study, symbol=_sym)
|
||||
else:
|
||||
apply_best_momentum_trial(study)
|
||||
|
||||
elif strategy == "scalp":
|
||||
ctx_s = prepare_scalp_search_context(
|
||||
@@ -850,6 +942,7 @@ def main() -> None:
|
||||
max_stocks=args.max_stocks,
|
||||
total_budget_krw=args.total_budget,
|
||||
orderbook_filter=args.orderbook_filter,
|
||||
history_source=args.universe_history_source,
|
||||
)
|
||||
if ctx_s is None:
|
||||
sys.exit(1)
|
||||
@@ -897,6 +990,7 @@ def main() -> None:
|
||||
max_stocks=args.max_stocks,
|
||||
total_budget_krw=args.total_budget,
|
||||
orderbook_filter=args.orderbook_filter,
|
||||
history_source=args.universe_history_source,
|
||||
)
|
||||
if ctx_b is None:
|
||||
sys.exit(1)
|
||||
|
||||
Reference in New Issue
Block a user