ls증권 히스토리 구독 넣음

This commit is contained in:
Your Name
2026-07-30 18:05:07 +09:00
parent 61bec4bd1d
commit 67eab24603
1593 changed files with 135733 additions and 1232 deletions

View File

@@ -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)