매매 성과 평가지표 시스템 구현

- modules/utils/performance_db.py 신규: 일별 자산 스냅샷(16:00~16:30) 및
  매매 기록 영구 저장 (PerformanceDB 클래스)
- modules/analysis/evaluator.py 신규: Sharpe/Sortino/MDD/Alpha 등 16개 지표 산출,
  S~F 등급 시스템, Ollama 5명 전문가 패널, 텔레그램 HTML 주간 보고서 (PerformanceEvaluator 클래스)
- modules/bot.py 수정: BUY/SELL 시 perf_db 기록 강화, 금요일 15:35 주간 평가 자동 실행,
  IPC 'evaluate' 명령 처리
- modules/services/telegram_bot/server.py 수정: /evaluate 명령어 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 23:07:34 +09:00
parent 4d41405ac4
commit 37f6d87bec
4 changed files with 872 additions and 41 deletions

View File

@@ -7,9 +7,17 @@ import logging
import subprocess
from telegram import Update
from telegram.ext import Application, CommandHandler, ContextTypes
# [디버깅] 파일 로깅 추가
log_file = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))),
"telegram_bot.log")
file_handler = logging.FileHandler(log_file, encoding='utf-8')
file_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO
level=logging.INFO,
handlers=[logging.StreamHandler(), file_handler]
)
logging.getLogger("httpx").setLevel(logging.WARNING)
@@ -42,6 +50,7 @@ class TelegramBotServer:
return self.bot_instance is not None
async def start_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
logging.info(f"[Command] /start from user {update.effective_user.id}")
await update.message.reply_text(
"<b>AI Trading Bot Command Center</b>\n"
"명령어 목록:\n"
@@ -51,7 +60,8 @@ class TelegramBotServer:
"/update_watchlist - Watchlist 즉시 업데이트\n"
"/macro - 거시경제 지표 및 시장 위험도\n"
"/system - PC 리소스(CPU/GPU) 상태\n"
"/ai - AI 모델 학습 상태 조회\n\n"
"/ai - AI 모델 학습 상태 조회\n"
"/evaluate - 즉시 성과 평가 보고서 생성\n\n"
"<b>[관리 명령어]</b>\n"
"/restart - 메인 봇 재시작 요청\n"
"/exec <code>명령어</code> - 원격 명령어 실행\n"
@@ -60,6 +70,7 @@ class TelegramBotServer:
)
async def status_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
logging.info(f"[Command] /status from user {update.effective_user.id}")
if not self.refresh_bot_instance():
await update.message.reply_text("메인 봇이 실행 중이 아닙니다.")
return
@@ -172,34 +183,67 @@ class TelegramBotServer:
await update.message.reply_text("데이터가 아직 수집되지 않았습니다.")
return
status = "SAFE"
msi = indices.get('MSI', 0)
msi = float(indices.get('MSI', 0))
if msi >= 50:
status = "DANGER"
risk_status = "🔴 DANGER"
risk_desc = "시장 극도 불안정 - 매수 중단 권고"
elif msi >= 30:
status = "CAUTION"
risk_status = "🟡 CAUTION"
risk_desc = "시장 불안정 - 보수적 매매 권고"
else:
risk_status = "🟢 SAFE"
risk_desc = "시장 안정 - 정상 매매 가능"
msg = f"<b>Market Risk: {status}</b>\n\n"
from datetime import datetime
now_str = datetime.now().strftime("%m/%d %H:%M")
if 'MSI' in indices:
msg += f"<b>Stress Index:</b> <code>{indices['MSI']}</code>\n"
msg = f"<b>거시경제 지표</b> <code>{now_str}</code>\n"
msg += f"━━━━━━━━━━━━━━━━━━\n"
msg += f"<b>Market Risk:</b> {risk_status}\n"
msg += f"<i>{risk_desc}</i>\n\n"
for k, v in indices.items():
if k != "MSI":
change = float(v.get('change', 0))
price = v.get('price', 0)
if change > 0:
icon = "🔴"
chg_str = f"+{change}"
elif change < 0:
icon = "🔵"
chg_str = f"{change}"
else:
icon = ""
chg_str = f"{change}"
msg += f"{icon} <b>{k}</b>: {price} ({chg_str}%)\n"
# MSI 상세
msi_bar = "" * int(msi / 10) + "" * (10 - int(msi / 10))
msg += f"<b>Stress Index (MSI):</b> <code>{msi:.1f}/100</code>\n"
msg += f"<code>[{msi_bar}]</code>\n\n"
# 지수 상세
index_order = ["KOSPI", "KOSDAQ", "KOSPI200"]
for k in index_order:
if k not in indices:
continue
v = indices[k]
price = float(v.get('price', 0))
change = float(v.get('change', 0))
change_val = float(v.get('change_val', 0))
high = float(v.get('high', 0))
low = float(v.get('low', 0))
prev_close = float(v.get('prev_close', 0))
volume = int(v.get('volume', 0))
if price == 0:
msg += f"⚫ <b>{k}:</b> <i>데이터 없음 (장 마감 후)</i>\n\n"
continue
if change > 0:
icon = "🔴"
chg_str = f"+{change:.2f}% (+{change_val:.2f}pt)"
elif change < 0:
icon = "🔵"
chg_str = f"{change:.2f}% ({change_val:.2f}pt)"
else:
icon = ""
chg_str = f"{change:.2f}%"
msg += f"{icon} <b>{k}:</b> <code>{price:,.2f}</code> {chg_str}\n"
if high and low:
msg += f" 고: <code>{high:,.2f}</code> 저: <code>{low:,.2f}</code>"
if prev_close:
msg += f" 전일종가: <code>{prev_close:,.2f}</code>"
msg += "\n"
if volume:
msg += f" 거래량: <code>{volume:,}천주</code>\n"
msg += "\n"
await update.message.reply_text(msg, parse_mode="HTML")
@@ -256,10 +300,11 @@ class TelegramBotServer:
await update.message.reply_text("메인 봇이 실행 중이 아닙니다.")
return
from modules.config import Config
gpu = self.bot_instance.ollama_monitor.get_gpu_status()
msg = "<b>AI Model Status</b>\n"
msg += "* <b>LLM Engine:</b> Ollama (Llama 3.1)\n"
msg += f"* <b>LLM Engine:</b> Ollama ({Config.OLLAMA_MODEL})\n"
msg += f"* <b>Device:</b> {gpu.get('name', 'GPU')}\n"
if gpu:
@@ -349,6 +394,29 @@ class TelegramBotServer:
except Exception as e:
await update.message.reply_text(f"실행 오류: {e}")
async def evaluate_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""/evaluate: 즉시 성과 평가 보고서 생성 (LLM 분석 포함)"""
await update.message.reply_text(
"📊 성과 평가를 실행합니다...\n"
"<i>LLM 전문가 패널 분석 포함 시 30초~1분 소요됩니다.</i>",
parse_mode="HTML"
)
try:
from modules.utils.performance_db import PerformanceDB
from modules.analysis.evaluator import PerformanceEvaluator
evaluator = PerformanceEvaluator()
loop = asyncio.get_running_loop()
report = await loop.run_in_executor(None, evaluator.generate_weekly_report)
if len(report) > 4000:
report = report[:4000] + "\n... (일부 생략)"
await update.message.reply_text(report, parse_mode="HTML")
except Exception as e:
logging.error(f"[Command] /evaluate error: {e}")
await update.message.reply_text(f"평가 오류: {e}")
def run(self):
handlers = [
("start", self.start_command),
@@ -359,6 +427,7 @@ class TelegramBotServer:
("macro", self.macro_command),
("system", self.system_command),
("ai", self.ai_status_command),
("evaluate", self.evaluate_command),
("restart", self.restart_command),
("stop", self.stop_command),
("exec", self.exec_command)
@@ -377,6 +446,7 @@ class TelegramBotServer:
self.application.add_error_handler(error_handler)
logging.info("[Telegram] Command Server Started (Shared Memory IPC Mode).")
print("[Telegram] Command Server Started (Shared Memory IPC Mode).")
try: