v3.1 과매수 방지, 앙상블 학습, KRX 캘린더 기반 장중 전용 운영 구현
[잔고 관리] - _today_buy_total 인스턴스 변수로 당일 누적 매수 추적 (KIS T+2 미차감 보완) - MAX_BUY_PER_CYCLE, MAX_DAILY_BUY_RATIO 설정 추가 - available_deposit = max_daily_buy - effective_today_buy 계산 [앙상블 & 포지션 사이징] - AdaptiveEnsemble 실제 연동 (하드코딩 가중치 제거) - Kelly Criterion Half-Kelly 포지션 비중 계산 - SignalWeights.normalize() Water-Filling 알고리즘으로 경계 위반 해결 - _accuracy_weighted() 크기 가중 정확도로 통일 - ensemble_weights.json → ensemble_history.json 통합 [LLM 클라이언트] - GeminiLLMClient 추가 (Gemini → Ollama 폴백 체인) - _class_last_call_ts 클래스 변수로 워커 재시작 후에도 스로틀 유지 - Ollama 미실행 조기 감지 및 명확한 오류 메시지 [KIS API] - 모든 requests.get/post에 timeout=Config.HTTP_TIMEOUT 적용 - get_balance()에 today_buy_amt 필드 추가 [장중 전용 운영] - KRXCalendar: exchange_calendars 기반, 2024~2026 공휴일 하드코딩 폴백 - EOD 셧다운: 15:35에 전체 상태 저장 후 서버 자동 종료 - Watchdog: .eod_date 마커로 EOD 후 재시작 차단 - daily_launcher.py: 매일 08:30 실행, 휴장일 감지 후 봇 미시작 - Windows 작업 스케줄러 WebAI_DailyLauncher 등록 [텔레그램 스킬 수정] - PYTHONIOENCODING=utf-8 서브프로세스 환경 설정 (cp949 이모지 오류 해결) - /regime: IPC macro_indices 파싱 구현, --json 모드 input() 블로킹 제거 - /weights: ensemble_history.json 형식 파싱 업데이트 - /model_health: glob 패턴 *_v3.pt 수정 - /postmortem: 거래 없을 때 빈 JSON 출력으로 Telegram 오류 해결 - /macro: price=0 시 prev_close 폴백 표시 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,11 @@ import time
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
try:
|
||||
import aiohttp
|
||||
except ImportError:
|
||||
aiohttp = None
|
||||
|
||||
from modules.config import Config
|
||||
|
||||
class KISClient:
|
||||
@@ -120,7 +125,7 @@ class KISClient:
|
||||
|
||||
try:
|
||||
print(f"🔑 [KIS] 토큰 발급 요청: {url}")
|
||||
res = requests.post(url, json=payload)
|
||||
res = requests.post(url, json=payload, timeout=Config.HTTP_TIMEOUT)
|
||||
res.raise_for_status()
|
||||
data = res.json()
|
||||
|
||||
@@ -164,7 +169,7 @@ class KISClient:
|
||||
"appsecret": self.app_secret
|
||||
}
|
||||
try:
|
||||
res = requests.post(url, headers=headers, json=datas)
|
||||
res = requests.post(url, headers=headers, json=datas, timeout=Config.HTTP_TIMEOUT)
|
||||
return res.json()["HASH"]
|
||||
except Exception as e:
|
||||
print(f"❌ Hash Key 생성 실패: {e}")
|
||||
@@ -185,10 +190,12 @@ class KISClient:
|
||||
|
||||
try:
|
||||
if method == "GET":
|
||||
res = requests.get(url, headers=headers, params=params)
|
||||
res = requests.get(url, headers=headers, params=params,
|
||||
timeout=Config.HTTP_TIMEOUT)
|
||||
else:
|
||||
res = requests.post(url, headers=headers, json=data)
|
||||
|
||||
res = requests.post(url, headers=headers, json=data,
|
||||
timeout=Config.HTTP_TIMEOUT)
|
||||
|
||||
# 토큰 만료 체크 (500 에러 or msg_cd 확인)
|
||||
is_token_error = False
|
||||
try:
|
||||
@@ -200,18 +207,20 @@ class KISClient:
|
||||
is_token_error = True
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
if is_token_error:
|
||||
print("🔄 [KIS] Token expired (caught). Refreshing...")
|
||||
self.ensure_token(force=True)
|
||||
headers = self._get_headers(tr_id)
|
||||
if use_hash and data and "hashkey" in headers:
|
||||
pass # Hash 재활용
|
||||
|
||||
|
||||
if method == "GET":
|
||||
res = requests.get(url, headers=headers, params=params)
|
||||
res = requests.get(url, headers=headers, params=params,
|
||||
timeout=Config.HTTP_TIMEOUT)
|
||||
else:
|
||||
res = requests.post(url, headers=headers, json=data)
|
||||
res = requests.post(url, headers=headers, json=data,
|
||||
timeout=Config.HTTP_TIMEOUT)
|
||||
|
||||
res.raise_for_status()
|
||||
return res.json()
|
||||
@@ -266,7 +275,8 @@ class KISClient:
|
||||
return {
|
||||
"holdings": holdings,
|
||||
"total_eval": int(summary['tot_evlu_amt']),
|
||||
"deposit": int(summary['dnca_tot_amt'])
|
||||
"deposit": int(summary['dnca_tot_amt']),
|
||||
"today_buy_amt": int(summary.get('thdt_buy_amt', 0)), # 금일매수금액 (T+2 차감 전 당일 집행액)
|
||||
}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
@@ -321,7 +331,7 @@ class KISClient:
|
||||
|
||||
try:
|
||||
print(f"📤 [KIS] 주문 전송: {buy_sell} {ticker} {qty}ea ({order_type_str})")
|
||||
res = requests.post(url, headers=headers, json=datas)
|
||||
res = requests.post(url, headers=headers, json=datas, timeout=Config.HTTP_TIMEOUT)
|
||||
res.raise_for_status()
|
||||
data = res.json()
|
||||
|
||||
@@ -348,7 +358,8 @@ class KISClient:
|
||||
}
|
||||
|
||||
try:
|
||||
res = requests.get(url, headers=headers, params=params)
|
||||
res = requests.get(url, headers=headers, params=params,
|
||||
timeout=Config.HTTP_TIMEOUT)
|
||||
res.raise_for_status()
|
||||
data = res.json()
|
||||
if data['rt_cd'] != '0':
|
||||
@@ -564,12 +575,13 @@ class KISClient:
|
||||
}
|
||||
|
||||
try:
|
||||
res = requests.get(url, headers=headers, params=params)
|
||||
res = requests.get(url, headers=headers, params=params,
|
||||
timeout=Config.HTTP_TIMEOUT)
|
||||
res.raise_for_status()
|
||||
data = res.json()
|
||||
if data['rt_cd'] != '0':
|
||||
return []
|
||||
|
||||
|
||||
results = []
|
||||
for item in data['output'][:limit]:
|
||||
# 코드는 shtn_iscd, 이름은 hts_kor_isnm
|
||||
@@ -664,7 +676,8 @@ class KISClient:
|
||||
}
|
||||
|
||||
try:
|
||||
res = requests.get(url, headers=headers, params=params)
|
||||
res = requests.get(url, headers=headers, params=params,
|
||||
timeout=Config.HTTP_TIMEOUT)
|
||||
res.raise_for_status()
|
||||
data = res.json()
|
||||
if data['rt_cd'] != '0':
|
||||
@@ -699,7 +712,9 @@ class KISAsyncClient:
|
||||
async def _async_get(self, session, url, headers, params):
|
||||
"""비동기 GET 요청"""
|
||||
try:
|
||||
async with session.get(url, headers=headers, params=params) as resp:
|
||||
timeout = aiohttp.ClientTimeout(total=Config.HTTP_TIMEOUT) if aiohttp else None
|
||||
async with session.get(url, headers=headers, params=params,
|
||||
timeout=timeout) as resp:
|
||||
return await resp.json()
|
||||
except Exception as e:
|
||||
print(f"[KIS Async] Request failed: {e}")
|
||||
|
||||
Reference in New Issue
Block a user