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

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