feat: 새로운 안전 규칙 및 최적화 적용을 통한 트레이딩 시스템 개선
변경 사항 (Changes): 구문 오류(Syntax error) 및 토큰 낭비를 방지하기 위해 에이전트 쉘(Agent shell)과 파이썬 코드 스니펫에 다수의 신규 안전 규칙(Safety rules)을 추가함. 스키마 검증 및 적절한 SQL 포맷팅을 보장하기 위해 임시(Ad-hoc) 데이터베이스 쿼리 작성 가이드라인을 도입함. 코드 수정 후 UI 기능이 정상 작동하는지 확인하기 위해, 백테스트 웹 서비스 재시작 및 브라우저 검증에 대한 새로운 규칙을 구현함. 시스템 전반의 무결성(Integrity)을 유지하기 위해 실전 매매(Live trading), 웹 백테스팅, 파라미터 탐색(Parameter searches) 간의 일관성 검사(Consistency checks) 체계를 확립함. 기대 효과 (Impact): 이러한 개선 사항들은 트레이딩 시스템의 견고성(Robustness)과 신뢰성을 향상시키며, 에러 발생을 최소화하고 다양한 시스템 컴포넌트 간의 원활한 상호작용을 보장함.
This commit is contained in:
@@ -1,15 +1,16 @@
|
||||
"""
|
||||
kis_trader/strategies/momentum.py — 모멘텀 전략 (1분봉 추세추격)
|
||||
kis_trader/strategies/momentum.py — MOMENTUM A안 (HTS momentum E∧F∧H∧I 돌파·주도주 추격)
|
||||
================================================================
|
||||
스캘핑 reversal(SCALP)과 완전 분리 — ``momentum_engine`` 전용.
|
||||
|
||||
[SCAN vs TRIGGER]
|
||||
- SCAN: HTS/KIS ``scalp`` 조건검색 → target_candidates_history
|
||||
- TRIGGER: 양봉, RSI 강세, 거래량 spike, 방어필터 (고점추격·급등·시가위치)
|
||||
[SCAN vs TRIGGER vs 청산]
|
||||
- SCAN: 키움 ``momentum`` 조건검색 → target_candidates_history
|
||||
- TRIGGER: ``MOMENTUM_SKIP_HTS_SCAN_DUPES=true`` (kiwoom 기본) 시 E·양봉·거래량 중복 생략, 진입 타이밍만
|
||||
- 청산: 래칫·어깨·트레일·손절·시간컷 (``momentum_hts_logic``)
|
||||
|
||||
[엔진]
|
||||
- 진입: ``momentum_engine.check_buy_signal_momentum_live``
|
||||
- 청산: ``momentum_engine.check_sell_signal_momentum_live`` (어깨·트레일 선행, tp_max 상한 익절 마지막)
|
||||
- 청산: ``momentum_engine.check_sell_signal_momentum_live``
|
||||
- 백테: ``momentum_engine.run_momentum_backtest`` / ``check_sell_signal_momentum_backtest_bar``
|
||||
"""
|
||||
from __future__ import annotations
|
||||
@@ -54,7 +55,7 @@ class MomentumStrategy(BaseStrategy):
|
||||
self.mom_rsi_max = float(base.get("mom_rsi_max", 80.0))
|
||||
self.max_daily = int(base.get("max_daily", 5))
|
||||
self.eod_enabled = get_env_bool("MOMENTUM_EOD_ENABLED", True)
|
||||
self.eod_hm = get_env_from_db("MOMENTUM_EOD_HM", "15:25")
|
||||
self.eod_hm = get_env_from_db("MOMENTUM_EOD_HM", "15:20")
|
||||
except Exception as e:
|
||||
self.logger.debug("momentum_engine defaults 조회 실패: %s", e)
|
||||
self._engine_params = {}
|
||||
@@ -62,13 +63,41 @@ class MomentumStrategy(BaseStrategy):
|
||||
def _candidate_filter(self, candidate: Dict) -> bool:
|
||||
return bool(candidate.get("scalp_on", True))
|
||||
|
||||
def _reentry_cooldown_sec(self) -> int:
|
||||
# wall-clock 이중 게이트 제거 — 엔진 cooldown_min(신호봉 시계)만 사용 (BT 정합)
|
||||
if bool((self._engine_params or {}).get("cooldown_engine_only", True)):
|
||||
return 0
|
||||
return super()._reentry_cooldown_sec()
|
||||
|
||||
def check_buy(self, code: str, name: str) -> Optional[Dict]:
|
||||
try:
|
||||
if get_env_bool("FORCE_BUY_TEST", False):
|
||||
return self._force_buy_test(code, name)
|
||||
|
||||
candles_raw = self.ws.get_candles(code, self.candle_tf, n=50)
|
||||
if len(candles_raw) < 5:
|
||||
# E조건(전일시가) — 당일 50봉만으로는 불가 → 키움 REST 갭보정 RAM (DB 구데이터 미사용)
|
||||
min_need = get_env_int("MOMENTUM_LIVE_MIN_CANDLES", 500)
|
||||
candles_raw = list(self.ws.get_candles(code, self.candle_tf, n=min_need) or [])
|
||||
# ALIGN: 형성 중 봉(T)을 진입봉으로 붙여 BT(portfolio)와 동일 시점
|
||||
use_forming = bool((self._engine_params or {}).get("live_align_use_forming_bar", True))
|
||||
if (self._engine_params or {}).get("live_backtest_align", True) and use_forming:
|
||||
try:
|
||||
cur = self.ws.get_current_candle(code, self.candle_tf)
|
||||
except Exception:
|
||||
cur = None
|
||||
if cur and float(cur.get("open", 0) or 0) > 0:
|
||||
ct = str(cur.get("candle_time") or "")[:12]
|
||||
last_ct = ""
|
||||
if candles_raw:
|
||||
last_ct = str(candles_raw[-1].get("candle_time") or "")[:12]
|
||||
if ct and ct != last_ct:
|
||||
cur_d = dict(cur)
|
||||
cur_d["is_confirmed"] = 0
|
||||
candles_raw.append(cur_d)
|
||||
if len(candles_raw) < 6:
|
||||
try:
|
||||
self.ws.fill_gap([code])
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
candles = [self._norm_candle(c) for c in candles_raw]
|
||||
|
||||
@@ -79,6 +108,9 @@ class MomentumStrategy(BaseStrategy):
|
||||
last_exit_dt = dt.fromtimestamp(self.recently_sold[code])
|
||||
if last_exit_dt.strftime("%Y%m%d") != today:
|
||||
last_exit_dt = None
|
||||
# 분 단위 floor — 엔진 쿨다운이 신호봉 candle_time 과 같은 시계를 쓰도록
|
||||
elif bool((self._engine_params or {}).get("cooldown_use_candle_floor", True)):
|
||||
last_exit_dt = last_exit_dt.replace(second=0, microsecond=0)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
@@ -101,6 +133,13 @@ class MomentumStrategy(BaseStrategy):
|
||||
params["slot_money"] = self.slot_money
|
||||
reject, msg, sig = me.check_buy_signal_momentum_live(candles, params, state)
|
||||
if reject:
|
||||
# 갭보정 워밍업 중 — 전일시가 없음 로그 스팸 방지
|
||||
if reject == "탈락-전일시가없음" and len(candles_raw) < min_need:
|
||||
try:
|
||||
self.ws.fill_gap([code])
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
self.logger.info("🔍 [%s] %s %s: %s", reject, name, code, msg or "")
|
||||
return None
|
||||
if not sig:
|
||||
@@ -201,9 +240,9 @@ class MomentumStrategy(BaseStrategy):
|
||||
now = dt.now()
|
||||
is_eod = is_live_eod_now(
|
||||
getattr(self, "eod_enabled", True),
|
||||
getattr(self, "eod_hm", "15:25"),
|
||||
getattr(self, "eod_hm", "15:20"),
|
||||
now,
|
||||
default_hm="15:25",
|
||||
default_hm="15:20",
|
||||
)
|
||||
params = dict(self._engine_params or me.get_momentum_defaults_from_db())
|
||||
|
||||
|
||||
Reference in New Issue
Block a user