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,23 +1,88 @@
"""
kis_trader/execution/orphan_reconcile.py — 봇 고아 포지션 복구 (장마감 후 배치)
kis_trader/execution/orphan_reconcile.py — 봇 고아 포지션 복구 (장마감 전·후 배치)
================================================================================
실계좌 잔고(inquire-balance, 페이징)와 active_trades·orders 를 대조해
「봇이 매수했는데 active_trades 미기록」 고아만 복구한다.
이어서 GHOST_PURGE_ON_RECONCILE 시 「DB有·브로커0」 유령 행을 삭제한다.
- 수동매수(orders BUY 없음) · MANUAL_HOLD_CODES 지정분은 건드리지 않음.
- Pre-EOD: 활성 전략 중 가장 이른 EOD 시각 N분 (기본 7분) 1회
- Post-EOD: 15:36~ 장마감 후 1회 (기존)
- 수동매수(orders BUY 없음) · MANUAL_HOLD_CODES · strategy=HOLDING 은 건드리지 않음.
- 매매 알림은 보내지 않음(로그 + 선택적 요약 MM).
"""
from __future__ import annotations
import datetime
import json
import logging
from typing import Any, Dict, List, Optional, Set, Tuple
from ..utils.env import get_env_bool, get_env_from_db
from ..engine.strategy_eod import _STRATEGY_EOD_SPEC, parse_eod_hm, resolve_strategy_eod_params
from ..utils.env import get_env_bool, get_env_from_db, get_env_int
from ..utils.strategy_ids import canonical_strategy_id
logger = logging.getLogger("kis_trader.orphan_reconcile")
# Pre-EOD 시각 산출 — (전략 ON 플래그, strategy_eod spec 키)
_PRE_EOD_STRATEGY_FLAGS: Tuple[Tuple[str, str, bool], ...] = (
("STRATEGY_BREAKOUT_ENABLED", "BREAKOUT", False),
("STRATEGY_MOMENTUM_ENABLED", "MOMENTUM", False),
("STRATEGY_SHORT_ENABLED", "TAIL", True),
)
def resolve_pre_eod_reconcile_hm() -> Tuple[int, int]:
"""
활성 전략 EOD 시각 중 가장 이른 시각 ORPHAN_RECONCILE_PRE_EOD_LEAD_MIN 분.
활성 EOD 없으면 BREAKOUT 기본 15:15 기준.
"""
lead = max(1, get_env_int("ORPHAN_RECONCILE_PRE_EOD_LEAD_MIN", 7))
candidates: List[int] = []
for flag, sid, def_on in _PRE_EOD_STRATEGY_FLAGS:
if not get_env_bool(flag, def_on):
continue
spec = _STRATEGY_EOD_SPEC.get(sid)
if spec is None:
continue
en_key, hm_key, def_en, def_hm, leg_key = spec
params: Dict[str, Any] = {
en_key: get_env_from_db(en_key, str(def_en)),
hm_key: get_env_from_db(hm_key, def_hm),
}
if leg_key:
params[leg_key] = get_env_from_db(leg_key, str(def_en))
enabled, eod_hm = resolve_strategy_eod_params(params, sid)
if not enabled:
continue
hh, mm = parse_eod_hm(eod_hm, def_hm)
candidates.append(hh * 60 + mm)
if not candidates:
base_min = 15 * 60 + 15
else:
base_min = min(candidates)
pre_min = max(9 * 60, base_min - lead)
return pre_min // 60, pre_min % 60
def is_pre_eod_reconcile_window(
now: Optional[datetime.datetime] = None,
*,
window_min: int = 3,
) -> bool:
"""Pre-EOD 고아복구 실행 윈도우 (기본 3분)."""
if not get_env_bool("ORPHAN_RECONCILE_PRE_EOD_ENABLED", True):
return False
if not get_env_bool("ORPHAN_RECONCILE_ENABLED", True):
return False
t = now or datetime.datetime.now()
pre_h, pre_m = resolve_pre_eod_reconcile_hm()
cur = t.hour * 60 + t.minute
start = pre_h * 60 + pre_m
return start <= cur < start + max(1, window_min)
def parse_manual_hold_codes() -> Set[str]:
"""MANUAL_HOLD_CODES env — 쉼표/세미콜론 구분 종목코드."""
@@ -101,12 +166,95 @@ def _num(v, default: float = 0.0) -> float:
return default
def _broker_qty(broker: Dict[str, Any], code: str) -> int:
br = (broker or {}).get(code) or (broker or {}).get(str(code).strip())
if not br:
return 0
try:
return int((br or {}).get("qty") or 0)
except (TypeError, ValueError):
return 0
def purge_ghost_active_trades(order_mgr, broker: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""
DB(active_trades) 有 · 브로커 0주 → 유령 행 삭제.
- MANUAL_HOLD_CODES · strategy=HOLDING(장기) 은 보호(삭제 안 함).
- 잔고 API 실패 시 삭제하지 않음(오판 방지).
"""
out: Dict[str, Any] = {"purged": [], "skipped_manual": [], "skipped_holding": [], "failed": []}
if not get_env_bool("GHOST_PURGE_ON_RECONCILE", True):
out["msg"] = "GHOST_PURGE_ON_RECONCILE=false"
return out
db = order_mgr.db
if broker is None:
broker = order_mgr.get_broker_holdings(force=True)
if not getattr(order_mgr, "_holdings_last_fetch_ok", False):
out["error"] = "실계좌 잔고 조회 실패"
return out
manual_hold = parse_manual_hold_codes()
try:
rows = db.conn.execute(
"SELECT code, name, strategy, current_qty FROM active_trades"
).fetchall() or []
except Exception as e:
out["error"] = str(e)
logger.warning("유령잔고 조회 실패: %s", e)
return out
for row in rows:
d = dict(row)
code = str(d.get("code") or "").strip()
if not code:
continue
name = str(d.get("name") or code)
strategy = str(d.get("strategy") or "").strip()
if code in manual_hold:
out["skipped_manual"].append({"code": code, "name": name, "strategy": strategy})
continue
# 장기 홀딩봇 포지션은 당일 EOD 대상이 아님 — 브로커 0이어도 수동 확인 전 보존
if strategy.upper() == "HOLDING":
out["skipped_holding"].append({"code": code, "name": name})
continue
if _broker_qty(broker, code) > 0:
continue
try:
db.delete_active_trade(code=code, strategy=strategy or None)
out["purged"].append({
"code": code,
"name": name,
"strategy": strategy,
"qty": int(d.get("current_qty") or 0),
})
logger.warning(
"🧹 [유령잔고삭제] [%s] %s %s — 브로커 0주 → active_trades 삭제",
strategy, name, code,
)
except Exception as e:
out["failed"].append({"code": code, "name": name, "error": str(e)})
logger.exception("유령잔고 삭제 실패 %s", code)
out["purged_count"] = len(out["purged"])
out["failed_count"] = len(out["failed"])
if out["purged"]:
try:
order_mgr.invalidate_holdings_cache()
except Exception:
pass
return out
def reconcile_orphan_positions(order_mgr) -> Dict[str, Any]:
"""
장마감 후 1회 호출 — 봇 고아만 active_trades 에 upsert.
이어서 GHOST_PURGE_ON_RECONCILE 시 유령(DB有·브로커0) 삭제.
Returns:
reconciled, skipped_tracked, skipped_manual, skipped_no_order, failed
reconciled, skipped_tracked, skipped_manual, skipped_no_order, failed,
ghost_purged, ghost_purged_count
"""
result: Dict[str, Any] = {
"reconciled": [],
@@ -114,6 +262,7 @@ def reconcile_orphan_positions(order_mgr) -> Dict[str, Any]:
"skipped_manual": [],
"skipped_no_order": [],
"failed": [],
"ghost_purged": [],
}
if not get_env_bool("ORPHAN_RECONCILE_ENABLED", True):
@@ -214,11 +363,21 @@ def reconcile_orphan_positions(order_mgr) -> Dict[str, Any]:
except Exception:
pass
# 유령(DB有·브로커0) — 동일 잔고 스냅샷으로 삭제 (추가 REST 없음)
ghost = purge_ghost_active_trades(order_mgr, broker=broker)
result["ghost_purged"] = list(ghost.get("purged") or [])
result["ghost_purged_count"] = int(ghost.get("purged_count") or 0)
if ghost.get("error"):
result["ghost_error"] = ghost["error"]
for f in ghost.get("failed") or []:
result["failed"].append(f)
result["reconciled_count"] = len(result["reconciled"])
result["failed_count"] = len(result["failed"])
logger.info(
"🧩 [고아복구] 완료 — 복구 %d / 실패 %d / 보호(수동) %d / 이미추적 %d / 주문없음 %d",
"🧩 [고아복구] 완료 — 복구 %d / 유령삭제 %d / 실패 %d / 보호(수동) %d / 이미추적 %d / 주문없음 %d",
len(result["reconciled"]),
result["ghost_purged_count"],
len(result["failed"]),
len(result["skipped_manual"]),
len(result["skipped_tracked"]),