Changes: - Introduced new files for strategy definitions and study names. - Enhanced `backtest_web.py` with functions to handle integer display prices and trade data formatting. - Updated backtesting logic to incorporate end-of-day (EOD) parameters for breakout and momentum strategies. - Added EOD configuration options in the database and parameter search files. Impact: - These changes improve the modularity and usability of the backtesting framework, allowing for better integration of EOD strategies and clearer trade data presentation.
252 lines
9.4 KiB
Python
252 lines
9.4 KiB
Python
"""
|
|
kis_trader/engine/momentum_env_keys.py — 모멘텀(MOMENTUM) env 키 단일 정의
|
|
==========================================================================
|
|
1분봉 단기 추세추격 — 스캘핑(SCALP reversal)과 분리.
|
|
config_momentum · momentum_engine · 웹 · 파라서치 공통.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, Dict, Optional
|
|
|
|
# 레거시 → MOMENTUM (DB 마이그레이션·1회 승계용)
|
|
LEGACY_TO_MOMENTUM: Dict[str, str] = {
|
|
"SCALP_MOM_RSI_MIN": "MOMENTUM_RSI_MIN",
|
|
"SCALP_MOM_RSI_MAX": "MOMENTUM_RSI_MAX",
|
|
"SCALP_MOM_VOL_MULT": "MOMENTUM_VOL_MULT",
|
|
"SCALP_MOM_VOL_WIN": "MOMENTUM_VOL_WIN",
|
|
"SCALP_MOM_TIME_END_HM": "MOMENTUM_TIME_END_HM",
|
|
"SCALP_STOP_LOSS_PCT": "MOMENTUM_STOP_LOSS_PCT",
|
|
"SCALP_TAKE_PROFIT_PCT": "MOMENTUM_TAKE_PROFIT_PCT",
|
|
"SCALP_TP_MAX_PCT": "MOMENTUM_TP_MAX_PCT",
|
|
"SCALP_SHOULDER_MIN_HIGH_PCT": "MOMENTUM_SHOULDER_MIN_HIGH_PCT",
|
|
"SCALP_SHOULDER_CUT_PCT": "MOMENTUM_SHOULDER_CUT_PCT",
|
|
"SCALP_MAX_DAILY": "MOMENTUM_MAX_DAILY",
|
|
"SCALP_MIN_DROP_PCT_FOR_LOSS_CUT": "MOMENTUM_MIN_DROP_PCT_FOR_LOSS_CUT",
|
|
"SCALP_MIN_PROFIT_PCT": "MOMENTUM_MIN_PROFIT_PCT",
|
|
"SCALP_MIN_HOLD_SEC": "MOMENTUM_MIN_HOLD_SEC",
|
|
"MAX_LOSS_PER_TRADE_KRW": "MOMENTUM_MAX_LOSS_PER_TRADE_KRW",
|
|
"HIGH_CHASE_THR": "MOMENTUM_HIGH_CHASE_THR",
|
|
"MAX_DAILY_CHG": "MOMENTUM_MAX_DAILY_CHG",
|
|
"MIN_STOCK_PRICE": "MOMENTUM_MIN_PRICE",
|
|
"SLOT_MONEY_DEFAULT": "MOMENTUM_SLOT_MONEY",
|
|
}
|
|
|
|
MOMENTUM_CONFIG_KEYS = frozenset({
|
|
"MOMENTUM_RSI_MIN",
|
|
"MOMENTUM_RSI_MAX",
|
|
"MOMENTUM_VOL_MULT",
|
|
"MOMENTUM_VOL_WIN",
|
|
"MOMENTUM_TIME_END_HM",
|
|
"MOMENTUM_TIME_START",
|
|
"MOMENTUM_MAX_FROM_OPEN_PCT",
|
|
"MOMENTUM_MIN_FROM_OPEN_PCT",
|
|
"MOMENTUM_SKIP_HTS_SCAN_DUPES",
|
|
"MOMENTUM_STOP_LOSS_PCT",
|
|
"MOMENTUM_TAKE_PROFIT_PCT",
|
|
"MOMENTUM_TP_MAX_PCT",
|
|
"MOMENTUM_SHOULDER_MIN_HIGH_PCT",
|
|
"MOMENTUM_SHOULDER_CUT_PCT",
|
|
"MOMENTUM_RATCHET_TIERS",
|
|
"MOMENTUM_TRAIL_PCT",
|
|
"MOMENTUM_TRAIL_ARM_PCT",
|
|
"MOMENTUM_MAX_HOLD_BARS",
|
|
"MOMENTUM_MAX_DAILY",
|
|
"MOMENTUM_MIN_PRICE",
|
|
"MOMENTUM_MAX_DAILY_CHG",
|
|
"MOMENTUM_HIGH_CHASE_THR",
|
|
"MOMENTUM_USE_DEFENSE_FILTERS",
|
|
"MOMENTUM_USE_HIGH_CHASE_FILTER",
|
|
"MOMENTUM_USE_DAILY_RANGE_FILTER",
|
|
"MOMENTUM_USE_EMA_FILTER",
|
|
"MOMENTUM_EMA_FAST_PERIOD",
|
|
"MOMENTUM_EMA_SLOW_PERIOD",
|
|
"MOMENTUM_USE_RSI_MAX_FILTER",
|
|
"MOMENTUM_PATTERN_BREAKOUT",
|
|
"MOMENTUM_PATTERN_PULLBACK",
|
|
"MOMENTUM_CHASE_LOOKBACK_MIN",
|
|
"MOMENTUM_PULLBACK_LOOKBACK_MIN",
|
|
"MOMENTUM_PULLBACK_MIN_PCT",
|
|
"MOMENTUM_PULLBACK_MAX_PCT",
|
|
"MOMENTUM_SETUP_VOL_MAX_MULT",
|
|
"MOMENTUM_SETUP_BEAR_BARS_MIN",
|
|
"MOMENTUM_MAX_LOSS_PER_TRADE_KRW",
|
|
"MOMENTUM_MIN_DROP_PCT_FOR_LOSS_CUT",
|
|
"MOMENTUM_MIN_PROFIT_PCT",
|
|
"MOMENTUM_MIN_HOLD_SEC",
|
|
"MOMENTUM_SLOT_MONEY",
|
|
"MOMENTUM_MAX_STOCKS",
|
|
"MOMENTUM_TOTAL_BUDGET_KRW",
|
|
"MOMENTUM_MAX_BUY_AMOUNT",
|
|
"MOMENTUM_RSI_PERIOD",
|
|
"MOMENTUM_COOLDOWN_SEC",
|
|
"MOMENTUM_LIVE_BACKTEST_ALIGN",
|
|
"MOMENTUM_BACKTEST_USE_TICK_EXIT",
|
|
"MOMENTUM_BACKTEST_USE_TICK_ENTRY",
|
|
"MOMENTUM_BACKTEST_TICK_FALLBACK_OHLC",
|
|
"MOMENTUM_BACKTEST_POLL_MS",
|
|
"MOMENTUM_BACKTEST_SELL_SLIP_PCT",
|
|
"MOMENTUM_BACKTEST_BUY_SLIP_PCT",
|
|
"MOMENTUM_BACKTEST_UNIVERSE_STRICT",
|
|
"MOMENTUM_BACKTEST_UNIVERSE_STRICT_LAG_MIN",
|
|
"MOMENTUM_BACKTEST_LIVE_SCAN_QUEUE",
|
|
"MOMENTUM_BACKTEST_SCAN_SEC",
|
|
"MOMENTUM_BACKTEST_UNIVERSE_SCAN_AT",
|
|
"MOMENTUM_BACKTEST_CANDLE_WARMUP_BARS",
|
|
"MOMENTUM_UNIVERSE_EXIT_DEBOUNCE_SEC",
|
|
"MOMENTUM_LIVE_SIGNAL_LOOKBACK_BARS",
|
|
"MOMENTUM_EOD_ENABLED",
|
|
"MOMENTUM_EOD_HM",
|
|
"MOMENTUM_FORCE_EOD_EXIT",
|
|
})
|
|
|
|
|
|
def _row_val(row: Dict[str, Any], key: str, default: Any = None) -> Any:
|
|
v = row.get(key)
|
|
if v not in (None, "", "None"):
|
|
return v
|
|
return default
|
|
|
|
|
|
def momentum_env_float(row: Dict[str, Any], key: str, default: float) -> float:
|
|
v = _row_val(row, key)
|
|
if v is None:
|
|
return float(default)
|
|
try:
|
|
return float(v)
|
|
except (TypeError, ValueError):
|
|
return float(default)
|
|
|
|
|
|
def momentum_env_int(row: Dict[str, Any], key: str, default: int) -> int:
|
|
v = _row_val(row, key)
|
|
if v is None:
|
|
return int(default)
|
|
try:
|
|
return int(float(v))
|
|
except (TypeError, ValueError):
|
|
return int(default)
|
|
|
|
|
|
def momentum_env_bool(row: Dict[str, Any], key: str, default: bool) -> bool:
|
|
v = _row_val(row, key)
|
|
if v is None:
|
|
return default
|
|
s = str(v).strip().lower()
|
|
if s in ("1", "true", "t", "y", "yes", "on"):
|
|
return True
|
|
if s in ("0", "false", "f", "n", "no", "off", ""):
|
|
return False
|
|
return default
|
|
|
|
|
|
def _legacy_float(row: Dict[str, Any], primary: str, legacy_keys: tuple, default: float) -> float:
|
|
v = _row_val(row, primary)
|
|
if v is not None:
|
|
try:
|
|
return float(v)
|
|
except (TypeError, ValueError):
|
|
pass
|
|
for lk in legacy_keys:
|
|
v = _row_val(row, lk)
|
|
if v is not None:
|
|
try:
|
|
return float(v)
|
|
except (TypeError, ValueError):
|
|
continue
|
|
return float(default)
|
|
|
|
|
|
def params_to_momentum_env_patch(p: Dict[str, Any]) -> Dict[str, str]:
|
|
"""파라서치·웹 params → MOMENTUM env 패치 (문자열)."""
|
|
patch: Dict[str, str] = {}
|
|
if not p:
|
|
return patch
|
|
|
|
def _set(k: str, v: Any) -> None:
|
|
if v is not None and v != "":
|
|
patch[k] = str(v)
|
|
|
|
if "mom_rsi_min" in p:
|
|
_set("MOMENTUM_RSI_MIN", p["mom_rsi_min"])
|
|
if "mom_rsi_max" in p:
|
|
_set("MOMENTUM_RSI_MAX", p["mom_rsi_max"])
|
|
if "mom_vol_mult" in p:
|
|
_set("MOMENTUM_VOL_MULT", p["mom_vol_mult"])
|
|
if "mom_vol_win" in p:
|
|
_set("MOMENTUM_VOL_WIN", p["mom_vol_win"])
|
|
if "mom_time_end_hm" in p:
|
|
_set("MOMENTUM_TIME_END_HM", int(float(p["mom_time_end_hm"])))
|
|
if "mom_max_from_open_pct" in p:
|
|
_set("MOMENTUM_MAX_FROM_OPEN_PCT", p["mom_max_from_open_pct"])
|
|
if "mom_min_from_open_pct" in p:
|
|
_set("MOMENTUM_MIN_FROM_OPEN_PCT", p["mom_min_from_open_pct"])
|
|
if "sl_pct" in p:
|
|
_set("MOMENTUM_STOP_LOSS_PCT", -abs(float(p["sl_pct"])))
|
|
if "tp_pct" in p:
|
|
_set("MOMENTUM_TAKE_PROFIT_PCT", p["tp_pct"])
|
|
if "tp_max_pct" in p:
|
|
_set("MOMENTUM_TP_MAX_PCT", p["tp_max_pct"])
|
|
if "shoulder_min_high" in p:
|
|
_set("MOMENTUM_SHOULDER_MIN_HIGH_PCT", p["shoulder_min_high"])
|
|
if "shoulder_cut_pct" in p:
|
|
_set("MOMENTUM_SHOULDER_CUT_PCT", p["shoulder_cut_pct"])
|
|
if "ratchet_tiers" in p:
|
|
patch["MOMENTUM_RATCHET_TIERS"] = str(p.get("ratchet_tiers") or "").strip()
|
|
if "trail_pct" in p:
|
|
_set("MOMENTUM_TRAIL_PCT", abs(float(p["trail_pct"])))
|
|
if "trail_arm_pct" in p:
|
|
_set("MOMENTUM_TRAIL_ARM_PCT", abs(float(p["trail_arm_pct"])))
|
|
if "max_hold_bars" in p and p["max_hold_bars"] is not None:
|
|
_set("MOMENTUM_MAX_HOLD_BARS", int(float(p["max_hold_bars"])))
|
|
if "max_daily" in p:
|
|
_set("MOMENTUM_MAX_DAILY", int(float(p["max_daily"])))
|
|
if "min_price" in p:
|
|
_set("MOMENTUM_MIN_PRICE", p["min_price"])
|
|
if "max_daily_chg" in p:
|
|
_set("MOMENTUM_MAX_DAILY_CHG", p["max_daily_chg"])
|
|
if "high_chase_thr" in p and p["high_chase_thr"] is not None:
|
|
_set("MOMENTUM_HIGH_CHASE_THR", float(p["high_chase_thr"]))
|
|
if "max_loss_krw" in p:
|
|
_set("MOMENTUM_MAX_LOSS_PER_TRADE_KRW", int(float(p["max_loss_krw"])))
|
|
if "cooldown_min" in p and p["cooldown_min"] is not None:
|
|
_set("MOMENTUM_COOLDOWN_SEC", int(float(p["cooldown_min"]) * 60))
|
|
if "time_start_hm" in p:
|
|
_set("MOMENTUM_TIME_START", int(float(p["time_start_hm"])))
|
|
if "time_end_hm" in p:
|
|
_set("MOMENTUM_TIME_END", int(float(p["time_end_hm"])))
|
|
for js_k, env_k in (
|
|
("skip_hts_scan_dupes", "MOMENTUM_SKIP_HTS_SCAN_DUPES"),
|
|
("use_defense_filters", "MOMENTUM_USE_DEFENSE_FILTERS"),
|
|
("use_high_chase_filter", "MOMENTUM_USE_HIGH_CHASE_FILTER"),
|
|
("use_daily_range_filter", "MOMENTUM_USE_DAILY_RANGE_FILTER"),
|
|
("use_ema_filter", "MOMENTUM_USE_EMA_FILTER"),
|
|
("use_rsi_max_filter", "MOMENTUM_USE_RSI_MAX_FILTER"),
|
|
("pattern_breakout", "MOMENTUM_PATTERN_BREAKOUT"),
|
|
("pattern_pullback", "MOMENTUM_PATTERN_PULLBACK"),
|
|
):
|
|
if js_k in p:
|
|
_set(env_k, str(p[js_k]).lower())
|
|
if "chase_lookback_min" in p:
|
|
_set("MOMENTUM_CHASE_LOOKBACK_MIN", int(float(p["chase_lookback_min"])))
|
|
if "pullback_lookback_min" in p:
|
|
_set("MOMENTUM_PULLBACK_LOOKBACK_MIN", int(float(p["pullback_lookback_min"])))
|
|
if "pullback_min_pct" in p:
|
|
_set("MOMENTUM_PULLBACK_MIN_PCT", float(p["pullback_min_pct"]))
|
|
if "pullback_max_pct" in p:
|
|
_set("MOMENTUM_PULLBACK_MAX_PCT", float(p["pullback_max_pct"]))
|
|
if "setup_vol_max_mult" in p:
|
|
_set("MOMENTUM_SETUP_VOL_MAX_MULT", float(p["setup_vol_max_mult"]))
|
|
if "setup_bear_bars_min" in p:
|
|
_set("MOMENTUM_SETUP_BEAR_BARS_MIN", int(float(p["setup_bear_bars_min"])))
|
|
if "ema_fast_period" in p:
|
|
_set("MOMENTUM_EMA_FAST_PERIOD", int(float(p["ema_fast_period"])))
|
|
if "ema_slow_period" in p:
|
|
_set("MOMENTUM_EMA_SLOW_PERIOD", int(float(p["ema_slow_period"])))
|
|
if "slot_money" in p:
|
|
_set("MOMENTUM_SLOT_MONEY", int(float(p["slot_money"])))
|
|
if "max_stocks" in p:
|
|
_set("MOMENTUM_MAX_STOCKS", int(float(p["max_stocks"])))
|
|
if "total_budget_krw" in p:
|
|
_set("MOMENTUM_TOTAL_BUDGET_KRW", int(float(p["total_budget_krw"])))
|
|
return patch
|