커서가 망쳐놓은 듯
This commit is contained in:
181
kis_long_ver1.py
181
kis_long_ver1.py
@@ -8,7 +8,7 @@ KIS Long Trading Bot Ver1 - 늘림목 전략용 한투 API 트레이딩 시스
|
||||
|
||||
import os
|
||||
import json
|
||||
import ㅁㅁ
|
||||
import time
|
||||
import random
|
||||
import logging
|
||||
import datetime
|
||||
@@ -52,9 +52,9 @@ except ImportError:
|
||||
ML_AVAILABLE = False
|
||||
logger.warning("⚠️ ml_predictor 미설치 - ML 예측 기능 사용 불가")
|
||||
|
||||
# DB 초기화
|
||||
# DB 초기화 — MariaDB 192.168.0.141 (database.py 모듈 상수 사용)
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
db = TradeDB(db_path=str(SCRIPT_DIR / "quant_bot.db"))
|
||||
db = TradeDB() # db_path 인수 무시됨, MariaDB 직접 연결
|
||||
|
||||
# DB에서 환경변수 로드 (초기 버전: Mattermost/Gemini 설정용)
|
||||
def get_env_from_db(key, default=""):
|
||||
@@ -153,6 +153,43 @@ def _save_kis_token_cache(access_token, access_token_token_expired, mock):
|
||||
pass
|
||||
|
||||
|
||||
def _invalidate_kis_token_cache(mock: bool = False):
|
||||
"""
|
||||
토큰 만료(EGW00123 등) 감지 시 캐시 무효화 → 다음 _headers()에서 자동 재발급.
|
||||
1. KisTokenManager 메모리 캐시 초기화
|
||||
2. 레거시 파일 캐시 삭제 (폴백용)
|
||||
"""
|
||||
try:
|
||||
from kis_token_manager import KisTokenManager, _km_instances, _km_instances_lock
|
||||
import threading
|
||||
with _km_instances_lock:
|
||||
if mock in _km_instances:
|
||||
inst = _km_instances[mock]
|
||||
inst._token = None
|
||||
inst._expiry = None
|
||||
logger.info("한투 토큰 메모리 캐시 초기화 (만료 감지) [%s]",
|
||||
"모의" if mock else "실전")
|
||||
except Exception as e:
|
||||
logger.debug("KisTokenManager 초기화 실패: %s", e)
|
||||
# 레거시 파일도 삭제
|
||||
try:
|
||||
if KIS_TOKEN_CACHE_PATH.exists():
|
||||
KIS_TOKEN_CACHE_PATH.unlink()
|
||||
logger.info("한투 토큰 레거시 캐시 삭제: %s", KIS_TOKEN_CACHE_PATH)
|
||||
except Exception as e:
|
||||
logger.warning("한투 토큰 캐시 삭제 실패(%s): %s", KIS_TOKEN_CACHE_PATH, e)
|
||||
|
||||
|
||||
def _is_token_expired_response(j: dict) -> bool:
|
||||
"""응답이 '기간이 만료된 token' 오류(EGW00123 등)인지 여부."""
|
||||
if not j or not isinstance(j, dict):
|
||||
return False
|
||||
msg_cd = j.get("msg_cd") or ""
|
||||
msg1 = str(j.get("msg1", "") or "")
|
||||
# EGW00123 외에도 msg1 문자열에 '만료된 token' / '만료' 가 포함되어 있으면 토큰 만료로 간주
|
||||
return msg_cd == "EGW00123" or "만료된 token" in msg1 or "만료" in msg1
|
||||
|
||||
|
||||
class KISClientWithOrder:
|
||||
"""주문 기능이 추가된 KIS 클라이언트 (env 키는 단타 봇과 동일: KIS_APP_KEY_MOCK/REAL, KIS_ACCOUNT_NO_MOCK/REAL)"""
|
||||
def __init__(self, mock=None):
|
||||
@@ -213,32 +250,46 @@ class KISClientWithOrder:
|
||||
self._auth()
|
||||
|
||||
def _auth(self):
|
||||
"""접근 토큰 발급"""
|
||||
"""
|
||||
접근 토큰 준비 — KisTokenManager 싱글톤 우선 사용.
|
||||
- 메모리 캐시 → 파일 캐시 → 신규 발급 순서로 처리
|
||||
- 만료 10분 전 선제 갱신 (KIS 23시간 정책 자동 준수)
|
||||
"""
|
||||
if not self.app_key or not self.app_secret:
|
||||
hint = "KIS_APP_KEY_MOCK, KIS_APP_SECRET_MOCK" if self.mock else "KIS_APP_KEY_REAL, KIS_APP_SECRET_REAL (또는 KIS_APP_KEY, KIS_APP_SECRET)"
|
||||
hint = "KIS_APP_KEY_MOCK, KIS_APP_SECRET_MOCK" if self.mock else "KIS_APP_KEY_REAL, KIS_APP_SECRET_REAL"
|
||||
raise ValueError(f"한투 앱키 미설정: {hint}")
|
||||
|
||||
|
||||
mode_str = "모의" if self.mock else "실전"
|
||||
try:
|
||||
from kis_token_manager import KisTokenManager
|
||||
token = KisTokenManager.instance(is_mock=self.mock).get_token()
|
||||
if token:
|
||||
self.access_token = token
|
||||
logger.info("✅ 한투 토큰 준비 완료 [%s] (KisTokenManager, 앞8자: %s…)",
|
||||
mode_str, token[:8])
|
||||
return
|
||||
except Exception as e:
|
||||
logger.debug("KisTokenManager 실패, 파일 캐시 폴백: %s", e)
|
||||
|
||||
# 폴백: 기존 파일 캐시
|
||||
cached = _load_kis_token_cache(self.mock)
|
||||
if cached:
|
||||
self.access_token = cached
|
||||
logger.info("한투 토큰 캐시 사용 (%s)", "모의" if self.mock else "실전")
|
||||
logger.info("✅ 한투 토큰 파일 캐시 사용 [%s]", mode_str)
|
||||
return
|
||||
|
||||
url = f"{self.base_url}/oauth2/tokenP"
|
||||
body = {"grant_type": "client_credentials", "appkey": self.app_key, "appsecret": self.app_secret}
|
||||
|
||||
# 폴백: kis_token_manager 경로로만 발급 (잠금·1일1회 준수, SMS 시각 안정화)
|
||||
try:
|
||||
r = requests.post(url, json=body, timeout=10)
|
||||
data = r.json()
|
||||
if "access_token" in data:
|
||||
self.access_token = data["access_token"]
|
||||
expired = data.get("access_token_token_expired") or ""
|
||||
_save_kis_token_cache(self.access_token, expired, self.mock)
|
||||
logger.info("한투 토큰 발급 완료 (%s)", "모의" if self.mock else "실전")
|
||||
else:
|
||||
raise RuntimeError("한투 토큰 발급 실패")
|
||||
from kis_token_manager import ensure_token
|
||||
if ensure_token(self.mock):
|
||||
cached = _load_kis_token_cache(self.mock)
|
||||
if cached:
|
||||
self.access_token = cached
|
||||
logger.info("✅ 한투 토큰 발급 완료 [%s]", mode_str)
|
||||
return
|
||||
except Exception as e:
|
||||
logger.error("한투 인증 예외: %s", e)
|
||||
raise
|
||||
logger.warning("kis_token_manager 발급 실패: %s", e)
|
||||
raise RuntimeError("한투 토큰 발급 실패")
|
||||
|
||||
def _get_hashkey(self, body):
|
||||
"""해시키(Hashkey) 발급 - POST 요청 시 body 무결성 검증용"""
|
||||
@@ -260,7 +311,19 @@ class KISClientWithOrder:
|
||||
return None
|
||||
|
||||
def _headers(self, tr_id, hashkey=None):
|
||||
"""API 호출용 헤더"""
|
||||
"""
|
||||
API 호출용 헤더.
|
||||
KisTokenManager.get_token() 으로 만료 10분 전 선제 갱신:
|
||||
- 유효하면 메모리에서 즉시 반환 (오버헤드 없음)
|
||||
- 만료 임박 시 자동 갱신 후 새 토큰 사용 → EGW00123 방지
|
||||
"""
|
||||
try:
|
||||
from kis_token_manager import KisTokenManager
|
||||
fresh = KisTokenManager.instance(is_mock=self.mock).get_token()
|
||||
if fresh:
|
||||
self.access_token = fresh
|
||||
except Exception:
|
||||
pass # 실패 시 기존 access_token 유지
|
||||
headers = {
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"authorization": f"Bearer {self.access_token}",
|
||||
@@ -272,24 +335,39 @@ class KISClientWithOrder:
|
||||
headers["hashkey"] = hashkey
|
||||
return headers
|
||||
|
||||
def _get(self, path, tr_id, params, max_retries=3):
|
||||
"""GET 요청. 429 시 지수 백오프 재시도"""
|
||||
def _get(self, path, tr_id, params, max_retries=5):
|
||||
"""GET 요청. 429/500+EGW00201(초당 거래건수 초과) 시 대기 후 재시도, 토큰 만료 시 캐시 삭제 후 1회 재인증."""
|
||||
url = f"{self.base_url}{path}"
|
||||
token_refreshed = False
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
r = requests.get(url, headers=self._headers(tr_id), params=params, timeout=15)
|
||||
if r.status_code == 429:
|
||||
wait_time = (2 ** attempt) + random.uniform(0.5, 1.5)
|
||||
logger.warning(f"⏳ API 호출 제한 (429) -> {wait_time:.1f}초 대기 후 재시도 ({attempt+1}/{max_retries})")
|
||||
wait_time = 1.5 + (attempt * 1.0)
|
||||
logger.warning(f"⏳ API 호출 제한 (429) -> {wait_time:.1f}초 대기 후 재시도 ({attempt+1}/{max_retries}) path={path}")
|
||||
time.sleep(wait_time)
|
||||
continue
|
||||
if r.status_code == 200:
|
||||
j = r.json()
|
||||
if j.get("rt_cd") == "0":
|
||||
# 200 또는 500(한투가 EGW00201 시 500 반환) 응답에서 body 파싱
|
||||
if r.status_code in (200, 500):
|
||||
try:
|
||||
j = r.json()
|
||||
except Exception:
|
||||
j = {}
|
||||
if r.status_code == 200 and j.get("rt_cd") == "0":
|
||||
return r
|
||||
elif "초과" in str(j.get("msg1", "")) or "과부하" in str(j.get("msg1", "")):
|
||||
wait_time = (2 ** attempt) + random.uniform(0.5, 1.5)
|
||||
logger.warning(f"⏳ API 과부하 -> {wait_time:.1f}초 대기 후 재시도")
|
||||
if _is_token_expired_response(j) and not token_refreshed:
|
||||
logger.warning("🔑 한투 토큰 만료 감지(EGW00123 등) → 캐시 무효화 후 재인증, GET 재시도")
|
||||
_invalidate_kis_token_cache(mock=self.mock)
|
||||
self._auth()
|
||||
token_refreshed = True
|
||||
time.sleep(0.5)
|
||||
continue
|
||||
# EGW00201 또는 msg1에 '초과'/'과부하' → 초당 거래건수 초과, 대기 후 재시도
|
||||
if j.get("msg_cd") == "EGW00201" or "초과" in str(j.get("msg1", "")) or "과부하" in str(j.get("msg1", "")):
|
||||
wait_time = 1.5 + (attempt * 1.0)
|
||||
logger.warning(
|
||||
f"⏳ API 초당거래 초과 (EGW00201) GET {path} -> {wait_time:.1f}초 대기 후 재시도 ({attempt+1}/{max_retries}) msg1={j.get('msg1', '')}"
|
||||
)
|
||||
time.sleep(wait_time)
|
||||
continue
|
||||
return r
|
||||
@@ -456,33 +534,48 @@ class KISClientWithOrder:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _post(self, path, tr_id, body, use_hashkey=True, max_retries=3):
|
||||
"""POST 요청. 해시키 사용 및 429 시 지수 백오프 재시도"""
|
||||
def _post(self, path, tr_id, body, use_hashkey=True, max_retries=5):
|
||||
"""POST 요청. 429/500+EGW00201 시 대기 후 재시도, 토큰 만료 시 캐시 삭제 후 1회 재인증."""
|
||||
url = f"{self.base_url}{path}"
|
||||
hashkey = None
|
||||
|
||||
|
||||
if use_hashkey:
|
||||
hashkey = self._get_hashkey(body)
|
||||
if not hashkey:
|
||||
logger.debug("해시키 발급 실패, 해시키 없이 진행")
|
||||
|
||||
|
||||
token_refreshed = False
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
r = requests.post(url, headers=self._headers(tr_id, hashkey), json=body, timeout=15)
|
||||
if r.status_code == 429:
|
||||
wait_time = (2 ** attempt) + random.uniform(0.5, 1.5)
|
||||
logger.warning(f"⏳ API 호출 제한 (429) -> {wait_time:.1f}초 대기 후 재시도 ({attempt+1}/{max_retries})")
|
||||
wait_time = 1.5 + (attempt * 1.0)
|
||||
logger.warning(f"⏳ API 호출 제한 (429) -> {wait_time:.1f}초 대기 후 재시도 ({attempt+1}/{max_retries}) path={path}")
|
||||
time.sleep(wait_time)
|
||||
if use_hashkey:
|
||||
hashkey = self._get_hashkey(body)
|
||||
continue
|
||||
if r.status_code == 200:
|
||||
j = r.json()
|
||||
if j.get("rt_cd") == "0":
|
||||
if r.status_code in (200, 500):
|
||||
try:
|
||||
j = r.json()
|
||||
except Exception:
|
||||
j = {}
|
||||
if r.status_code == 200 and j.get("rt_cd") == "0":
|
||||
return r
|
||||
elif "초과" in str(j.get("msg1", "")) or "과부하" in str(j.get("msg1", "")):
|
||||
wait_time = (2 ** attempt) + random.uniform(0.5, 1.5)
|
||||
logger.warning(f"⏳ API 과부하 -> {wait_time:.1f}초 대기 후 재시도")
|
||||
if _is_token_expired_response(j) and not token_refreshed:
|
||||
logger.warning("🔑 한투 토큰 만료 감지(EGW00123 등) → 캐시 무효화 후 재인증, POST 재시도")
|
||||
_invalidate_kis_token_cache(mock=self.mock)
|
||||
self._auth()
|
||||
token_refreshed = True
|
||||
if use_hashkey:
|
||||
hashkey = self._get_hashkey(body)
|
||||
time.sleep(0.5)
|
||||
continue
|
||||
if j.get("msg_cd") == "EGW00201" or "초과" in str(j.get("msg1", "")) or "과부하" in str(j.get("msg1", "")):
|
||||
wait_time = 1.5 + (attempt * 1.0)
|
||||
logger.warning(
|
||||
f"⏳ API 초당거래 초과 (EGW00201) POST {path} -> {wait_time:.1f}초 대기 후 재시도 ({attempt+1}/{max_retries}) msg1={j.get('msg1', '')}"
|
||||
)
|
||||
time.sleep(wait_time)
|
||||
if use_hashkey:
|
||||
hashkey = self._get_hashkey(body)
|
||||
|
||||
Reference in New Issue
Block a user