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:
2026-07-06 02:18:14 +09:00
parent 94a7608f6f
commit 336d637b72
9 changed files with 2193 additions and 235 deletions

View File

@@ -888,6 +888,111 @@ def _ui_to_engine_params(ui_params: dict) -> dict:
return engine_params
def evaluate_momentum_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 = 3_000_000.0,
max_stocks: int = 3,
total_budget_krw: float = 9_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,
start_key: str = "",
end_key: str = "",
) -> Optional[Dict[str, Any]]:
"""단일 모멘텀 조합 백테 — Grid 워커·Optuna objective 공통."""
if not _momentum_combo_grid_valid(combo):
return None
ui_params = dict(base_fixed)
ui_params.update(combo)
engine_params = _ui_to_engine_params(ui_params)
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 log_verdict_by_code:
engine_params["_backtest_log_verdict_by_code"] = log_verdict_by_code
meta: Dict[str, Any] = {}
if len(start_key) >= 12:
meta["start_key"] = start_key
engine_params["_backtest_period_start_key"] = start_key[:12]
if len(end_key) >= 12:
meta["end_key"] = end_key
trades = mbc.run_momentum_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 = mbc.summarize_momentum_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"],
"sell_reasons": mbc.count_momentum_sell_reasons(trades),
"skipped_micro_buys": int(
(meta.get("skip_stats") or {}).get("skipped_micro_buys") or 0
),
"merged_params": merged,
}
def _evaluate_momentum_chunk(
param_chunk: List[Dict[str, Any]],
base_fixed: Dict[str, Any],
@@ -910,6 +1015,7 @@ def _evaluate_momentum_chunk(
orderbook_preloaded = None
program_preloaded = None
ticks_preloaded = None
log_verdict_preloaded = None
if shared:
if codes_candles is None:
codes_candles = shared.get("codes_candles") or {}
@@ -938,89 +1044,34 @@ def _evaluate_momentum_chunk(
local_heap: List[Tuple[float, float, int, Dict]] = []
for combo in param_chunk:
assert_parent_alive()
if not _momentum_combo_grid_valid(combo):
continue
ui_params = dict(base_fixed)
ui_params.update(combo)
engine_params = _ui_to_engine_params(ui_params)
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:
lv = shared.get("log_verdict_by_code")
if lv:
engine_params["_backtest_log_verdict_by_code"] = lv
meta: Dict[str, Any] = {}
if shared:
sk = str(shared.get("start_key") or "")
if len(sk) >= 12:
meta["start_key"] = sk
engine_params["_backtest_period_start_key"] = sk[:12]
# 웹 백테와 동일: end_key 있어야 scan_at 유니버스 타임라인 부착
ek = str(shared.get("end_key") or "")
if len(ek) >= 12:
meta["end_key"] = ek
trades = mbc.run_momentum_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_momentum_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,
start_key=str(shared.get("start_key") or "") if shared else "",
end_key=str(shared.get("end_key") or "") if shared else "",
)
stats = mbc.summarize_momentum_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)
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"],
"sell_reasons": mbc.count_momentum_sell_reasons(trades),
"skipped_micro_buys": int(
(meta.get("skip_stats") or {}).get("skipped_micro_buys") or 0
),
"merged_params": merged,
}
# 청크 내 상위 top_n (총손익 기준): root = 현재 보관 중 최저 손익 → heapreplace 로만 교체
# (과거 min-heap + pushpop 버그로 워커·머지 단계에서 손익/승률이 뒤바뀌어 순위가 무너짐)
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)