Files
kis_bot/kis_trader/engine/momentum_env_keys.py
Hwang 61c72a8a4c feat(tests): 신규 키움 웹소켓 조건검색 및 실시간 조건검색 테스트 추가
변경 사항
----
- _test_kiwoom_condition_list.py: 키움 웹소켓 조건검색 '목록조회' 기능을 단독으로 테스트하는 스크립트 추가
- _test_kiwoom_condition_realtime.py: 'momentum' 조건식을 실시간으로 등록하고 초기 매칭 종목 리스트 및 실시간 편입/이탈을 수신하는 테스트 스크립트 추가
- _verify_columnar_bitid.py, _verify_shared_e2e_breakout.py, _verify_shared_e2e.py: 공유 메모리 및 dict 간의 데이터 일관성을 검증하는 테스트 추가

영향
----
- 신규 테스트 스크립트 추가로 키움 웹소켓 API의 기능 검증 및 안정성을 높임
- 기존 기능에 대한 영향 없음

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 01:27:00 +09:00

250 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_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