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:
Your Name
2026-07-17 01:09:09 +09:00
parent a4626e0351
commit fc27e726f9
151 changed files with 20718 additions and 6450 deletions

View File

@@ -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())