feat(backtest): 대대적인 Optuna 백테스트 웹 UI 및 백엔드 파이프라인 개편

- Web UI:
  - Optuna 탭 추가 및 mode_combo (최빈값 조합), 사후합격 Top 10 시각화 기능
  - 파라미터 분포(p25~p75, median, mode) 히스토그램 및 과적합(Overfit) 위험도 진단 UI 신설
  - 체크박스 렌더링 깨짐 현상을 네이티브(appearance: auto)로 강제 복구 (CSS)
  - 다단 트레일링 스탑, 꼬리 진입/돌파 손절 등 고급 조건 설정 폼 UI 고도화

- Backend (Optuna Jobs):
  - CLI 환경에서 구동된 Optuna json 결과물을 웹 대시보드로 읽어오는 import 기능 강화
  - JSON 메타데이터에 sort_by, mode, 호가 적용 여부 등 핵심 파라미터 파싱 누락 수정
  - optuna_mode_combo.py 등 최빈값 조합 및 후보군 2차 검증을 위한 신규 모듈 추가

- DB & Execution:
  - WebSocket 호가/틱 피드 수집 통계(api_feed_collect_stats) 메모리 캐시 최적화
  - KIS client 접속 키(approval_key) 등 인프라스트럭처 안정성 및 공유 관리 구조 개선
  - 테스트 및 디버깅용 briefing 마크다운 자동 생성 기능 추가
This commit is contained in:
Your Name
2026-09-01 02:47:51 +09:00
parent 3d0255519e
commit 9ba9ab73b6
92 changed files with 8211 additions and 487 deletions

View File

@@ -124,7 +124,7 @@ def token_covers_session(
return exp_dt >= deadline
def get_token_status(is_mock: bool) -> dict:
def get_token_status(is_mock: bool, current_app_key: str = None) -> dict:
"""
캐시 파일 상태 반환.
반환: valid=세션커버(ensure 재사용 기준), usable=만료 전 API 사용 가능
@@ -143,6 +143,18 @@ def get_token_status(is_mock: bool) -> dict:
token = cache.get("access_token", "")
expired_s = cache.get("access_token_token_expired", "")
exp_dt = _parse_expired(expired_s)
app_key_prefix = cache.get("app_key_prefix", "")
if current_app_key and app_key_prefix:
if not current_app_key.startswith(app_key_prefix):
return {
"valid": False,
"usable": False,
"token": "",
"expires": "앱키변경됨",
"expires_in_h": -999,
}
if not token or exp_dt is None:
return {
"valid": False,
@@ -299,6 +311,7 @@ def _issue_token(app_key: str, app_secret: str, is_mock: bool) -> bool:
"access_token_token_expired": exp,
"mock": is_mock,
"issued_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"app_key_prefix": app_key[:8] if app_key else "",
}, ensure_ascii=False, indent=2),
encoding="utf-8",
)
@@ -321,7 +334,14 @@ def ensure_token(is_mock: bool, env: dict = None) -> bool:
단일 모드(실전/모의) 토큰: 오늘 세션을 덮으면 재사용, 아니면만 발급.
1일 1회 원칙 — 충분하면 tokenP 호출 없음.
"""
status = get_token_status(is_mock)
if env is None:
env = _load_env()
key_suffix = "MOCK" if is_mock else "REAL"
app_key = str(env.get(f"KIS_APP_KEY_{key_suffix}", "") or "").strip()
app_secret = str(env.get(f"KIS_APP_SECRET_{key_suffix}", "") or "").strip()
status = get_token_status(is_mock, current_app_key=app_key)
mode = "모의" if is_mock else "실전"
if status["valid"]:
@@ -351,7 +371,7 @@ def ensure_token(is_mock: bool, env: dict = None) -> bool:
return False
try:
# 잠금 획득 후 다시 확인 (다른 프로세스가 갱신했을 수 있음)
status = get_token_status(is_mock)
status = get_token_status(is_mock, current_app_key=app_key)
if status["valid"]:
logger.info(f"🔑 {mode} 토큰 이미 갱신됨 (다른 프로세스) → 재사용")
return True
@@ -427,6 +447,15 @@ class KisTokenManager:
self._lock = threading.Lock()
self._token: Optional[str] = None
self._expiry: Optional[datetime] = None
self._app_key_prefix: Optional[str] = None
env = _load_env()
if env:
suffix = "MOCK" if is_mock else "REAL"
key = str(env.get(f"KIS_APP_KEY_{suffix}", "")).strip()
if key:
self._app_key_prefix = key[:8]
self._load_from_file() # 재시작 후에도 기존 토큰 재사용
# ── 내부 ──────────────────────────────────────────────────────
@@ -438,6 +467,14 @@ class KisTokenManager:
data = json.loads(self._cache_path.read_text(encoding="utf-8"))
token = data.get("access_token", "")
exp_dt = _parse_expired(data.get("access_token_token_expired", ""))
# 앱키 변경 감지: 캐시된 app_key_prefix 가 있고, 현재 prefix 와 다르면 무시
cached_prefix = data.get("app_key_prefix", "")
if self._app_key_prefix and cached_prefix:
if self._app_key_prefix != cached_prefix:
logger.warning("🔑 [%s] 앱키 변경 감지 → 기존 토큰 캐시 폐기", self._mode_str)
return
if token and exp_dt:
self._token = token
self._expiry = exp_dt