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

@@ -20,8 +20,19 @@ from kis_trader.backtest.breakout_tick_loader import (
tick_coverage_stats,
)
from kis_trader.backtest.optuna_search_space import scalp_grid_axis_keys, suggest_scalp_params
from kis_trader.backtest.optuna_scalping_tpe_space import (
scalp_tpe_axis_keys,
suggest_scalp_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_cli_common import (
apply_session_to_fixed,
combo_passes_search_filters,
@@ -87,13 +98,25 @@ def prepare_scalp_search_context(
max_stocks: Optional[int] = None,
total_budget_krw: Optional[float] = None,
orderbook_filter: str = "off",
history_source: Optional[str] = None,
) -> Optional[ScalpSearchContext]:
grids = _scalp_grids()
if mode not in grids:
logger.error("❌ 스캘핑 mode: %s (fast/trigger/exit/coarse/fine/full/wide)", mode)
if mode == "tpe":
grid_axes: Dict[str, Any] = {}
logger.info(
"📌 mode=tpe — 연속(float/int) 탐색 (Grid categorical 미사용, TPE 가 구간 축소)"
)
elif mode not in grids:
logger.error(
"❌ 스캘핑 mode: %s (fast/trigger/exit/coarse/fine/full/wide/tpe)", mode,
)
return None
else:
grid_axes = grids[mode]
base_fixed = _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()
@@ -135,36 +158,62 @@ def prepare_scalp_search_context(
format_session_hm(base_fixed),
)
grid_axes = grids[mode]
rsi_cands = grid_axes.get("rsi_period") or [base_fixed.get("rsi_period") or 3]
# grid_axes 는 상단에서 mode별 설정 (tpe=빈 dict). grids[mode] 재조회 금지.
rsi_cands = grid_axes.get("rsi_period") or [base_fixed.get("rsi_period") or 7]
try:
rsi_period = max(int(float(x)) for x in rsi_cands)
except (TypeError, ValueError):
rsi_period = int(base_fixed.get("rsi_period") or 3)
codes_candles = _load_candles_for_search(start, end, rsi_period)
rsi_period = int(base_fixed.get("rsi_period") or 7)
if mode == "tpe":
# TPE 상한까지 RSI 로드 (3~14)
rsi_period = max(rsi_period, 14)
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, rsi_period, 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)
# ── 틱재생(ws_ticks) — 웹·실매 정합 (돌파/모멘텀 Optuna 와 동일) ──
# ── 틱재생 — history_source=ls 이면 ls_ws_ticks ──
start_key = (start.replace("-", "") + "0000") if start else "202601010000"
end_key = (end.replace("-", "") + "2359") if end else "999912312359"
ticks_by_code: Dict[str, Any] = {}
tick_rows = 0
tick_backtest_meta: Dict[str, Any] = {}
engine_probe = _ui_to_engine_params(base_fixed)
engine_probe["backtest_tick_fallback_ohlc"] = False
base_fixed["backtest_tick_fallback_ohlc"] = False
if sbc._scalp_backtest_wants_ticks(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()),
)
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()),
)
_tick_lbl = "ls_ws_ticks"
else:
ticks_by_code, tick_rows = load_breakout_ticks_by_code(
_tick_db, start_key, end_key, set(codes_candles.keys()),
)
_tick_lbl = "ws_ticks"
tick_backtest_meta = tick_coverage_stats(codes_candles, ticks_by_code)
tick_backtest_meta["ws_tick_rows_loaded"] = tick_rows
tick_backtest_meta["tick_table"] = _tick_lbl
cov = tick_backtest_meta.get("tick_bar_coverage_pct", 0)
logger.info(
"ws_ticks %s건 | 분봉 커버리지 %s%% (%s/%s종목)",
"%s %s건 | 분봉 커버리지 %s%% (%s/%s종목)",
_tick_lbl,
f"{tick_rows:,}",
cov,
tick_backtest_meta.get("tick_codes_with_data", 0),
@@ -210,13 +259,17 @@ def prepare_scalp_search_context(
if not use_fallback_universe and start_ymd and end_ymd:
history, src, n_slots, scan_iv = sbc.resolve_scalp_universe(
start_ymd, end_ymd, use_saved_history=True, strategy_id="SCALP",
history_source=_hs,
)
if history:
universe_by_slot = history
universe_source = src
base_fixed["scan_interval_min"] = scan_iv
avg = sum(len(v) for v in history.values()) / max(1, n_slots)
logger.info("✅ 유니버스: SCALP 이력 | %d슬롯 · 평균 %.1f종목", n_slots, avg)
logger.info(
"✅ 유니버스: SCALP 이력 src=%s | %d슬롯 · 평균 %.1f종목",
src, n_slots, avg,
)
if universe_by_slot is None:
universe_top_n = int(os.environ.get("UPDATE_UNIVERSE_TOP_N", "20"))
@@ -280,7 +333,7 @@ def prepare_scalp_search_context(
total_budget_krw=total_budget_f,
period_days=period_days,
portfolio=portfolio,
grid_keys=scalp_grid_axis_keys(mode),
grid_keys=scalp_tpe_axis_keys() if mode == "tpe" else scalp_grid_axis_keys(mode),
start_key=start_key,
end_key=end_key,
ticks_by_code=ticks_by_code,
@@ -298,7 +351,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 _scalp_objective_value(result: Dict[str, Any], sort_by: str) -> float:
@@ -336,7 +390,10 @@ def run_scalp_optuna(
)
def objective(trial: optuna.Trial) -> float:
combo = suggest_scalp_params(trial, ctx.mode)
if ctx.mode == "tpe":
combo = suggest_scalp_params_tpe(trial)
else:
combo = suggest_scalp_params(trial, ctx.mode)
result = evaluate_scalp_param_combo(
combo,
base_fixed=ctx.base_fixed,
@@ -377,6 +434,7 @@ def run_scalp_optuna(
"merged_json",
json.dumps(result.get("merged_params") or {}, ensure_ascii=False),
)
set_optuna_trial_stability_attrs(trial, result)
return float(obj)
logger.info(
@@ -411,6 +469,7 @@ def run_scalp_optuna(
"score": float(trial.user_attrs.get("score") or 0),
"optuna_trial_number": trial.number,
}
row.update(stability_fields_from_trial_attrs(trial))
passing.append(row)
if sort_by == "score":
@@ -420,9 +479,7 @@ def run_scalp_optuna(
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)
out_data = {
"engine": "optuna",
@@ -453,7 +510,7 @@ def run_scalp_optuna(
"elapsed_sec": round(elapsed, 1),
"ws_tick_rows_loaded": int(ctx.tick_rows),
"tick_backtest": ctx.tick_backtest_meta,
"results": passing[:5000],
**tiers,
}
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
@@ -514,15 +571,29 @@ def run_scalp_optuna(
def apply_best_scalp_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="score", 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_raw = study.best_trial.user_attrs.get("merged_json") or "{}"
merged_raw = trial.user_attrs.get("merged_json") or "{}"
merged = json.loads(merged_raw)
apply_params_to_db(merged)
logger.info("🚀 [Optuna apply-best] scalp trial #%d → env_config", study.best_trial.number)
logger.info("🚀 [Optuna apply-best] scalp 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="scalp",
log=logger,
)
except Exception as exc:
logger.warning("⚠️ 다단트레일 추천 반영 스킵: %s", exc)
return True