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:
@@ -62,6 +62,14 @@ class TelegramBotServer:
|
||||
"/system - PC 리소스(CPU/GPU) 상태\n"
|
||||
"/ai - AI 모델 학습 상태 조회\n"
|
||||
"/evaluate - 즉시 성과 평가 보고서 생성\n\n"
|
||||
"<b>[AI 진단 스킬]</b>\n"
|
||||
"/syshealth - 시스템 종합 건강 진단\n"
|
||||
"/risk - 리스크 대시보드 (MDD, 연속손절)\n"
|
||||
"/regime - 코스피 시장 레짐 감지\n"
|
||||
"/model_health - LSTM 모델 건강 체크\n"
|
||||
"/weights - 앙상블 가중치 분석\n"
|
||||
"/postmortem [일수] - 매매 사후 분석 (기본 30일)\n"
|
||||
"/watchlist_check - 감시 종목 스코어링\n\n"
|
||||
"<b>[관리 명령어]</b>\n"
|
||||
"/restart - 메인 봇 재시작 요청\n"
|
||||
"/exec <code>명령어</code> - 원격 명령어 실행\n"
|
||||
@@ -222,7 +230,11 @@ class TelegramBotServer:
|
||||
volume = int(v.get('volume', 0))
|
||||
|
||||
if price == 0:
|
||||
msg += f"⚫ <b>{k}:</b> <i>데이터 없음 (장 마감 후)</i>\n\n"
|
||||
# 장 마감 후: prev_close(전일 종가)라도 표시
|
||||
if prev_close > 0:
|
||||
msg += f"⚫ <b>{k}:</b> <code>{prev_close:,.2f}</code> <i>(전일 종가 기준, 장 마감)</i>\n\n"
|
||||
else:
|
||||
msg += f"⚫ <b>{k}:</b> <i>데이터 없음 (장 마감 후)</i>\n\n"
|
||||
continue
|
||||
|
||||
if change > 0:
|
||||
@@ -303,9 +315,18 @@ class TelegramBotServer:
|
||||
from modules.config import Config
|
||||
gpu = self.bot_instance.ollama_monitor.get_gpu_status()
|
||||
|
||||
if Config.GEMINI_API_KEY:
|
||||
llm_primary = f"Gemini ({Config.GEMINI_MODEL})"
|
||||
llm_fallback = f"Ollama ({Config.OLLAMA_MODEL})"
|
||||
else:
|
||||
llm_primary = f"Ollama ({Config.OLLAMA_MODEL})"
|
||||
llm_fallback = None
|
||||
|
||||
msg = "<b>AI Model Status</b>\n"
|
||||
msg += f"* <b>LLM Engine:</b> Ollama ({Config.OLLAMA_MODEL})\n"
|
||||
msg += f"* <b>Device:</b> {gpu.get('name', 'GPU')}\n"
|
||||
msg += f"* <b>LLM Engine:</b> {llm_primary}\n"
|
||||
if llm_fallback:
|
||||
msg += f"* <b>Fallback:</b> {llm_fallback}\n"
|
||||
msg += f"* <b>LSTM Device:</b> {gpu.get('name', 'GPU')}\n"
|
||||
|
||||
if gpu:
|
||||
msg += f"* <b>GPU Load:</b> <code>{gpu.get('load', 0)}%</code>\n"
|
||||
@@ -417,6 +438,121 @@ class TelegramBotServer:
|
||||
logging.error(f"[Command] /evaluate error: {e}")
|
||||
await update.message.reply_text(f"평가 오류: {e}")
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# AI 진단 스킬 명령어 (skill_runner 기반)
|
||||
# ──────────────────────────────────────────────
|
||||
|
||||
async def syshealth_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""/syshealth: 시스템 종합 건강 진단"""
|
||||
await update.message.reply_text("🔍 시스템 건강 진단 중... (최대 30초 소요)", parse_mode="HTML")
|
||||
try:
|
||||
from modules.services.telegram_bot import skill_runner
|
||||
result = await skill_runner.run_syshealth()
|
||||
for chunk in result:
|
||||
await update.message.reply_text(chunk, parse_mode="HTML")
|
||||
except Exception as e:
|
||||
logging.error(f"[Command] /syshealth error: {e}")
|
||||
await update.message.reply_text(f"진단 오류: {e}")
|
||||
|
||||
async def risk_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""/risk: 리스크 대시보드 (MDD, 연속손절, 포지션 집중도)"""
|
||||
await update.message.reply_text("📊 리스크 데이터 분석 중...", parse_mode="HTML")
|
||||
try:
|
||||
from modules.services.telegram_bot import skill_runner
|
||||
result = await skill_runner.run_risk()
|
||||
for chunk in result:
|
||||
await update.message.reply_text(chunk, parse_mode="HTML")
|
||||
except Exception as e:
|
||||
logging.error(f"[Command] /risk error: {e}")
|
||||
await update.message.reply_text(f"리스크 분석 오류: {e}")
|
||||
|
||||
async def regime_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""/regime: 코스피 시장 레짐 감지"""
|
||||
await update.message.reply_text("📈 시장 레짐 분석 중...", parse_mode="HTML")
|
||||
try:
|
||||
from modules.services.telegram_bot import skill_runner
|
||||
result = await skill_runner.run_regime()
|
||||
for chunk in result:
|
||||
await update.message.reply_text(chunk, parse_mode="HTML")
|
||||
except Exception as e:
|
||||
logging.error(f"[Command] /regime error: {e}")
|
||||
await update.message.reply_text(f"레짐 분석 오류: {e}")
|
||||
|
||||
async def model_health_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""/model_health: LSTM 모델 건강 체크"""
|
||||
await update.message.reply_text("🧠 LSTM 모델 체크포인트 스캔 중...", parse_mode="HTML")
|
||||
try:
|
||||
from modules.services.telegram_bot import skill_runner
|
||||
result = await skill_runner.run_model_health()
|
||||
for chunk in result:
|
||||
await update.message.reply_text(chunk, parse_mode="HTML")
|
||||
except Exception as e:
|
||||
logging.error(f"[Command] /model_health error: {e}")
|
||||
await update.message.reply_text(f"모델 건강 체크 오류: {e}")
|
||||
|
||||
async def weights_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""/weights: 앙상블 가중치 분석"""
|
||||
await update.message.reply_text("⚖️ 앙상블 가중치 분석 중...", parse_mode="HTML")
|
||||
try:
|
||||
from modules.services.telegram_bot import skill_runner
|
||||
result = await skill_runner.run_weights()
|
||||
for chunk in result:
|
||||
await update.message.reply_text(chunk, parse_mode="HTML")
|
||||
except Exception as e:
|
||||
logging.error(f"[Command] /weights error: {e}")
|
||||
await update.message.reply_text(f"가중치 분석 오류: {e}")
|
||||
|
||||
async def postmortem_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""/postmortem [days]: 매매 사후 분석 (기본 30일)"""
|
||||
args = context.args
|
||||
days = 30
|
||||
if args:
|
||||
try:
|
||||
days = int(args[0])
|
||||
days = max(7, min(days, 365))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
await update.message.reply_text(
|
||||
f"🔬 최근 {days}일 매매 사후 분석 중...", parse_mode="HTML")
|
||||
try:
|
||||
from modules.services.telegram_bot import skill_runner
|
||||
result = await skill_runner.run_postmortem(days)
|
||||
for chunk in result:
|
||||
await update.message.reply_text(chunk, parse_mode="HTML")
|
||||
except Exception as e:
|
||||
logging.error(f"[Command] /postmortem error: {e}")
|
||||
await update.message.reply_text(f"사후 분석 오류: {e}")
|
||||
|
||||
async def watchlist_check_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""/watchlist_check: 현재 감시 종목 스코어링"""
|
||||
await update.message.reply_text("🔎 감시 종목 스코어링 중...", parse_mode="HTML")
|
||||
try:
|
||||
from modules.services.telegram_bot import skill_runner
|
||||
|
||||
# 현재 watchlist에서 종목 코드 목록 로드
|
||||
candidates = []
|
||||
try:
|
||||
import json, os
|
||||
from modules.config import Config
|
||||
wl_path = Config.WATCHLIST_FILE
|
||||
if os.path.exists(wl_path):
|
||||
with open(wl_path, encoding="utf-8") as f:
|
||||
wl_data = json.load(f)
|
||||
if isinstance(wl_data, dict):
|
||||
candidates = list(wl_data.keys())
|
||||
elif isinstance(wl_data, list):
|
||||
candidates = wl_data
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
result = await skill_runner.run_watchlist_check(candidates)
|
||||
for chunk in result:
|
||||
await update.message.reply_text(chunk, parse_mode="HTML")
|
||||
except Exception as e:
|
||||
logging.error(f"[Command] /watchlist_check error: {e}")
|
||||
await update.message.reply_text(f"스코어링 오류: {e}")
|
||||
|
||||
def run(self):
|
||||
handlers = [
|
||||
("start", self.start_command),
|
||||
@@ -428,6 +564,13 @@ class TelegramBotServer:
|
||||
("system", self.system_command),
|
||||
("ai", self.ai_status_command),
|
||||
("evaluate", self.evaluate_command),
|
||||
("syshealth", self.syshealth_command),
|
||||
("risk", self.risk_command),
|
||||
("regime", self.regime_command),
|
||||
("model_health", self.model_health_command),
|
||||
("weights", self.weights_command),
|
||||
("postmortem", self.postmortem_command),
|
||||
("watchlist_check", self.watchlist_check_command),
|
||||
("restart", self.restart_command),
|
||||
("stop", self.stop_command),
|
||||
("exec", self.exec_command)
|
||||
|
||||
Reference in New Issue
Block a user