Files
kis_bot/kis_trader/engine/us_momentum_env.py
2026-07-30 18:05:07 +09:00

194 lines
7.9 KiB
Python

"""
kis_trader/engine/us_momentum_env.py — 해외 모멘텀(US_MOMENTUM_*) env 오버레이
=============================================================================
국내 MOMENTUM_* 와 엔진 키는 동일하되, 저장·실매는 US_MOMENTUM_* 만 사용.
값이 없으면 국내 기본(이미 get_momentum_defaults_from_db 로 로드된 params) 유지.
"""
from __future__ import annotations
from typing import Any, Dict, Optional
from kis_trader.utils.env import get_env_bool, get_env_float, get_env_from_db, get_env_int
# 웹 저장·ENV_CONFIG_KEYS 등록용 (TRIGGER/청산 핵심)
US_MOMENTUM_TRIGGER_EXIT_KEYS = (
"US_MOMENTUM_RSI_MIN",
"US_MOMENTUM_RSI_MAX",
"US_MOMENTUM_VOL_MULT",
"US_MOMENTUM_VOL_WIN",
"US_MOMENTUM_TIME_START",
"US_MOMENTUM_TIME_END",
"US_MOMENTUM_SELL_TIME_END",
"US_MOMENTUM_MAX_FROM_OPEN_PCT",
"US_MOMENTUM_MIN_FROM_OPEN_PCT",
"US_MOMENTUM_STOP_LOSS_PCT",
"US_MOMENTUM_TAKE_PROFIT_PCT",
"US_MOMENTUM_TP_MAX_PCT",
"US_MOMENTUM_SHOULDER_MIN_HIGH_PCT",
"US_MOMENTUM_SHOULDER_CUT_PCT",
"US_MOMENTUM_RATCHET_TIERS",
"US_MOMENTUM_TRAIL_PCT",
"US_MOMENTUM_TRAIL_ARM_PCT",
"US_MOMENTUM_MAX_HOLD_BARS",
"US_MOMENTUM_MAX_DAILY",
"US_MOMENTUM_MIN_PRICE",
"US_MOMENTUM_MAX_DAILY_CHG",
"US_MOMENTUM_HIGH_CHASE_THR",
"US_MOMENTUM_USE_DEFENSE_FILTERS",
"US_MOMENTUM_USE_HIGH_CHASE_FILTER",
"US_MOMENTUM_USE_DAILY_RANGE_FILTER",
"US_MOMENTUM_USE_EMA_FILTER",
"US_MOMENTUM_EMA_FAST_PERIOD",
"US_MOMENTUM_EMA_SLOW_PERIOD",
"US_MOMENTUM_USE_RSI_MAX_FILTER",
"US_MOMENTUM_PATTERN_BREAKOUT",
"US_MOMENTUM_PATTERN_PULLBACK",
"US_MOMENTUM_CHASE_LOOKBACK_MIN",
"US_MOMENTUM_PULLBACK_LOOKBACK_MIN",
"US_MOMENTUM_PULLBACK_MIN_PCT",
"US_MOMENTUM_PULLBACK_MAX_PCT",
"US_MOMENTUM_SETUP_VOL_MAX_MULT",
"US_MOMENTUM_SETUP_BEAR_BARS_MIN",
"US_MOMENTUM_MAX_LOSS_PER_TRADE",
"US_MOMENTUM_MIN_PROFIT_PCT",
"US_MOMENTUM_SLOT_MONEY",
"US_MOMENTUM_MAX_STOCKS",
"US_MOMENTUM_TOTAL_BUDGET",
"US_MOMENTUM_MAX_BUY_AMOUNT",
"US_MOMENTUM_COOLDOWN_SEC",
"US_MOMENTUM_EOD_ENABLED",
"US_MOMENTUM_EOD_HM",
"US_MOMENTUM_DAILY_PROFIT_TARGET_ENABLED",
"US_MOMENTUM_DAILY_PROFIT_TRAIL_TIERS",
"US_MOMENTUM_DAILY_PROFIT_MODE",
)
def _pct_ratio_from_env(key: str) -> Optional[float]:
"""DB 소수(0.015) 또는 퍼센트(1.5) → 엔진 비율. 키 없으면 None."""
raw = get_env_from_db(key, "")
if raw in (None, "", "None"):
return None
try:
x = abs(float(raw))
except (TypeError, ValueError):
return None
if x == 0:
return 0.0
return x if x < 0.5 else x / 100.0
def overlay_us_momentum_engine_params(params: Dict[str, Any]) -> Dict[str, Any]:
"""국내 defaults 위에 US_MOMENTUM_* 가 있으면 덮어씀 (실매 UsMomentumStrategy)."""
out = dict(params or {})
def set_if(key: str, env_key: str, cast=float):
raw = get_env_from_db(env_key, "")
if raw in (None, "", "None"):
return
try:
out[key] = cast(raw)
except (TypeError, ValueError):
pass
for eng, envk, cast in (
("mom_rsi_min", "US_MOMENTUM_RSI_MIN", float),
("mom_rsi_max", "US_MOMENTUM_RSI_MAX", float),
("mom_vol_mult", "US_MOMENTUM_VOL_MULT", float),
("mom_vol_win", "US_MOMENTUM_VOL_WIN", int),
("max_daily", "US_MOMENTUM_MAX_DAILY", int),
("min_price", "US_MOMENTUM_MIN_PRICE", float),
("max_daily_chg", "US_MOMENTUM_MAX_DAILY_CHG", float),
("high_chase_thr", "US_MOMENTUM_HIGH_CHASE_THR", float),
("mom_max_from_open_pct", "US_MOMENTUM_MAX_FROM_OPEN_PCT", float),
("mom_min_from_open_pct", "US_MOMENTUM_MIN_FROM_OPEN_PCT", float),
("chase_lookback_min", "US_MOMENTUM_CHASE_LOOKBACK_MIN", int),
("pullback_lookback_min", "US_MOMENTUM_PULLBACK_LOOKBACK_MIN", int),
("pullback_min_pct", "US_MOMENTUM_PULLBACK_MIN_PCT", float),
("pullback_max_pct", "US_MOMENTUM_PULLBACK_MAX_PCT", float),
("setup_vol_max_mult", "US_MOMENTUM_SETUP_VOL_MAX_MULT", float),
("setup_bear_bars_min", "US_MOMENTUM_SETUP_BEAR_BARS_MIN", int),
("ema_fast_period", "US_MOMENTUM_EMA_FAST_PERIOD", int),
("ema_slow_period", "US_MOMENTUM_EMA_SLOW_PERIOD", int),
("max_hold_bars", "US_MOMENTUM_MAX_HOLD_BARS", int),
("slot_money", "US_MOMENTUM_SLOT_MONEY", int),
("max_stocks", "US_MOMENTUM_MAX_STOCKS", int),
):
set_if(eng, envk, cast)
# 세션 HM 은 국내 defaults(900~1530)가 남아 있으면 야간 US 봉이 조용히 스킵됨.
# US_MOMENTUM_TIME_* 는 항상 강제 (DB 없으면 코드 기본 2230~500).
_ts = int(get_env_int("US_MOMENTUM_TIME_START", 2230) or 2230)
_te = int(get_env_int("US_MOMENTUM_TIME_END", 500) or 500)
out["time_start_hm"] = _ts
out["time_start"] = _ts
out["time_end_hm"] = _te
out["mom_time_end_hm"] = _te
out["mom_time_end"] = _te
for eng, envk in (
("sl_pct", "US_MOMENTUM_STOP_LOSS_PCT"),
("tp_pct", "US_MOMENTUM_TAKE_PROFIT_PCT"),
("tp_max_pct", "US_MOMENTUM_TP_MAX_PCT"),
("shoulder_min_high", "US_MOMENTUM_SHOULDER_MIN_HIGH_PCT"),
("shoulder_cut_pct", "US_MOMENTUM_SHOULDER_CUT_PCT"),
("trail_pct", "US_MOMENTUM_TRAIL_PCT"),
("trail_arm_pct", "US_MOMENTUM_TRAIL_ARM_PCT"),
):
r = _pct_ratio_from_env(envk)
if r is not None:
out[eng] = r
cd = get_env_int("US_MOMENTUM_COOLDOWN_SEC", 0)
if cd > 0:
out["cooldown_min"] = cd / 60.0
rt = get_env_from_db("US_MOMENTUM_RATCHET_TIERS", "")
if rt not in (None, "", "None"):
out["ratchet_tiers"] = str(rt).strip()
mp = get_env_float("US_MOMENTUM_MIN_PROFIT_PCT", -1.0)
if mp >= 0:
# UI는 % 숫자(0.2)로 저장하는 경우 많음 → 엔진은 그대로 쓰거나 소수
out["min_margin"] = mp / 100.0 if mp >= 0.5 else mp
for eng, envk, default in (
("use_defense_filters", "US_MOMENTUM_USE_DEFENSE_FILTERS", True),
("use_high_chase_filter", "US_MOMENTUM_USE_HIGH_CHASE_FILTER", False),
("use_daily_range_filter", "US_MOMENTUM_USE_DAILY_RANGE_FILTER", False),
("use_ema_filter", "US_MOMENTUM_USE_EMA_FILTER", True),
("use_rsi_max_filter", "US_MOMENTUM_USE_RSI_MAX_FILTER", False),
("pattern_breakout", "US_MOMENTUM_PATTERN_BREAKOUT", True),
("pattern_pullback", "US_MOMENTUM_PATTERN_PULLBACK", True),
):
raw = get_env_from_db(envk, "")
if raw not in (None, "", "None"):
out[eng] = get_env_bool(envk, default)
# 해외 고정
out["_orderbook_filter_enabled"] = False
out["_program_filter_enabled"] = False
# HTS SCAN 없음 — skip_hts 숏컷(무조건 시그널) 금지. 추격 패턴은 hts_logic US 분기.
out["skip_hts_scan_dupes"] = True
out["trigger_e_confirm"] = False
out["use_rsi_filter"] = True # V4: RSI 하한 적용 (US 추격 경로)
out["market"] = "US"
out["_session_wrap_midnight"] = True
# 국내 min_price(천원대) 잔존 시 USD 종목 전부 탈락-최소가격
out["min_price"] = float(get_env_float("US_MOMENTUM_MIN_PRICE", 1.0) or 1.0)
# 고정 유니버스 — 손절액으로 슬롯 재계산하지 않음 (DB SLOT_MONEY 고정)
out["max_loss_krw"] = 0.0
# EOD: 국내 MOMENTUM_EOD_HM(15:20) 잔존 금지 — US 전용만
out["eod_enabled"] = bool(get_env_bool("US_MOMENTUM_EOD_ENABLED", False))
_eod = str(get_env_from_db("US_MOMENTUM_EOD_HM", "05:00") or "05:00").strip() or "05:00"
# 잘못 저장된 국장대(10:00~16:59)면 해외 기본 05:00 으로
try:
_eh = int(_eod.replace(":", "")[:4]) if ":" in _eod else int(float(_eod))
except (TypeError, ValueError):
_eh = 500
if 1000 <= _eh <= 1659:
_eod = "05:00"
out["eod_hm"] = _eod
out["force_eod_exit"] = bool(out["eod_enabled"])
return out