ls증권 히스토리 구독 넣음
This commit is contained in:
@@ -16,8 +16,19 @@ from optuna.samplers import RandomSampler, TPESampler
|
||||
from database import TradeDB
|
||||
from kis_trader.backtest import breakout_backtest_common as bbc
|
||||
from kis_trader.backtest.optuna_search_space import breakout_grid_axis_keys, suggest_breakout_params
|
||||
from kis_trader.backtest.optuna_breakout_tpe_space import (
|
||||
breakout_tpe_axis_keys,
|
||||
suggest_breakout_params_tpe,
|
||||
)
|
||||
from kis_trader.backtest.optuna_mode_combo import enrich_out_data_with_mode_combo
|
||||
from kis_trader.backtest.optuna_common import announce_optuna_json_path, release_shared_tick_store
|
||||
from kis_trader.backtest.optuna_common import (
|
||||
announce_optuna_json_path,
|
||||
build_optuna_result_tiers,
|
||||
pick_gated_apply_trial,
|
||||
release_shared_tick_store,
|
||||
set_optuna_trial_stability_attrs,
|
||||
stability_fields_from_trial_attrs,
|
||||
)
|
||||
from kis_trader.backtest.param_search_breakout import (
|
||||
_bo_fixed_defaults,
|
||||
_load_candles_for_search,
|
||||
@@ -81,13 +92,24 @@ def prepare_breakout_search_context(
|
||||
max_stocks: Optional[int] = None,
|
||||
total_budget_krw: Optional[float] = None,
|
||||
orderbook_filter: str = "off",
|
||||
history_source: Optional[str] = None,
|
||||
) -> Optional[BreakoutSearchContext]:
|
||||
grids = _breakout_grids()
|
||||
if mode not in grids:
|
||||
logger.error("❌ 돌파 mode: %s (fast/coarse/fine/full)", mode)
|
||||
# tpe = 연속 Optuna 전용 (Grid 메뉴 미사용)
|
||||
if mode == "tpe":
|
||||
grid: Dict[str, Any] = {}
|
||||
logger.info(
|
||||
"📌 mode=tpe — 연속(float/int) 탐색 (Grid categorical 미사용, TPE 가 구간 축소)"
|
||||
)
|
||||
elif mode not in grids:
|
||||
logger.error("❌ 돌파 mode: %s (fast/coarse/fine/wide/full/tpe)", mode)
|
||||
return None
|
||||
else:
|
||||
grid = grids[mode]
|
||||
|
||||
base_fixed = _bo_fixed_defaults()
|
||||
if mode == "tpe":
|
||||
base_fixed["skip_hts_scan_dupes"] = False
|
||||
apply_session_to_fixed(base_fixed, time_start_hm=time_start_hm, time_end_hm=time_end_hm)
|
||||
|
||||
_ob_mode = (orderbook_filter or "off").strip().lower()
|
||||
@@ -129,13 +151,21 @@ def prepare_breakout_search_context(
|
||||
)
|
||||
logger.info("📌 진입 모드: %s", breakout_entry_mode())
|
||||
|
||||
from kis_trader.backtest.universe_history_source import (
|
||||
resolve_backtest_universe_history_source,
|
||||
)
|
||||
|
||||
_hs = resolve_backtest_universe_history_source(history_source)
|
||||
base_fixed["_universe_history_source"] = _hs
|
||||
|
||||
codes_candles = _load_candles_for_search(
|
||||
start, end, base_fixed.get("lookback_min", 1), base_fixed,
|
||||
history_source=_hs,
|
||||
)
|
||||
if not codes_candles:
|
||||
logger.error("❌ 캔들 데이터 없음")
|
||||
return None
|
||||
logger.info("✅ 데이터 로드: %s종목", len(codes_candles))
|
||||
logger.info("✅ 데이터 로드: %s종목 (history=%s)", len(codes_candles), _hs)
|
||||
|
||||
share_denom_by_code: Dict[str, float] = {}
|
||||
_share_db = TradeDB()
|
||||
@@ -156,10 +186,18 @@ def prepare_breakout_search_context(
|
||||
if breakout_backtest_wants_tick_replay(engine_probe):
|
||||
_tick_db = TradeDB()
|
||||
try:
|
||||
ticks_by_code, tick_rows = load_breakout_ticks_by_code(
|
||||
_tick_db, start_key, end_key, set(codes_candles.keys()),
|
||||
)
|
||||
logger.info("✅ ws_ticks %s건", f"{tick_rows:,}")
|
||||
if _hs in ("ls", "ls_condition", "ls_ws"):
|
||||
from kis_trader.backtest.ls_history_loaders import load_ls_ticks_by_code
|
||||
|
||||
ticks_by_code, tick_rows = load_ls_ticks_by_code(
|
||||
_tick_db, start_key, end_key, set(codes_candles.keys()),
|
||||
)
|
||||
logger.info("✅ ls_ws_ticks %s건", f"{tick_rows:,}")
|
||||
else:
|
||||
ticks_by_code, tick_rows = load_breakout_ticks_by_code(
|
||||
_tick_db, start_key, end_key, set(codes_candles.keys()),
|
||||
)
|
||||
logger.info("✅ ws_ticks %s건", f"{tick_rows:,}")
|
||||
finally:
|
||||
_tick_db.close()
|
||||
|
||||
@@ -182,7 +220,7 @@ def prepare_breakout_search_context(
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
grid = grids[mode]
|
||||
# grid 는 상단에서 mode별 설정 (tpe=빈 dict). grids[mode] 재조회 금지.
|
||||
_ob_axes = ("max_spread_pct", "min_bid_ask_ratio", "ask_wall_max_qty")
|
||||
_ob_sweeping = any(len(set(grid.get(k) or [])) > 1 for k in _ob_axes)
|
||||
if ob_filter_on and _ob_sweeping:
|
||||
@@ -208,13 +246,18 @@ def prepare_breakout_search_context(
|
||||
if not use_fallback_universe and start_ymd and end_ymd:
|
||||
try:
|
||||
from kis_trader.backtest.breakout_backtest_common import resolve_breakout_universe
|
||||
|
||||
history, src, n_bins, _scan_iv = resolve_breakout_universe(
|
||||
start_ymd, end_ymd, use_saved_history=True,
|
||||
history_source=_hs,
|
||||
)
|
||||
if history:
|
||||
universe_by_slot = history
|
||||
avg = sum(len(v) for v in history.values()) / max(1, n_bins)
|
||||
logger.info("✅ 유니버스: BREAKOUT 이력 | %s분봉 · 평균 %.1f종목", n_bins, avg)
|
||||
logger.info(
|
||||
"✅ 유니버스: BREAKOUT 이력 src=%s | %s분봉 · 평균 %.1f종목",
|
||||
src, n_bins, avg,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.debug("유니버스 이력 스킵: %s", exc)
|
||||
|
||||
@@ -255,7 +298,7 @@ def prepare_breakout_search_context(
|
||||
total_budget_krw=total_budget_f,
|
||||
period_days=period_days,
|
||||
portfolio=portfolio,
|
||||
grid_keys=breakout_grid_axis_keys(mode),
|
||||
grid_keys=breakout_tpe_axis_keys() if mode == "tpe" else breakout_grid_axis_keys(mode),
|
||||
start_key=start_key,
|
||||
end_key=end_key,
|
||||
cache_holder=cache_holder,
|
||||
@@ -267,7 +310,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_breakout_optuna(
|
||||
@@ -294,7 +338,10 @@ def run_breakout_optuna(
|
||||
)
|
||||
|
||||
def objective(trial: optuna.Trial) -> float:
|
||||
combo = suggest_breakout_params(trial, ctx.mode)
|
||||
if ctx.mode == "tpe":
|
||||
combo = suggest_breakout_params_tpe(trial)
|
||||
else:
|
||||
combo = suggest_breakout_params(trial, ctx.mode)
|
||||
result = evaluate_breakout_param_combo(
|
||||
combo,
|
||||
base_fixed=ctx.base_fixed,
|
||||
@@ -327,6 +374,7 @@ def run_breakout_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("merged_json", json.dumps(result.get("merged_params") or {}, ensure_ascii=False))
|
||||
set_optuna_trial_stability_attrs(trial, result)
|
||||
return obj
|
||||
|
||||
logger.info("🔬 Optuna BREAKOUT | study=%s | trials=%d", study_name, n_trials)
|
||||
@@ -346,7 +394,7 @@ def run_breakout_optuna(
|
||||
merged = json.loads(merged_raw)
|
||||
except json.JSONDecodeError:
|
||||
merged = dict(trial.params)
|
||||
passing.append({
|
||||
row = {
|
||||
"params": dict(trial.params),
|
||||
"merged_params": merged,
|
||||
"total_trades": int(trial.user_attrs.get("total_trades") or 0),
|
||||
@@ -354,15 +402,15 @@ def run_breakout_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 == "win_rate":
|
||||
passing.sort(key=lambda r: (-r["win_rate"], -r["total_pnl"]))
|
||||
else:
|
||||
passing.sort(key=lambda r: (-r["total_pnl"], -r["win_rate"]))
|
||||
profitable = [r for r in passing if r["total_pnl"] > 0]
|
||||
if profitable:
|
||||
passing = profitable
|
||||
tiers = build_optuna_result_tiers(passing, sort_by=sort_by)
|
||||
|
||||
hints: Dict[str, str] = {}
|
||||
out_data = {
|
||||
@@ -388,7 +436,7 @@ def run_breakout_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")
|
||||
@@ -449,14 +497,28 @@ def run_breakout_optuna(
|
||||
|
||||
|
||||
def apply_best_breakout_trial(study: optuna.Study) -> bool:
|
||||
if not study.best_trial or study.best_value <= _FAIL_OBJECTIVE + 1:
|
||||
logger.warning("⚠️ 적용할 best trial 없음")
|
||||
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
|
||||
merged = json.loads(study.best_trial.user_attrs.get("merged_json") or "{}")
|
||||
merged = json.loads(trial.user_attrs.get("merged_json") or "{}")
|
||||
apply_params_to_db(merged)
|
||||
logger.info("🚀 [Optuna apply-best] breakout trial #%d → env_config", study.best_trial.number)
|
||||
logger.info("🚀 [Optuna apply-best] breakout gated trial #%d → env_config", trial.number)
|
||||
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="breakout",
|
||||
log=logger,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning("⚠️ 다단트레일 추천 반영 스킵: %s", exc)
|
||||
return True
|
||||
|
||||
Reference in New Issue
Block a user