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:
@@ -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 장 시작 알림 메시지 — 보유종목·예수금·주문가능금액 현황."""
|
||||
|
||||
Reference in New Issue
Block a user