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

@@ -107,6 +107,13 @@ _DEFAULT_CONDITION_BY_STRATEGY: Dict[str, Tuple[str, str]] = {
"UPDOW": ("reversal", "2"),
"DBBAND": ("dbband", "5"),
}
# 키움 HTS 전용 조건식 — KIS NAME 과 다를 때만 별도 지정 (비면 CONDITION_{SID}_NAME 폴백)
_DEFAULT_KIWOOM_CONDITION_BY_STRATEGY: Dict[str, Tuple[str, str]] = {
"SCALP": ("scalp_re", ""),
"SHORT": ("tail", ""),
"MOMENTUM": ("momentum", ""),
"BREAKOUT": ("breakout", ""),
}
_CONDITION_ENV_KEYS: Dict[str, Tuple[str, str]] = {
"SCALP": ("CONDITION_SCALP_NAME", "CONDITION_SCALP_SEQ"),
"SHORT": ("CONDITION_SHORT_NAME", "CONDITION_SHORT_SEQ"),
@@ -116,8 +123,8 @@ _CONDITION_ENV_KEYS: Dict[str, Tuple[str, str]] = {
"UPDOW": ("CONDITION_UPDOW_NAME", "CONDITION_UPDOW_SEQ"),
"DBBAND": ("CONDITION_DBBAND_NAME", "CONDITION_DBBAND_SEQ"),
}
# 키움 전용 조건식 seq (선택). 이름은 CONDITION_{SID}_NAME 공통 사용 — KIS/키움 HTS 이름이 같으면 NAME 하나로 충분.
# UNIVERSE_SOURCE=kiwoom_condition 일 때만 KIWOOM_SEQ 가 쓰임 (비어 있으면 CNSRLST 로 name→seq 자동 해결).
# 키움 전용 조건식 — ``CONDITION_{SID}_KIWOOM_NAME`` / ``CONDITION_{SID}_KIWOOM_SEQ``
# UNIVERSE_SOURCE=kiwoom_condition 일 때만 사용 (seq 비면 CNSRLST 로 name→seq 자동 해결).
_STARTUP_NOTIFY_STRATEGY_ORDER: Tuple[str, ...] = (
"SCALP", "SHORT", "MOMENTUM", "BREAKOUT", "RANGE_BREAK", "UPDOW", "DBBAND",
)
@@ -137,7 +144,7 @@ class TradingOrchestrator:
# 전략별 UNIVERSE_SOURCE 기본값 (BaseStrategy 와 동기화)
_DEFAULT_SOURCE = {
"SCALP": "condition",
"SCALP": "kiwoom_condition",
"SHORT": "kiwoom_condition",
"BREAKOUT": "kiwoom_condition",
"RANGE_BREAK": "condition",
@@ -210,6 +217,8 @@ class TradingOrchestrator:
self._market_open_report_date: str = ""
# 15:36~ 장마감 고아복구 중복 실행 가드 (당일 1회)
self._orphan_reconcile_date: str = ""
# Pre-EOD 고아복구 중복 실행 가드 (당일 1회)
self._orphan_pre_eod_reconcile_date: str = ""
# start_day_asset 조회가 모의 서버 500 등으로 실패할 때 무한 재시도 방지.
# 다음 시도 가능 epoch (0 = 즉시 가능). 실패 시 N초 백오프.
# 한투 모의 inquire-balance 가 간헐 500 → 20초 hb 마다 폭주하던 이슈 방지.
@@ -399,12 +408,21 @@ class TradingOrchestrator:
# 무조건 condition 으로 리셋하면 스위치가 무의미해지므로).
cur_src = (snap.get(f"{sid}_UNIVERSE_SOURCE") or "").strip().lower()
if cur_src not in ("ranking", "condition", "kiwoom_condition"):
patch[f"{sid}_UNIVERSE_SOURCE"] = "condition"
patch[f"{sid}_UNIVERSE_SOURCE"] = self._DEFAULT_SOURCE.get(sid, "condition")
nk, sk = _CONDITION_ENV_KEYS.get(sid, ("", ""))
if nk and not (snap.get(nk) or "").strip():
patch[nk] = dname
if sk and not (snap.get(sk) or "").strip():
patch[sk] = str(dseq)
kw_nk = f"CONDITION_{sid}_KIWOOM_NAME"
kw_sk = f"CONDITION_{sid}_KIWOOM_SEQ"
kd = _DEFAULT_KIWOOM_CONDITION_BY_STRATEGY.get(sid)
if kd:
kdname, kdseq = kd
if not (snap.get(kw_nk) or "").strip() and kdname:
patch[kw_nk] = kdname
if not (snap.get(kw_sk) or "").strip() and kdseq:
patch[kw_sk] = str(kdseq)
if not patch:
return
merged = dict(snap)
@@ -1516,31 +1534,45 @@ class TradingOrchestrator:
except Exception as e:
logger.error("장마감 최종 리포트 실패: %s", e)
# Pre-EOD 고아복구 — 가장 이른 EOD N분 (하루 1회, EOD 청산 전)
if self._orphan_pre_eod_reconcile_date != today:
try:
from .execution.orphan_reconcile import is_pre_eod_reconcile_window
if is_pre_eod_reconcile_window(now):
self._run_orphan_reconcile(today, phase="pre")
self._orphan_pre_eod_reconcile_date = today
except Exception as e:
logger.error("Pre-EOD 고아복구 실패: %s", e)
# 15:36~16:00 장마감 고아복구 (하루 1회 — 매매 없을 때 REST 잔고↔DB 대조)
if self._orphan_reconcile_date != today:
in_reconcile_window = (h == 15 and 36 <= m <= 59) or (h == 16 and m < 30)
if in_reconcile_window:
try:
self._run_orphan_reconcile(today)
self._run_orphan_reconcile(today, phase="post")
self._orphan_reconcile_date = today
except Exception as e:
logger.error("장마감 고아복구 실패: %s", e)
def _run_orphan_reconcile(self, today: str) -> None:
"""장마감 후 봇 고아(active_trades 미기록) 1회 복구."""
def _run_orphan_reconcile(self, today: str, *, phase: str = "post") -> None:
"""봇 고아(active_trades 미기록) 1회 복구. phase=pre|post."""
from .execution.orphan_reconcile import reconcile_orphan_positions
result = reconcile_orphan_positions(self.order_mgr)
self._orphan_reconcile_date = today
n = int(result.get("reconciled_count") or 0)
if n <= 0 and not result.get("error"):
logger.debug("🧩 [고아복구] 복구 대상 없음")
g = int(result.get("ghost_purged_count") or 0)
if n <= 0 and g <= 0 and not result.get("error"):
label = "Pre-EOD" if phase == "pre" else "장마감"
logger.debug("🧩 [%s 고아복구] 복구·유령삭제 대상 없음", label)
return
if result.get("error"):
logger.warning("🧩 [고아복구] 중단: %s", result["error"])
return
title = "Pre-EOD 고아복구" if phase == "pre" else "장마감 고아복구"
lines = [
"🧩 **[장마감 고아복구]**",
f"🧩 **[{title}]**",
f"- 복구: {n}종목",
f"- 유령삭제: {g}종목",
f"- 실패: {int(result.get('failed_count') or 0)}",
]
for it in (result.get("reconciled") or [])[:8]:
@@ -1550,13 +1582,17 @@ class TradingOrchestrator:
)
if n > 8:
lines.append(f" …외 {n - 8}종목")
for it in (result.get("ghost_purged") or [])[:5]:
lines.append(
f" ✕유령 {it.get('name')}({it.get('code')}) [{it.get('strategy')}]"
)
body = "\n".join(lines)
try:
from .utils.logger import msg_mm
msg_mm(body, channel_alias=self._system_mm_channel(), jitter=False)
except Exception as e:
logger.debug("고아복구 MM 전송 실패: %s", e)
logger.info("🧩 [고아복구] %d종목 active_trades 반영", n)
logger.info("🧩 [고아복구] 복구 %d · 유령삭제 %d active_trades 반영", n, g)
def _build_market_open_report(self, today_ymd: str) -> str:
"""09:00 장 시작 알림 메시지 — 보유종목·예수금·주문가능금액 현황."""