""" 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_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_TRIGGER_E_CONFIRM", "MOMENTUM_TRIGGER_REQUIRE_BULL_BAR", "MOMENTUM_USE_VOL_TRIGGER", "MOMENTUM_USE_RSI_FILTER", "MOMENTUM_E_MIN_CHG_PCT", "MOMENTUM_EXIT_OB_ENABLED", "MOMENTUM_EXIT_OB_RATIO_MIN", "MOMENTUM_EXIT_OB_MA_WINDOW", "MOMENTUM_EXIT_OB_MIN_PROFIT_PCT", "MOMENTUM_EXIT_OB_MIN_HOLD_BARS", "MOMENTUM_STOP_OB_ENABLED", "MOMENTUM_STOP_OB_RATIO_MIN", "MOMENTUM_STOP_OB_MA_WINDOW", "MOMENTUM_STOP_OB_MIN_LOSS_PCT", "MOMENTUM_STOP_OB_MIN_HOLD_BARS", "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_LIVE_ALIGN_USE_FORMING_BAR", "MOMENTUM_COOLDOWN_USE_CANDLE_FLOOR", "MOMENTUM_COOLDOWN_ENGINE_ONLY", "MOMENTUM_BACKTEST_SKIP_PRE_SUBSCRIBE", "MOMENTUM_BACKTEST_USE_TICK_EXIT", "MOMENTUM_BACKTEST_USE_TICK_ENTRY", "MOMENTUM_BACKTEST_TICK_FALLBACK_OHLC", "MOMENTUM_BACKTEST_WALLCLOCK_LAST_PRICE", "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_BACKTEST_REST_WARMUP", "MOMENTUM_BACKTEST_REST_WARMUP_BARS", "MOMENTUM_BACKTEST_REST_WARMUP_BARS_RETRY", "MOMENTUM_BACKTEST_REST_MAX_CODES", "MOMENTUM_BACKTEST_REST_SLEEP_SEC", "MOMENTUM_BACKTEST_REST_KIWOOM_MOCK", "MOMENTUM_PREV_DAY_OPEN_HM_MAX", "MOMENTUM_UNIVERSE_EXIT_DEBOUNCE_SEC", "MOMENTUM_LIVE_SIGNAL_LOOKBACK_BARS", "MOMENTUM_LIVE_MIN_CANDLES", "MOMENTUM_GAP_FILL_LIMIT", "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 "e_min_chg_pct" in p: _set("MOMENTUM_E_MIN_CHG_PCT", p["e_min_chg_pct"]) 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"]))) if "exit_ob_enabled" in p: _set("MOMENTUM_EXIT_OB_ENABLED", str(p["exit_ob_enabled"]).lower()) if "exit_ob_ratio_min" in p and p.get("exit_ob_ratio_min") not in (None, ""): _set("MOMENTUM_EXIT_OB_RATIO_MIN", float(p["exit_ob_ratio_min"])) if "exit_ob_ma_window" in p and p.get("exit_ob_ma_window") not in (None, ""): _set("MOMENTUM_EXIT_OB_MA_WINDOW", int(float(p["exit_ob_ma_window"]))) if "exit_ob_min_profit_pct" in p and p.get("exit_ob_min_profit_pct") not in (None, ""): _set("MOMENTUM_EXIT_OB_MIN_PROFIT_PCT", float(p["exit_ob_min_profit_pct"])) if "exit_ob_min_hold_bars" in p and p.get("exit_ob_min_hold_bars") not in (None, ""): _set("MOMENTUM_EXIT_OB_MIN_HOLD_BARS", int(float(p["exit_ob_min_hold_bars"]))) if "stop_ob_enabled" in p: _set("MOMENTUM_STOP_OB_ENABLED", str(p["stop_ob_enabled"]).lower()) if "stop_ob_ratio_min" in p and p.get("stop_ob_ratio_min") not in (None, ""): _set("MOMENTUM_STOP_OB_RATIO_MIN", float(p["stop_ob_ratio_min"])) if "stop_ob_ma_window" in p and p.get("stop_ob_ma_window") not in (None, ""): _set("MOMENTUM_STOP_OB_MA_WINDOW", int(float(p["stop_ob_ma_window"]))) if "stop_ob_min_loss_pct" in p and p.get("stop_ob_min_loss_pct") not in (None, ""): _set("MOMENTUM_STOP_OB_MIN_LOSS_PCT", float(p["stop_ob_min_loss_pct"])) if "stop_ob_min_hold_bars" in p and p.get("stop_ob_min_hold_bars") not in (None, ""): _set("MOMENTUM_STOP_OB_MIN_HOLD_BARS", int(float(p["stop_ob_min_hold_bars"]))) return patch