ls증권 히스토리 구독 넣음
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user