feat: Enhance trading system with new e_min_chg_pct parameter and related logic

Changes:
- Introduced the `e_min_chg_pct` parameter to define the minimum price change percentage compared to the previous day's close, enhancing the momentum trading strategy.
- Updated various functions and classes to incorporate this new parameter, ensuring it is utilized in both backtesting and live trading scenarios.
- Improved documentation and comments to clarify the purpose and usage of the new parameter across the codebase.

Impact:
- This addition allows for more precise control over trading conditions, potentially increasing the effectiveness of the momentum strategy while maintaining system integrity and performance.
This commit is contained in:
Your Name
2026-08-01 16:19:24 +09:00
parent 7050f788c5
commit cb7e5037a0
30 changed files with 2206 additions and 253 deletions

View File

@@ -121,6 +121,8 @@ def get_momentum_defaults_from_db(db=None, *, env_row: Optional[Dict[str, Any]]
mom_vol_mult = momentum_env_float(r, "MOMENTUM_VOL_MULT", 1.05)
mom_vol_win = momentum_env_int(r, "MOMENTUM_VOL_WIN", 5)
mom_time_end = momentum_env_int(r, "MOMENTUM_TIME_END_HM", 1530)
# HTS K: 전일 종가 대비 최소 등락률(%). 0=단순 종가 초과만. HTS 기본 0.2.
e_min_chg_pct = momentum_env_float(r, "MOMENTUM_E_MIN_CHG_PCT", 0.2)
sl_pct = abs(_legacy_float(
r, "MOMENTUM_STOP_LOSS_PCT", ("SCALP_STOP_LOSS_PCT",), 0.03,
))
@@ -184,6 +186,7 @@ def get_momentum_defaults_from_db(db=None, *, env_row: Optional[Dict[str, Any]]
except Exception:
rsi_period, mom_rsi_min, mom_rsi_max = 3, 50.0, 80.0
mom_vol_mult, mom_vol_win, mom_time_end = 1.2, 5, 1530
e_min_chg_pct = 0.2
sl_pct, tp_pct, tp_max = 0.03, 0.08, 0.10
shoulder_high, shoulder_cut = 0.005, 0.003
ratchet_tiers, trail_pct, trail_arm_pct = "", 0.02, 0.01
@@ -216,6 +219,7 @@ def get_momentum_defaults_from_db(db=None, *, env_row: Optional[Dict[str, Any]]
"mom_vol_mult": mom_vol_mult,
"mom_vol_win": mom_vol_win,
"mom_time_end_hm": mom_time_end,
"e_min_chg_pct": e_min_chg_pct,
"mom_max_from_open_pct": mom_max_open,
"mom_min_from_open_pct": mom_min_open,
"sl_pct": sl_pct,

View File

@@ -42,6 +42,7 @@ MOMENTUM_CONFIG_KEYS = frozenset({
"MOMENTUM_TRIGGER_REQUIRE_BULL_BAR",
"MOMENTUM_USE_VOL_TRIGGER",
"MOMENTUM_USE_RSI_FILTER",
"MOMENTUM_E_MIN_CHG_PCT",
"MOMENTUM_PATTERN_BREAKOUT",
"MOMENTUM_PATTERN_PULLBACK",
"MOMENTUM_CHASE_LOOKBACK_MIN",
@@ -169,6 +170,8 @@ def params_to_momentum_env_patch(p: Dict[str, Any]) -> Dict[str, str]:
_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:

View File

@@ -4,14 +4,12 @@ momentum_hts_logic.py — HTS momentum 조건식(E∧F∧H∧I) 정합 TRIGGER·
====================================================================
[역할 분담 — SCAN vs TRIGGER vs 청산]
- SCAN (키움 WS ``momentum`` 조건검색): E∧F∧H∧I 일봉 필터 → target_candidates_history
· E: 종가 > 전일 시가
· F: 2천~20만원
· H: 5일 평균거래량 105% 이상
· I: 거래량증감률 상위 400
- SCAN (HTS ``momentum``): F∧H∧I∧K 등 → 후보 풀
· F: 가격대 · H: 1분 거래량 펄스 · I: 거래량증감 상위(SCAN만)
· K: 전일 종가 대비 최소 등락(예 0.2%) — TRIGGER ``e_min_chg_pct`` 와 대응
- TRIGGER (본 모듈): SCAN 후 **진입 타이밍**만 검사
· ``MOMENTUM_SKIP_HTS_SCAN_DUPES=true`` (kiwoom 기본): E·양봉·분봉거래량·RSI 중복 생략
· false: E 유지·양봉·거래량 펄스(선택) — 당일 모멘텀 살아있음 확인
· ``MOMENTUM_SKIP_HTS_SCAN_DUPES=true``: K·양봉·분봉거래량 중복 생략
· false: 전일종가+e_min_chg · 양봉 · 거래량 펄스(선택)
- 청산 (본 모듈): 래칫·어깨·트레일·손절·시간컷 (A안 돌파 추격)
"""
from __future__ import annotations
@@ -45,7 +43,7 @@ def resolve_prev_trading_day_open(
day: str,
) -> Optional[float]:
"""
전일(직전 거래일) 시가 — HTS momentum E 조건 ``close > prev_open`` 확인용.
전일(직전 거래일) 시가 — 레거시 참고용 (현재 E 조건은 종가 사용).
1분봉에서 전일 **장 시작 구간** 첫 봉 open = 일봉 시가.
전일 오후 봉만 있으면(웜업 부족) 오후 open을 시가로 오인하므로 None 반환.
@@ -54,7 +52,6 @@ def resolve_prev_trading_day_open(
# 전일 시가로 인정할 최대 HHMM.
# 기본 1000: 저유동 종목이 09:14 첫체결만 있어도 시가로 인정.
# (구 910은 월요 갭 후 전일 오전 봉이 얇으면 전부 '전일시가없음'으로 매수 불가)
open_hm_max = max(900, int(get_env_int("MOMENTUM_PREV_DAY_OPEN_HM_MAX", 1000)))
prev_day: Optional[str] = None
@@ -83,17 +80,44 @@ def resolve_prev_trading_day_open(
earliest_hm = hm
if prev_open is None or prev_open <= 0:
return None
# 전일 시가 = 정규장 시작 근처 봉이 시리즈에 있어야 함 (오후만 있으면 미확정)
if earliest_hm is None or earliest_hm > open_hm_max:
return None
return prev_open
def resolve_prev_trading_day_close(
candles: List[Dict],
i: int,
day: str,
) -> Optional[float]:
"""
전일(직전 거래일) 종가 — HTS momentum E 조건 ``close > prev_close`` 확인용.
1분봉 역스캔에서 전일(day 이전) 마지막 봉 close = 일봉 종가.
전일 봉이 1개도 없으면 None 반환.
"""
prev_day: Optional[str] = None
prev_close: Optional[float] = None
for j in range(i - 1, -1, -1):
ct = str(candles[j].get("candle_time", ""))
d = ct[:8]
if not d or d >= day:
continue
if prev_day is None:
prev_day = d
# 역스캔 첫 번째로 만난 전일 봉 = 전일 마지막(최신) 봉 → 종가
cl = float(candles[j].get("close", 0) or 0)
if cl > 0:
prev_close = cl
break
return prev_close if (prev_close is not None and prev_close > 0) else None
def candles_have_prev_session_open(
candles: List[Dict],
day: str,
) -> bool:
"""기간일 ``day`` 기준 직전 거래일 **장시작 시가** 봉이 있는지 (E조건 해석 가능)."""
"""기간일 ``day`` 기준 직전 거래일 **장시작 시가** 봉이 있는지 (레거시 호환)."""
if not candles:
return False
d = str(day or "")[:8]
@@ -102,6 +126,19 @@ def candles_have_prev_session_open(
return resolve_prev_trading_day_open(candles, len(candles) - 1, d) is not None
def candles_have_prev_session_close(
candles: List[Dict],
day: str,
) -> bool:
"""기간일 ``day`` 기준 직전 거래일 **종가** 봉이 있는지 (E 조건 해석 가능)."""
if not candles:
return False
d = str(day or "")[:8]
if len(d) < 8:
return True
return resolve_prev_trading_day_close(candles, len(candles) - 1, d) is not None
def _volume_pulse_ok(
candles: List[Dict],
i: int,
@@ -285,7 +322,7 @@ def eval_momentum_hts_buy_at_index(
return (None, None, None)
skip_hts = _to_bool(params.get("skip_hts_scan_dupes"), False)
prev_open = resolve_prev_trading_day_open(candles, i, day)
prev_close = resolve_prev_trading_day_close(candles, i, day)
# kiwoom momentum SCAN — E∧F∧H∧I 이미 통과, TRIGGER 는 타이밍·호가·휩쏘만
# ※ 해외(US)는 HTS SCAN 이 없으므로 skip_hts 숏컷으로 매 봉 시그널 내면 안 됨
@@ -322,7 +359,7 @@ def eval_momentum_hts_buy_at_index(
"mode": "us_momentum_chase",
"pattern": pat_name or "chase",
"signal_candle_time": c.get("candle_time"),
"prev_day_open": prev_open,
"prev_day_close": prev_close,
}
if metrics:
sig_hts.update(metrics)
@@ -335,7 +372,7 @@ def eval_momentum_hts_buy_at_index(
"mode": "momentum_hts",
"pattern": "momentum_hts_scan",
"signal_candle_time": c.get("candle_time"),
"prev_day_open": prev_open,
"prev_day_close": prev_close,
}
ws_rej, ws_msg = whipsaw_reject_for_signal(
params, "MOMENTUM", signal_bar=c, current_price=cl,
@@ -350,14 +387,19 @@ def eval_momentum_hts_buy_at_index(
return (prog_rej, prog_msg, None)
return (None, None, sig_hts)
# ── E: 전일 돌파 유지 (HTS momentum 핵심) ─────────────────────
# ── E: 전일 대비 최소 등락률 이상 (HTS K 조건 대응)
# e_min_chg_pct > 0 이면 "전일 종가 + X%" 이상이어야 진입 (HTS 0.2% = 0.2 입력).
# 0.0 = 전일 종가 초과만 확인 (기존 동작).
e_min_chg_pct = float(params.get("e_min_chg_pct", 0.0))
if trigger_e_confirm:
if prev_open is None or prev_open <= 0:
return ("탈락-전일가없음", "전일 가 미확인", None)
if cl <= prev_open:
if prev_close is None or prev_close <= 0:
return ("탈락-전일가없음", "전일 가 미확인", None)
threshold = prev_close * (1.0 + e_min_chg_pct / 100.0)
if cl < threshold:
chg_pct = (cl - prev_close) / prev_close * 100.0
return (
"탈락-E미충족",
"종가 %.0f 전일시가 %.0f" % (cl, prev_open),
"등락 %.2f%% < %.1f%% (종가%.0f 전일종가%.0f)" % (chg_pct, e_min_chg_pct, cl, prev_close),
None,
)
@@ -407,7 +449,7 @@ def eval_momentum_hts_buy_at_index(
"mode": "momentum_hts",
"pattern": "hts_e_confirm",
"signal_candle_time": c.get("candle_time"),
"prev_day_open": prev_open,
"prev_day_close": prev_close,
}
if rsi_val is not None:
sig["rsi"] = rsi_val