feat(param-search): Add new evaluation functions for breakout, momentum, and tail parameter combinations
Changes: - Added `apply_params_to_db` function to streamline parameter application to the database. - Introduced `evaluate_breakout_param_combo`, `evaluate_momentum_param_combo`, and `evaluate_tail_param_combo` functions to enhance the evaluation of parameter combinations for respective strategies. - Updated `requirements.txt` to include `optuna==4.2.1` for improved optimization capabilities. Impact: - These additions improve the modularity and efficiency of parameter evaluations across different trading strategies, facilitating better optimization and backtesting processes.
This commit is contained in:
@@ -623,6 +623,11 @@ def _apply_to_db(best_params: dict) -> None:
|
||||
print(f" {db_col:<35s} = {val}")
|
||||
|
||||
|
||||
def apply_params_to_db(best_params: dict) -> None:
|
||||
"""웹·CLI 공통 — 파라서치 merged → DB."""
|
||||
_apply_to_db(best_params)
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# UI(%) → 엔진(비율) 변환
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
@@ -632,6 +637,116 @@ def _ui_to_engine_params(ui_params: dict) -> dict:
|
||||
return breakout_ui_to_engine_params(ui_params)
|
||||
|
||||
|
||||
def evaluate_breakout_param_combo(
|
||||
combo: Dict[str, Any],
|
||||
*,
|
||||
base_fixed: Dict[str, Any],
|
||||
grid_keys: List[str],
|
||||
codes_candles: Dict[str, List[Dict]],
|
||||
min_trades: int,
|
||||
min_win_rate: float,
|
||||
min_pf: float,
|
||||
universe_by_slot: Optional[Dict[str, List[str]]] = None,
|
||||
slot_money: float = 2_000_000.0,
|
||||
max_stocks: int = 3,
|
||||
total_budget_krw: float = 6_000_000.0,
|
||||
fee_rate: float = 0.00015,
|
||||
sell_tax: float = 0.0018,
|
||||
period_days: int = 1,
|
||||
cache_holder: Optional[Dict[str, Any]] = None,
|
||||
ticks_by_code: Any = None,
|
||||
orderbook_by_code: Any = None,
|
||||
program_by_code: Any = None,
|
||||
log_verdict_by_code: Any = None,
|
||||
share_denom_by_code: Optional[Dict[str, float]] = None,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""단일 돌파 조합 백테 — Grid 워커·Optuna objective 공통."""
|
||||
if "prev_chg_min" in combo and "prev_chg_max" in combo:
|
||||
if float(combo["prev_chg_min"]) >= float(combo["prev_chg_max"]):
|
||||
return None
|
||||
|
||||
ui_params = dict(base_fixed)
|
||||
ui_params.update(combo)
|
||||
engine_params = _ui_to_engine_params(ui_params)
|
||||
if "max_spread_pct" in ui_params and ui_params.get("max_spread_pct") is not None:
|
||||
engine_params["_ob_max_spread_pct"] = float(ui_params["max_spread_pct"])
|
||||
if "min_bid_ask_ratio" in ui_params and ui_params.get("min_bid_ask_ratio") is not None:
|
||||
engine_params["_ob_min_bid_ask_ratio"] = float(ui_params["min_bid_ask_ratio"])
|
||||
if "ask_wall_max_qty" in ui_params and ui_params.get("ask_wall_max_qty") is not None:
|
||||
engine_params["_ob_ask_wall_max_qty"] = float(ui_params["ask_wall_max_qty"])
|
||||
if ui_params.get("_orderbook_filter_enabled") is not None:
|
||||
engine_params["_orderbook_filter_enabled"] = bool(ui_params["_orderbook_filter_enabled"])
|
||||
if cache_holder:
|
||||
engine_params.update(cache_holder)
|
||||
engine_params["slot_money"] = float(slot_money)
|
||||
engine_params["max_stocks"] = int(max_stocks)
|
||||
engine_params["total_budget_krw"] = float(total_budget_krw)
|
||||
engine_params["portfolio_mode"] = True
|
||||
if share_denom_by_code:
|
||||
engine_params["share_denom_by_code"] = share_denom_by_code
|
||||
if log_verdict_by_code:
|
||||
engine_params["_backtest_log_verdict_by_code"] = log_verdict_by_code
|
||||
|
||||
meta: Dict[str, Any] = {}
|
||||
trades = bbc.run_breakout_backtest_web_aligned(
|
||||
codes_candles, engine_params, universe_by_slot,
|
||||
slot_money=slot_money, fee_rate=fee_rate, sell_tax=sell_tax,
|
||||
max_stocks=max_stocks, total_budget_krw=total_budget_krw,
|
||||
ticks_by_code=ticks_by_code,
|
||||
orderbook_by_code=orderbook_by_code,
|
||||
program_by_code=program_by_code,
|
||||
meta_out=meta,
|
||||
)
|
||||
stats = bbc.summarize_breakout_trades(
|
||||
trades, total_budget_krw=total_budget_krw, period_days=period_days,
|
||||
)
|
||||
total_trades = stats["total_trades"]
|
||||
if total_trades < min_trades:
|
||||
return None
|
||||
|
||||
total_pnl = stats["total_pnl"]
|
||||
win_rate = stats["win_rate"]
|
||||
pf = float(stats.get("pf") or 0)
|
||||
if not combo_passes_search_filters(
|
||||
win_rate=win_rate, pf=pf,
|
||||
min_win_rate=min_win_rate, min_pf=min_pf,
|
||||
):
|
||||
return None
|
||||
|
||||
avg_hold = stats["avg_hold_min"]
|
||||
peak, mdd, cum = 0.0, 0.0, 0.0
|
||||
for t in trades:
|
||||
cum += t["pnl"]
|
||||
if cum > peak:
|
||||
peak = cum
|
||||
dd = peak - cum
|
||||
if dd > mdd:
|
||||
mdd = dd
|
||||
|
||||
merged = dict(ui_params)
|
||||
merged["slot_money"] = float(slot_money)
|
||||
merged["max_stocks"] = int(max_stocks)
|
||||
merged["total_budget_krw"] = float(total_budget_krw)
|
||||
|
||||
return {
|
||||
"params": {k: ui_params[k] for k in grid_keys if k in ui_params},
|
||||
"total_pnl": int(total_pnl),
|
||||
"win_rate": round(win_rate, 2),
|
||||
"total_trades": total_trades,
|
||||
"pf": round(pf, 2),
|
||||
"avg_hold": round(avg_hold, 1),
|
||||
"mdd": round(mdd),
|
||||
"bot_pct": stats["bot_pct"],
|
||||
"daily_avg_pct": stats["daily_avg_pct"],
|
||||
"avg_profit_rate": _avg_profit_rate_pct(trades),
|
||||
"sell_reasons": _count_sell_reasons(trades),
|
||||
"skipped_micro_buys": int(
|
||||
(meta.get("skip_stats") or {}).get("skipped_micro_buys") or 0
|
||||
),
|
||||
"merged_params": merged,
|
||||
}
|
||||
|
||||
|
||||
def _evaluate_breakout_chunk(
|
||||
param_chunk: List[Dict[str, Any]],
|
||||
base_fixed: Dict[str, Any],
|
||||
@@ -654,6 +769,8 @@ def _evaluate_breakout_chunk(
|
||||
ticks_preloaded = None
|
||||
orderbook_preloaded = None
|
||||
program_preloaded = None
|
||||
log_verdict_preloaded = None
|
||||
share_denom_preloaded = None
|
||||
if shared:
|
||||
if codes_candles is None:
|
||||
codes_candles = shared.get("codes_candles") or {}
|
||||
@@ -672,6 +789,8 @@ def _evaluate_breakout_chunk(
|
||||
ticks_preloaded = _tm
|
||||
orderbook_preloaded = shared.get("orderbook_by_code")
|
||||
program_preloaded = shared.get("program_by_code")
|
||||
log_verdict_preloaded = shared.get("log_verdict_by_code")
|
||||
share_denom_preloaded = shared.get("share_denom_by_code")
|
||||
if codes_candles is None:
|
||||
codes_candles = {}
|
||||
cache_holder: Dict[str, Any] = {}
|
||||
@@ -679,96 +798,33 @@ def _evaluate_breakout_chunk(
|
||||
local_heap: List[Tuple[float, float, int, Dict]] = []
|
||||
for combo in param_chunk:
|
||||
assert_parent_alive()
|
||||
if "prev_chg_min" in combo and "prev_chg_max" in combo:
|
||||
if float(combo["prev_chg_min"]) >= float(combo["prev_chg_max"]):
|
||||
continue
|
||||
|
||||
ui_params = dict(base_fixed)
|
||||
ui_params.update(combo)
|
||||
engine_params = _ui_to_engine_params(ui_params)
|
||||
# 호가필터 임계값 → per-run 오버라이드 (kiwoom_0d 본체 재계산 시 적용)
|
||||
if "max_spread_pct" in ui_params and ui_params.get("max_spread_pct") is not None:
|
||||
engine_params["_ob_max_spread_pct"] = float(ui_params["max_spread_pct"])
|
||||
if "min_bid_ask_ratio" in ui_params and ui_params.get("min_bid_ask_ratio") is not None:
|
||||
engine_params["_ob_min_bid_ask_ratio"] = float(ui_params["min_bid_ask_ratio"])
|
||||
if "ask_wall_max_qty" in ui_params and ui_params.get("ask_wall_max_qty") is not None:
|
||||
engine_params["_ob_ask_wall_max_qty"] = float(ui_params["ask_wall_max_qty"])
|
||||
# 호가필터 ON/OFF 플래그 전달 (base_fixed → 워커, 변환에서 누락 방지 위해 명시 복사)
|
||||
if ui_params.get("_orderbook_filter_enabled") is not None:
|
||||
engine_params["_orderbook_filter_enabled"] = bool(ui_params["_orderbook_filter_enabled"])
|
||||
engine_params.update(cache_holder)
|
||||
engine_params["slot_money"] = float(slot_money)
|
||||
engine_params["max_stocks"] = int(max_stocks)
|
||||
engine_params["total_budget_krw"] = float(total_budget_krw)
|
||||
engine_params["portfolio_mode"] = True
|
||||
if shared:
|
||||
sm = shared.get("share_denom_by_code")
|
||||
if sm:
|
||||
engine_params["share_denom_by_code"] = sm
|
||||
lv = shared.get("log_verdict_by_code")
|
||||
if lv:
|
||||
engine_params["_backtest_log_verdict_by_code"] = lv
|
||||
|
||||
meta: Dict[str, Any] = {}
|
||||
trades = bbc.run_breakout_backtest_web_aligned(
|
||||
codes_candles, engine_params, universe_by_slot,
|
||||
slot_money=slot_money, fee_rate=fee_rate, sell_tax=sell_tax,
|
||||
max_stocks=max_stocks, total_budget_krw=total_budget_krw,
|
||||
result_pkg = evaluate_breakout_param_combo(
|
||||
combo,
|
||||
base_fixed=base_fixed,
|
||||
grid_keys=keys,
|
||||
codes_candles=codes_candles,
|
||||
min_trades=min_trades,
|
||||
min_win_rate=min_win_rate,
|
||||
min_pf=min_pf,
|
||||
universe_by_slot=universe_by_slot,
|
||||
slot_money=slot_money,
|
||||
max_stocks=max_stocks,
|
||||
total_budget_krw=total_budget_krw,
|
||||
fee_rate=fee_rate,
|
||||
sell_tax=sell_tax,
|
||||
period_days=period_days,
|
||||
cache_holder=cache_holder,
|
||||
ticks_by_code=ticks_preloaded,
|
||||
orderbook_by_code=orderbook_preloaded,
|
||||
program_by_code=program_preloaded,
|
||||
meta_out=meta,
|
||||
log_verdict_by_code=log_verdict_preloaded,
|
||||
share_denom_by_code=share_denom_preloaded,
|
||||
)
|
||||
stats = bbc.summarize_breakout_trades(
|
||||
trades, total_budget_krw=total_budget_krw, period_days=period_days,
|
||||
)
|
||||
total_trades = stats["total_trades"]
|
||||
if total_trades < min_trades:
|
||||
if result_pkg is None:
|
||||
continue
|
||||
|
||||
total_pnl = stats["total_pnl"]
|
||||
win_rate = stats["win_rate"]
|
||||
pf = float(stats.get("pf") or 0)
|
||||
if not combo_passes_search_filters(
|
||||
win_rate=win_rate, pf=pf,
|
||||
min_win_rate=min_win_rate, min_pf=min_pf,
|
||||
):
|
||||
continue
|
||||
avg_hold = stats["avg_hold_min"]
|
||||
peak, mdd, cum = 0.0, 0.0, 0.0
|
||||
for t in trades:
|
||||
cum += t["pnl"]
|
||||
if cum > peak:
|
||||
peak = cum
|
||||
dd = peak - cum
|
||||
if dd > mdd:
|
||||
mdd = dd
|
||||
|
||||
merged = dict(ui_params)
|
||||
merged["slot_money"] = float(slot_money)
|
||||
merged["max_stocks"] = int(max_stocks)
|
||||
merged["total_budget_krw"] = float(total_budget_krw)
|
||||
|
||||
sell_reasons = _count_sell_reasons(trades)
|
||||
avg_profit_rate = _avg_profit_rate_pct(trades)
|
||||
|
||||
result_pkg = {
|
||||
"params": {k: ui_params[k] for k in keys},
|
||||
"total_pnl": int(total_pnl),
|
||||
"win_rate": round(win_rate, 2),
|
||||
"total_trades": total_trades,
|
||||
"pf": round(pf, 2),
|
||||
"avg_hold": round(avg_hold, 1),
|
||||
"mdd": round(mdd),
|
||||
"bot_pct": stats["bot_pct"],
|
||||
"daily_avg_pct": stats["daily_avg_pct"],
|
||||
"avg_profit_rate": avg_profit_rate,
|
||||
"sell_reasons": sell_reasons,
|
||||
"skipped_micro_buys": int(
|
||||
(meta.get("skip_stats") or {}).get("skipped_micro_buys") or 0
|
||||
),
|
||||
"merged_params": merged,
|
||||
}
|
||||
total_pnl = result_pkg["total_pnl"]
|
||||
win_rate = result_pkg["win_rate"]
|
||||
item_t = (total_pnl, win_rate, id(result_pkg), result_pkg)
|
||||
if len(local_heap) < top_n:
|
||||
heapq.heappush(local_heap, item_t)
|
||||
|
||||
Reference in New Issue
Block a user