feat: Add new files and enhance backtesting functionality

Changes:
- Introduced new files for strategy definitions and study names.
- Enhanced `backtest_web.py` with functions to handle integer display prices and trade data formatting.
- Updated backtesting logic to incorporate end-of-day (EOD) parameters for breakout and momentum strategies.
- Added EOD configuration options in the database and parameter search files.

Impact:
- These changes improve the modularity and usability of the backtesting framework, allowing for better integration of EOD strategies and clearer trade data presentation.
This commit is contained in:
2026-07-06 19:11:34 +09:00
parent 336d637b72
commit 78edb75e01
33 changed files with 1479 additions and 303 deletions

View File

@@ -182,6 +182,7 @@ class TradingOrchestrator:
self.overseas_ws = None
self.condition_mgr: ConditionSearchManager | None = None
self.kiwoom_condition_mgr: KiwoomConditionSearchManager | None = None
self._pending_kiwoom_condition_configs: List[dict] = []
self.ranking_mgr: VolumeRankManager | None = None
# 시장 급락 서킷브레이커 (KOSPI/KOSDAQ 지수 폭락 시 신규 매수 전면 차단).
# 지수 조회는 모의 도메인 미지원 가능성 → 실키 우선, 폴백 self.client.
@@ -310,16 +311,12 @@ class TradingOrchestrator:
)
for strat in self.strategies:
strat.daily_profit_halt = guard
from .engine.daily_profit_halt import load_global_profit_target
from .engine.daily_profit_halt import (
describe_profit_guard_startup,
load_global_profit_target,
)
cfg = load_global_profit_target()
if cfg.get("enabled") and (
float(cfg.get("krw") or 0) > 0 or float(cfg.get("pct") or 0) > 0
):
logger.info(
"🎯 [일일익절] 마스터 ON — 목표 %s원 / %s%% (신규매수 중단)",
f"{float(cfg.get('krw') or 0):,.0f}",
f"{float(cfg.get('pct') or 0):.2f}",
)
logger.info(describe_profit_guard_startup(cfg, scope="마스터"))
def _daily_profit_notify_mm(
self, body: str, strategy_id: Optional[str] = None,
@@ -516,6 +513,8 @@ class TradingOrchestrator:
# 시세 WS 마이그레이션 검증 / WS_SUBSCRIBE_KIS_MINIMAL 시 키움 WS 기동
self._start_ws_validator()
# 키움 조건검색은 시세 WS 와 **동일 세션** 공유 → 시세 WS 기동 후에만 등록
self._start_kiwoom_condition_manager()
self._wire_ws_split_feed_if_needed()
# 예수금 캐시: 전략 기동 전 API 선동기화 (재시작 직후 stale kv 사용 방지)
@@ -645,26 +644,32 @@ class TradingOrchestrator:
continue
src = self._resolve_source(sid) # 시작 시 '주 소스' (로그/기본값용)
# ── ranking 쪽 설정 수집 (항상 등록 — 기본값 fallback 존재) ─────
# ── ranking 쪽 설정 수집 (UNIVERSE_SOURCE=ranking 일 때만 KIS REST 폴링) ─────
# kiwoom_condition / condition 소스 전략은 거래량순위 REST 폴링에 등록하지 않는다.
sort_key, limit_key = rank_env_keys[sid]
sort_default = self._DEFAULT_RANK_SORT.get(sid, "volume")
sort_val = (
get_env_from_db(sort_key, sort_default) or sort_default
).strip().lower()
limit_val = get_env_int(limit_key, 20)
ranking_configs.append({
"strategy_id": sid,
"sort": sort_val,
"limit": limit_val,
"market": "J",
})
register_ranking = src == "ranking"
if register_ranking:
ranking_configs.append({
"strategy_id": sid,
"sort": sort_val,
"limit": limit_val,
"market": "J",
})
# ── condition 쪽 설정 수집 (NAME 또는 SEQ 가 있을 때만 등록) ────
# ── condition 쪽 설정 수집 (UNIVERSE_SOURCE=condition 일 때만 KIS REST 폴링) ────
# CONDITION_{SID}_NAME 은 키움 조건식과 HTS 동일명 공유용으로도 쓰임.
# kiwoom_condition / ranking 소스 전략은 KIS psearch REST 폴링에 등록하지 않는다.
name_key, seq_key = cond_env_keys[sid]
nm = (get_env_from_db(name_key, "") or "").strip()
sq = (get_env_from_db(seq_key, "") or "").strip()
has_cond = bool(nm or sq)
if has_cond:
register_kis_cond = has_cond and src == "condition"
if register_kis_cond:
condition_configs.append({
"strategy_id": sid,
"name": nm or None,
@@ -698,9 +703,11 @@ class TradingOrchestrator:
)
logger.info(
"🔀 [%s] 시작소스=%s (매니저등록: ranking= condition=%s kiwoom=%s) "
"— 런타임 DB %s_UNIVERSE_SOURCE 변경 시 재시작 없이 전환",
sid, src, ("" if has_cond else ""),
"🔀 [%s] 시작소스=%s (매니저등록: ranking=%s condition=%s kiwoom=%s) "
"— 런타임 DB %s_UNIVERSE_SOURCE 변경 시 재시작 없이 전환 "
"(KIS ranking/condition REST 는 시작소스 일치 시만)",
sid, src, ("" if register_ranking else ""),
("" if register_kis_cond else ""),
("" if has_kw_cond else ""), sid,
)
@@ -765,39 +772,9 @@ class TradingOrchestrator:
else:
logger.info(" condition 소스 쓰는 전략 없음 → 조건검색 매니저 비활성")
# ── 키움 조건검색 매니저 기동 (WS 실시간 CNSRREQ/REAL) ──────────
# KIS 와 달리 웹소켓 실시간 지원. 토큰은 키움 REST(au10001) 로 발급하며
# _get_kiwoom_token_cached 캐시를 재사용한다. KIS 계정과 무관한 별도 키.
# env: KIWOOM_APP_KEY_REAL / KIWOOM_APP_SECRET_REAL (구독성 데이터 → 무조건 실키·실전 도메인)
if kiwoom_condition_configs:
# ⚠️ 조건검색은 시세/구독성 데이터 → KIS 조건검색·랭킹, 키움 시세 WS
# (KIWOOM_WS_FORCE_REAL) 와 동일하게 **KIWOOM_MOCK 와 무관하게 무조건 실키**
# 로 매칭한다. 모의 도메인은 조건검색 미지원/불안정.
kw_key = (get_env_from_db("KIWOOM_APP_KEY_REAL", "") or "").strip()
kw_secret = (get_env_from_db("KIWOOM_APP_SECRET_REAL", "") or "").strip()
# 접미사 없는 공통 키로 폴백 (시세 WS 블록과 동일 규칙)
kw_key = kw_key or (get_env_from_db("KIWOOM_APP_KEY", "") or "").strip()
kw_secret = kw_secret or (get_env_from_db("KIWOOM_APP_SECRET", "") or "").strip()
if not (kw_key and kw_secret):
logger.warning(
"⚠️ KIWOOM_APP_KEY_REAL/SECRET_REAL 미설정 → 키움 조건검색 매니저 비활성 "
"(%d개 전략 다른 소스 폴백)", len(kiwoom_condition_configs),
)
else:
try:
self.kiwoom_condition_mgr = KiwoomConditionSearchManager(
app_key=kw_key,
app_secret=kw_secret,
is_mock=False, # 조건검색은 항상 실전 도메인
configs=kiwoom_condition_configs,
db=self.db,
)
if not self.kiwoom_condition_mgr.start():
self.kiwoom_condition_mgr = None
except Exception as e:
logger.error("키움 조건검색 매니저 기동 실패: %s", e)
self.kiwoom_condition_mgr = None
else:
# ── 키움 조건검색: 시세 WS 기동 후 _start_kiwoom_condition_manager() 에서 등록 ──
self._pending_kiwoom_condition_configs = kiwoom_condition_configs
if not kiwoom_condition_configs:
logger.info(" kiwoom_condition 소스 쓰는 전략 없음 → 키움 조건검색 매니저 비활성")
# ------------------------------------------------------------------
@@ -920,6 +897,49 @@ class TradingOrchestrator:
elif need_validator and not kis_ws_handle:
logger.warning("KIS WS 핸들 미발견 → Validator 비활성")
def _start_kiwoom_condition_manager(self) -> None:
"""키움 조건검색 — 시세 WS(KiwoomWebSocketPriceCache) 와 단일 세션 공유.
키움은 동일 OAuth 토큰으로 WS 2접속 시 먼저 붙은 쪽에 Bye(1000)를 보내 끊는다.
조건검색 전용 소켓을 따로 열면 시세 WS 가 5초마다 끊기는 증상이 발생한다.
"""
configs = self._pending_kiwoom_condition_configs or []
if not configs:
return
kw_key = (get_env_from_db("KIWOOM_APP_KEY_REAL", "") or "").strip()
kw_secret = (get_env_from_db("KIWOOM_APP_SECRET_REAL", "") or "").strip()
kw_key = kw_key or (get_env_from_db("KIWOOM_APP_KEY", "") or "").strip()
kw_secret = kw_secret or (get_env_from_db("KIWOOM_APP_SECRET", "") or "").strip()
if not (kw_key and kw_secret):
logger.warning(
"⚠️ KIWOOM_APP_KEY_REAL/SECRET_REAL 미설정 → 키움 조건검색 매니저 비활성 "
"(%d개 전략 다른 소스 폴백)", len(configs),
)
return
shared = self.kiwoom_ws
if shared is None:
logger.warning(
"⚠️ 키움 시세 WS 미기동 → 조건검색 단독 접속 시도 "
"(WS_PROVIDER=kis_with_validation 또는 WS_SUBSCRIBE_KIS_MINIMAL 권장)"
)
try:
self.kiwoom_condition_mgr = KiwoomConditionSearchManager(
app_key=kw_key,
app_secret=kw_secret,
is_mock=False,
configs=configs,
db=self.db,
shared_ws=shared,
)
if not self.kiwoom_condition_mgr.start():
self.kiwoom_condition_mgr = None
except Exception as e:
logger.error("키움 조건검색 매니저 기동 실패: %s", e)
self.kiwoom_condition_mgr = None
def _wire_short_holding_peak_provider(self) -> None:
"""SHORT 보유 종목 max_price → ws_candles.holding_peak (백테·tail_engine 정합)."""
ca = getattr(self.ws, "candle_agg", None)