반복적인 IPC 오류 해결, 봇 오류 해결, 인증 오류 해결, 서버 자원 할당 오류 해결, 코드 리팩토링
This commit is contained in:
@@ -484,31 +484,174 @@ class KISClient:
|
||||
self.ensure_token()
|
||||
url = f"{self.base_url}/uapi/domestic-stock/v1/quotations/inquire-investor"
|
||||
headers = self._get_headers(tr_id="FHKST01010900")
|
||||
|
||||
|
||||
params = {
|
||||
"FID_COND_MRKT_DIV_CODE": "J",
|
||||
"FID_INPUT_ISCD": ticker
|
||||
}
|
||||
|
||||
|
||||
try:
|
||||
res = requests.get(url, headers=headers, params=params)
|
||||
res.raise_for_status()
|
||||
data = res.json()
|
||||
if data['rt_cd'] != '0':
|
||||
return None
|
||||
|
||||
# output 리스트: [ {stck_bsop_date: 날짜, frgn_ntby_qty: 외인순매수, orgn_ntby_qty: 기관순매수, ...}, ... ]
|
||||
|
||||
trends = []
|
||||
for item in data['output'][:5]: # 최근 5일치만
|
||||
for item in data['output'][:5]:
|
||||
trends.append({
|
||||
"date": item['stck_bsop_date'],
|
||||
"foreigner": self._safe_int(item.get('frgn_ntby_qty')), # 외인 순매수량
|
||||
"institutional": self._safe_int(item.get('orgn_ntby_qty')), # 기관 순매수량
|
||||
"price_change": float(item['prdy_vrss']) # 전일대비 등락금액
|
||||
"foreigner": self._safe_int(item.get('frgn_ntby_qty')),
|
||||
"institutional": self._safe_int(item.get('orgn_ntby_qty')),
|
||||
"price_change": float(item['prdy_vrss'])
|
||||
})
|
||||
|
||||
# 최근일이 0번 인덱스임
|
||||
|
||||
return trends
|
||||
except Exception as e:
|
||||
print(f"❌ 투자자 동향 조회 실패({ticker}): {e}")
|
||||
print(f"[KIS] 투자자 동향 조회 실패({ticker}): {e}")
|
||||
return None
|
||||
|
||||
|
||||
class KISAsyncClient:
|
||||
"""
|
||||
비동기 KIS API 클라이언트
|
||||
- aiohttp 기반 HTTP 호출
|
||||
- 동기 KISClient의 토큰/설정을 공유
|
||||
- 다중 종목 병렬 수집용
|
||||
"""
|
||||
def __init__(self, sync_client):
|
||||
self.sync = sync_client
|
||||
self.min_interval = 0.5 # 초당 2회 제한
|
||||
|
||||
async def _async_get(self, session, url, headers, params):
|
||||
"""비동기 GET 요청"""
|
||||
try:
|
||||
async with session.get(url, headers=headers, params=params) as resp:
|
||||
return await resp.json()
|
||||
except Exception as e:
|
||||
print(f"[KIS Async] Request failed: {e}")
|
||||
return None
|
||||
|
||||
async def get_daily_price_async(self, ticker):
|
||||
"""비동기 일별 시세 조회"""
|
||||
import aiohttp
|
||||
import asyncio
|
||||
|
||||
self.sync.ensure_token()
|
||||
url = f"{self.sync.base_url}/uapi/domestic-stock/v1/quotations/inquire-daily-price"
|
||||
headers = self.sync._get_headers(tr_id="FHKST01010400")
|
||||
params = {
|
||||
"FID_COND_MRKT_DIV_CODE": "J",
|
||||
"FID_INPUT_ISCD": ticker,
|
||||
"FID_PERIOD_DIV_CODE": "D",
|
||||
"FID_ORG_ADJ_PRC": "1"
|
||||
}
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
data = await self._async_get(session, url, headers, params)
|
||||
if data and data.get('rt_cd') == '0':
|
||||
prices = [int(item['stck_clpr']) for item in data['output']]
|
||||
prices.reverse()
|
||||
return prices
|
||||
return []
|
||||
|
||||
async def get_investor_trend_async(self, ticker):
|
||||
"""비동기 투자자 동향 조회"""
|
||||
import aiohttp
|
||||
|
||||
self.sync.ensure_token()
|
||||
url = f"{self.sync.base_url}/uapi/domestic-stock/v1/quotations/inquire-investor"
|
||||
headers = self.sync._get_headers(tr_id="FHKST01010900")
|
||||
params = {
|
||||
"FID_COND_MRKT_DIV_CODE": "J",
|
||||
"FID_INPUT_ISCD": ticker
|
||||
}
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
data = await self._async_get(session, url, headers, params)
|
||||
if data and data.get('rt_cd') == '0':
|
||||
trends = []
|
||||
for item in data['output'][:5]:
|
||||
trends.append({
|
||||
"date": item['stck_bsop_date'],
|
||||
"foreigner": self.sync._safe_int(item.get('frgn_ntby_qty')),
|
||||
"institutional": self.sync._safe_int(item.get('orgn_ntby_qty')),
|
||||
"price_change": float(item['prdy_vrss'])
|
||||
})
|
||||
return trends
|
||||
return None
|
||||
|
||||
async def get_daily_prices_batch(self, tickers):
|
||||
"""여러 종목의 일별 시세를 병렬로 조회"""
|
||||
import aiohttp
|
||||
import asyncio
|
||||
|
||||
self.sync.ensure_token()
|
||||
results = {}
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
tasks = []
|
||||
for i, ticker in enumerate(tickers):
|
||||
# rate limit: 0.5초 간격으로 요청 생성
|
||||
if i > 0:
|
||||
await asyncio.sleep(self.min_interval)
|
||||
|
||||
url = f"{self.sync.base_url}/uapi/domestic-stock/v1/quotations/inquire-daily-price"
|
||||
headers = self.sync._get_headers(tr_id="FHKST01010400")
|
||||
params = {
|
||||
"FID_COND_MRKT_DIV_CODE": "J",
|
||||
"FID_INPUT_ISCD": ticker,
|
||||
"FID_PERIOD_DIV_CODE": "D",
|
||||
"FID_ORG_ADJ_PRC": "1"
|
||||
}
|
||||
tasks.append((ticker, self._async_get(session, url, headers, params)))
|
||||
|
||||
for ticker, task in tasks:
|
||||
data = await task
|
||||
if data and data.get('rt_cd') == '0':
|
||||
prices = [int(item['stck_clpr']) for item in data['output']]
|
||||
prices.reverse()
|
||||
results[ticker] = prices
|
||||
else:
|
||||
results[ticker] = []
|
||||
|
||||
return results
|
||||
|
||||
async def get_investor_trends_batch(self, tickers):
|
||||
"""여러 종목의 투자자 동향을 병렬로 조회"""
|
||||
import aiohttp
|
||||
import asyncio
|
||||
|
||||
self.sync.ensure_token()
|
||||
results = {}
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
tasks = []
|
||||
for i, ticker in enumerate(tickers):
|
||||
if i > 0:
|
||||
await asyncio.sleep(self.min_interval)
|
||||
|
||||
url = f"{self.sync.base_url}/uapi/domestic-stock/v1/quotations/inquire-investor"
|
||||
headers = self.sync._get_headers(tr_id="FHKST01010900")
|
||||
params = {
|
||||
"FID_COND_MRKT_DIV_CODE": "J",
|
||||
"FID_INPUT_ISCD": ticker
|
||||
}
|
||||
tasks.append((ticker, self._async_get(session, url, headers, params)))
|
||||
|
||||
for ticker, task in tasks:
|
||||
data = await task
|
||||
if data and data.get('rt_cd') == '0':
|
||||
trends = []
|
||||
for item in data['output'][:5]:
|
||||
trends.append({
|
||||
"date": item['stck_bsop_date'],
|
||||
"foreigner": self.sync._safe_int(item.get('frgn_ntby_qty')),
|
||||
"institutional": self.sync._safe_int(item.get('orgn_ntby_qty')),
|
||||
"price_change": float(item['prdy_vrss'])
|
||||
})
|
||||
results[ticker] = trends
|
||||
else:
|
||||
results[ticker] = None
|
||||
|
||||
return results
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import time
|
||||
import requests
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
class NewsCollector:
|
||||
"""
|
||||
NAS에서 뉴스를 받지 못할 경우, Windows 서버에서 직접 뉴스를 수집하는 모듈
|
||||
(Google News RSS 활용)
|
||||
"""
|
||||
"""동기 뉴스 수집 (Google News RSS)"""
|
||||
@staticmethod
|
||||
def get_market_news(query="주식 시장"):
|
||||
url = f"https://news.google.com/rss/search?q={query}&hl=ko&gl=KR&ceid=KR:ko"
|
||||
@@ -18,5 +17,56 @@ class NewsCollector:
|
||||
items.append({"title": title, "source": "Google News"})
|
||||
return items
|
||||
except Exception as e:
|
||||
print(f"❌ 뉴스 수집 실패: {e}")
|
||||
print(f"[News] Collection failed: {e}")
|
||||
return []
|
||||
|
||||
|
||||
class AsyncNewsCollector:
|
||||
"""비동기 뉴스 수집 + 5분 캐싱"""
|
||||
|
||||
def __init__(self):
|
||||
self._cache = None
|
||||
self._cache_time = 0
|
||||
self._cache_ttl = 300 # 5분
|
||||
|
||||
def get_market_news(self, query="주식 시장"):
|
||||
"""동기 인터페이스 (하위 호환)"""
|
||||
now = time.time()
|
||||
if self._cache and (now - self._cache_time) < self._cache_ttl:
|
||||
return self._cache
|
||||
|
||||
result = NewsCollector.get_market_news(query)
|
||||
self._cache = result
|
||||
self._cache_time = now
|
||||
return result
|
||||
|
||||
async def get_market_news_async(self, query="주식 시장"):
|
||||
"""비동기 뉴스 수집 (aiohttp + 캐싱)"""
|
||||
now = time.time()
|
||||
if self._cache and (now - self._cache_time) < self._cache_ttl:
|
||||
return self._cache
|
||||
|
||||
try:
|
||||
import aiohttp
|
||||
url = f"https://news.google.com/rss/search?q={query}&hl=ko&gl=KR&ceid=KR:ko"
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url, timeout=aiohttp.ClientTimeout(total=5)) as resp:
|
||||
content = await resp.read()
|
||||
root = ET.fromstring(content)
|
||||
items = []
|
||||
for item in root.findall(".//item")[:5]:
|
||||
title = item.find("title").text
|
||||
items.append({"title": title, "source": "Google News"})
|
||||
|
||||
self._cache = items
|
||||
self._cache_time = now
|
||||
return items
|
||||
except ImportError:
|
||||
# aiohttp 미설치 시 동기 fallback
|
||||
return self.get_market_news(query)
|
||||
except Exception as e:
|
||||
print(f"[News Async] Collection failed: {e}")
|
||||
# 캐시가 있으면 반환, 없으면 동기 fallback
|
||||
if self._cache:
|
||||
return self._cache
|
||||
return self.get_market_news(query)
|
||||
|
||||
@@ -23,7 +23,7 @@ class OllamaManager:
|
||||
try:
|
||||
if pynvml:
|
||||
pynvml.nvmlInit()
|
||||
self.handle = pynvml.nvmlDeviceGetHandleByIndex(0) # 0번 GPU (3070 Ti)
|
||||
self.handle = pynvml.nvmlDeviceGetHandleByIndex(0) # 0번 GPU (5070 Ti)
|
||||
self.gpu_available = True
|
||||
print("✅ [OllamaManager] NVIDIA GPU Monitoring On")
|
||||
else:
|
||||
@@ -74,16 +74,34 @@ class OllamaManager:
|
||||
print(f"⚠️ GPU Status Check Failed: {e}")
|
||||
return {"name": "N/A", "temp": 0, "vram_used": 0, "vram_total": 0, "load": 0}
|
||||
|
||||
def is_training_active(self):
|
||||
"""LSTM 학습 중인지 확인 (GPU 메모리 충돌 방지)"""
|
||||
try:
|
||||
import torch
|
||||
if torch.cuda.is_available():
|
||||
# VRAM 사용량으로 학습 여부 추정
|
||||
vram = self.check_vram()
|
||||
return vram > Config.VRAM_WARNING_THRESHOLD
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
def request_inference(self, prompt, context_data=None):
|
||||
"""
|
||||
Ollama에 추론 요청
|
||||
:param prompt: 시스템 프롬프트 + 사용자 입력
|
||||
:param context_data: (Optional) 이전 대화 컨텍스트
|
||||
- LSTM 학습 중이면 대기 (GPU 메모리 충돌 방지)
|
||||
"""
|
||||
# [5070Ti 최적화] VRAM이 14GB 이상이면 모델 언로드 시도 (16GB 중 여유분 확보)
|
||||
# LSTM 학습 중이면 최대 60초 대기
|
||||
import time as _time
|
||||
for _ in range(12):
|
||||
if not self.is_training_active():
|
||||
break
|
||||
print("[Ollama] Waiting for LSTM training to finish...")
|
||||
_time.sleep(5)
|
||||
|
||||
vram = self.check_vram()
|
||||
if vram > 14.0:
|
||||
print(f"⚠️ [OllamaManager] High VRAM Usage ({vram:.1f}GB). Requesting unload.")
|
||||
if vram > Config.VRAM_WARNING_THRESHOLD:
|
||||
print(f"[OllamaManager] High VRAM Usage ({vram:.1f}GB). Requesting unload.")
|
||||
try:
|
||||
# keep_alive=0으로 설정하여 모델 즉시 언로드
|
||||
requests.post(self.generate_url,
|
||||
|
||||
@@ -4,72 +4,73 @@
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import multiprocessing
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# 환경 변수 로드
|
||||
load_dotenv()
|
||||
|
||||
def run_telegram_bot_standalone():
|
||||
|
||||
def run_telegram_bot_standalone(ipc_lock=None, command_queue=None, shutdown_event=None):
|
||||
"""텔레그램 봇만 독립적으로 실행"""
|
||||
# 경로 문제 해결을 위해 상위 디렉토리 추가
|
||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../')))
|
||||
|
||||
from modules.services.telegram_bot.server import TelegramBotServer
|
||||
from modules.utils.ipc import BotIPC
|
||||
from modules.config import Config
|
||||
from modules.utils.ipc import SharedIPC
|
||||
from modules.utils.process_tracker import ProcessTracker
|
||||
|
||||
token = os.getenv("TELEGRAM_BOT_TOKEN")
|
||||
if not token:
|
||||
print("❌ [Telegram] TELEGRAM_BOT_TOKEN not found in .env")
|
||||
print("[Telegram] TELEGRAM_BOT_TOKEN not found in .env")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
ProcessTracker.register("Telegram Bot Standalone")
|
||||
print(f"🤖 [Telegram Bot Process] Starting... (PID: {os.getpid()})")
|
||||
print(f"🔗 [Telegram Bot] Standalone Process Mode (IPC Enabled)")
|
||||
|
||||
# IPC 초기화
|
||||
ipc = BotIPC()
|
||||
|
||||
# [최적화] 재시작 루프 구현
|
||||
print(f"[Telegram Bot Process] Starting... (PID: {os.getpid()})")
|
||||
|
||||
# IPC 초기화 (shared memory + command queue)
|
||||
ipc = SharedIPC(lock=ipc_lock, command_queue=command_queue)
|
||||
|
||||
while True:
|
||||
# shutdown 체크
|
||||
if shutdown_event and shutdown_event.is_set():
|
||||
print("[Telegram Bot] Shutdown signal received.")
|
||||
break
|
||||
|
||||
try:
|
||||
# 봇 서버 생성 (매번 새로 생성)
|
||||
bot_server = TelegramBotServer(token)
|
||||
|
||||
# IPC를 통해 메인 봇 데이터 가져오기
|
||||
bot_server = TelegramBotServer(token, ipc=ipc, shutdown_event=shutdown_event)
|
||||
|
||||
# 초기 데이터 로드
|
||||
try:
|
||||
# 초기 연결 시도
|
||||
instance_data = ipc.get_bot_instance_data()
|
||||
if instance_data:
|
||||
bot_server.set_bot_instance(instance_data)
|
||||
except Exception:
|
||||
pass # 연결 실패해도 일단 봇은 띄움
|
||||
|
||||
# 봇 실행 (블로킹)
|
||||
pass
|
||||
|
||||
bot_server.run()
|
||||
|
||||
# 재시작 요청 확인
|
||||
|
||||
if bot_server.should_restart:
|
||||
print("🔄 [Telegram Bot] Restarting instance...")
|
||||
import time
|
||||
time.sleep(1) # 잠시 대기
|
||||
print("[Telegram Bot] Restarting instance...")
|
||||
time.sleep(1)
|
||||
continue
|
||||
else:
|
||||
print("🛑 [Telegram Bot] Process exiting.")
|
||||
print("[Telegram Bot] Process exiting.")
|
||||
break
|
||||
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n🛑 [Telegram Bot] Stopped by user")
|
||||
print("[Telegram Bot] Stopped by user")
|
||||
break
|
||||
except Exception as e:
|
||||
if "Conflict" not in str(e):
|
||||
print(f"❌ [Telegram Bot] Error: {e}")
|
||||
print(f"[Telegram Bot] Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
break
|
||||
|
||||
# 정리
|
||||
ipc.cleanup()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
multiprocessing.freeze_support()
|
||||
run_telegram_bot_standalone()
|
||||
|
||||
@@ -1,54 +1,49 @@
|
||||
"""
|
||||
텔레그램 봇 최적화 버전
|
||||
- Polling 최적화 (CPU 사용률 감소)
|
||||
- 별도 프로세스로 분리
|
||||
- 봇 재시작 명령어
|
||||
- 원격 명령어 실행
|
||||
텔레그램 봇 - Shared Memory IPC + 양방향 명령 채널
|
||||
"""
|
||||
import os
|
||||
import asyncio
|
||||
import logging
|
||||
import subprocess
|
||||
import sys
|
||||
from telegram import Update
|
||||
from telegram.ext import Application, CommandHandler, ContextTypes
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# 로깅 설정
|
||||
logging.basicConfig(
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
level=logging.INFO
|
||||
)
|
||||
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||
|
||||
|
||||
class TelegramBotServer:
|
||||
def __init__(self, bot_token):
|
||||
# [최적화] 연결 풀 설정 추가
|
||||
def __init__(self, bot_token, ipc=None, shutdown_event=None):
|
||||
self.application = Application.builder()\
|
||||
.token(bot_token)\
|
||||
.concurrent_updates(True)\
|
||||
.build()
|
||||
|
||||
|
||||
self.bot_instance = None
|
||||
self.ipc = ipc
|
||||
self.shutdown_event = shutdown_event
|
||||
self.is_shutting_down = False
|
||||
self.should_restart = False
|
||||
|
||||
def set_bot_instance(self, bot):
|
||||
"""AutoTradingBot 인스턴스를 주입받음"""
|
||||
self.bot_instance = bot
|
||||
|
||||
def refresh_bot_instance(self):
|
||||
"""IPC에서 최신 봇 인스턴스 데이터 읽기"""
|
||||
from modules.utils.ipc import BotIPC
|
||||
ipc = BotIPC()
|
||||
self.bot_instance = ipc.get_bot_instance_data()
|
||||
if self.ipc:
|
||||
self.bot_instance = self.ipc.get_bot_instance_data()
|
||||
else:
|
||||
# fallback: 새 IPC 인스턴스 생성
|
||||
from modules.utils.ipc import SharedIPC
|
||||
ipc = SharedIPC()
|
||||
self.bot_instance = ipc.get_bot_instance_data()
|
||||
return self.bot_instance is not None
|
||||
|
||||
async def start_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""/start 명령어 핸들러"""
|
||||
print(f"📨 [Telegram] /start received from user {update.effective_user.id}")
|
||||
await update.message.reply_text(
|
||||
"🤖 <b>AI Trading Bot Command Center</b>\n"
|
||||
"<b>AI Trading Bot Command Center</b>\n"
|
||||
"명령어 목록:\n"
|
||||
"/status - 현재 봇 및 시장 상태 조회\n"
|
||||
"/portfolio - 현재 보유 종목 및 평가액\n"
|
||||
@@ -58,151 +53,170 @@ class TelegramBotServer:
|
||||
"/system - PC 리소스(CPU/GPU) 상태\n"
|
||||
"/ai - AI 모델 학습 상태 조회\n\n"
|
||||
"<b>[관리 명령어]</b>\n"
|
||||
"/restart - 봇 재시작\n"
|
||||
"/restart - 메인 봇 재시작 요청\n"
|
||||
"/exec <code>명령어</code> - 원격 명령어 실행\n"
|
||||
"/stop - 봇 종료",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
async def status_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""/status: 종합 상태 브리핑"""
|
||||
if not self.refresh_bot_instance():
|
||||
await update.message.reply_text("⚠️ 메인 봇이 실행 중이 아닙니다.")
|
||||
await update.message.reply_text("메인 봇이 실행 중이 아닙니다.")
|
||||
return
|
||||
|
||||
from datetime import datetime
|
||||
now = datetime.now()
|
||||
is_market_open = (9 <= now.hour < 15) or (now.hour == 15 and now.minute < 30)
|
||||
|
||||
status_msg = "✅ <b>System Status: ONLINE</b>\n"
|
||||
status_msg += f"🕒 <b>Market:</b> {'OPEN 🟢' if is_market_open else 'CLOSED 🔴'}\n"
|
||||
|
||||
|
||||
status_msg = "<b>System Status: ONLINE</b>\n"
|
||||
status_msg += f"<b>Market:</b> {'OPEN' if is_market_open else 'CLOSED'}\n"
|
||||
|
||||
macro_warn = self.bot_instance.is_macro_warning_sent
|
||||
status_msg += f"🌍 <b>Macro Filter:</b> {'DANGER 🚨 (Trading Halted)' if macro_warn else 'SAFE 🟢'}\n"
|
||||
|
||||
status_msg += f"<b>Macro Filter:</b> {'DANGER (Trading Halted)' if macro_warn else 'SAFE'}\n"
|
||||
|
||||
await update.message.reply_text(status_msg, parse_mode="HTML")
|
||||
|
||||
async def portfolio_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""/portfolio: 잔고 조회"""
|
||||
if not self.refresh_bot_instance():
|
||||
await update.message.reply_text("⚠️ 봇 인스턴스가 연결되지 않았습니다.")
|
||||
await update.message.reply_text("봇 인스턴스가 연결되지 않았습니다.")
|
||||
return
|
||||
|
||||
await update.message.reply_text("⏳ 잔고를 조회 중입니다...")
|
||||
|
||||
|
||||
await update.message.reply_text("잔고를 조회 중입니다...")
|
||||
|
||||
try:
|
||||
balance = self.bot_instance.kis.get_balance()
|
||||
if "error" in balance:
|
||||
await update.message.reply_text(f"❌ 잔고 조회 실패: {balance['error']}")
|
||||
await update.message.reply_text(f"잔고 조회 실패: {balance['error']}")
|
||||
return
|
||||
|
||||
msg = f"💰 <b>Total Asset:</b> <code>{int(balance['total_eval']):,} KRW</code>\n" \
|
||||
f"💵 <b>Deposit:</b> <code>{int(balance['deposit']):,} KRW</code>\n\n"
|
||||
|
||||
|
||||
msg = f"<b>Total Asset:</b> <code>{int(balance['total_eval']):,} KRW</code>\n" \
|
||||
f"<b>Deposit:</b> <code>{int(balance['deposit']):,} KRW</code>\n\n"
|
||||
|
||||
if balance['holdings']:
|
||||
msg += "<b>[Holdings]</b>\n"
|
||||
for stock in balance['holdings']:
|
||||
icon = "🔴" if stock['yield'] > 0 else "🔵"
|
||||
msg += f"{icon} <b>{stock['name']}</b> <code>{stock['yield']}%</code>\n" \
|
||||
f" (수량: {stock['qty']} / 평가손익: {stock['profit_loss']:,})\n"
|
||||
yld = float(stock.get('yield', 0))
|
||||
# 상승(빨강), 하락(파랑) 이모지 적용
|
||||
if yld > 0:
|
||||
icon = "🔴"
|
||||
yld_str = f"+{yld}"
|
||||
elif yld < 0:
|
||||
icon = "🔵"
|
||||
yld_str = f"{yld}"
|
||||
else:
|
||||
icon = "⚪"
|
||||
yld_str = f"{yld}"
|
||||
|
||||
msg += f"{icon} <b>{stock['name']}</b>: <code>{yld_str}%</code>\n" \
|
||||
f" (수량: {stock['qty']} / 손익: {stock['profit_loss']:,})\n"
|
||||
else:
|
||||
msg += "보유 중인 종목이 없습니다."
|
||||
|
||||
|
||||
await update.message.reply_text(msg, parse_mode="HTML")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
await update.message.reply_text(f"❌ Error: {str(e)}")
|
||||
await update.message.reply_text(f"Error: {str(e)}")
|
||||
|
||||
async def watchlist_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""/watchlist: 감시 대상 종목"""
|
||||
if not self.refresh_bot_instance():
|
||||
await update.message.reply_text("⚠️ 봇 인스턴스가 연결되지 않았습니다.")
|
||||
await update.message.reply_text("봇 인스턴스가 연결되지 않았습니다.")
|
||||
return
|
||||
|
||||
target_dict = self.bot_instance.load_watchlist()
|
||||
discovered = list(self.bot_instance.discovered_stocks)
|
||||
|
||||
msg = f"👀 <b>Watchlist: {len(target_dict)} items</b>\n"
|
||||
discovered = self.bot_instance.discovered_stocks
|
||||
|
||||
msg = f"<b>Watchlist: {len(target_dict)} items</b>\n"
|
||||
for code, name in target_dict.items():
|
||||
themes = self.bot_instance.theme_manager.get_themes(code)
|
||||
theme_str = f" ({', '.join(themes)})" if themes else ""
|
||||
msg += f"- {name}{theme_str}\n"
|
||||
|
||||
msg += f"• <b>{name}</b>{theme_str}\n"
|
||||
|
||||
if discovered:
|
||||
msg += f"\n✨ <b>Discovered Today ({len(discovered)}):</b>\n"
|
||||
msg += f"\n<b>Discovered Today ({len(discovered)}):</b>\n"
|
||||
for code in discovered:
|
||||
msg += f"- {code}\n"
|
||||
|
||||
|
||||
await update.message.reply_text(msg, parse_mode="HTML")
|
||||
|
||||
async def update_watchlist_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""/update_watchlist: Watchlist 즉시 업데이트"""
|
||||
await update.message.reply_text("🔄 Watchlist를 업데이트하고 있습니다... (30초 소요)")
|
||||
|
||||
try:
|
||||
from modules.services.kis import KISClient
|
||||
from watchlist_manager import WatchlistManager
|
||||
from modules.config import Config
|
||||
|
||||
temp_kis = KISClient()
|
||||
mgr = WatchlistManager(temp_kis, watchlist_file=Config.WATCHLIST_FILE)
|
||||
|
||||
summary = mgr.update_watchlist_daily()
|
||||
# HTML 특수문자 이스케이프
|
||||
summary = summary.replace("&", "&").replace("<", "<").replace(">", ">")
|
||||
await update.message.reply_text(summary)
|
||||
|
||||
except Exception as e:
|
||||
await update.message.reply_text(f"❌ 업데이트 실패: {e}")
|
||||
"""Watchlist 업데이트 - command queue를 통해 메인 봇에 요청"""
|
||||
if self.ipc and self.ipc.send_command('update_watchlist'):
|
||||
await update.message.reply_text("Watchlist 업데이트를 메인 봇에 요청했습니다.")
|
||||
else:
|
||||
# fallback: 직접 업데이트
|
||||
await update.message.reply_text("Watchlist를 업데이트하고 있습니다... (30초 소요)")
|
||||
try:
|
||||
from modules.services.kis import KISClient
|
||||
from watchlist_manager import WatchlistManager
|
||||
from modules.config import Config
|
||||
|
||||
temp_kis = KISClient()
|
||||
mgr = WatchlistManager(temp_kis, watchlist_file=Config.WATCHLIST_FILE)
|
||||
summary = mgr.update_watchlist_daily()
|
||||
summary = summary.replace("&", "&").replace("<", "<").replace(">", ">")
|
||||
await update.message.reply_text(summary)
|
||||
except Exception as e:
|
||||
await update.message.reply_text(f"업데이트 실패: {e}")
|
||||
|
||||
async def macro_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""/macro: 거시경제 지표 조회 (IPC 데이터 사용)"""
|
||||
if not self.refresh_bot_instance():
|
||||
await update.message.reply_text("⚠️ 메인 봇 연결 대기 중...")
|
||||
await update.message.reply_text("메인 봇 연결 대기 중...")
|
||||
return
|
||||
|
||||
await update.message.reply_text("⏳ 거시경제 데이터를 불러옵니다...")
|
||||
|
||||
|
||||
await update.message.reply_text("거시경제 데이터를 불러옵니다...")
|
||||
|
||||
try:
|
||||
indices = getattr(self.bot_instance.kis, '_macro_indices', {})
|
||||
|
||||
|
||||
if not indices:
|
||||
await update.message.reply_text("⚠️ 데이터가 아직 수집되지 않았습니다. 잠시 후 다시 시도하세요.")
|
||||
return
|
||||
await update.message.reply_text("데이터가 아직 수집되지 않았습니다.")
|
||||
return
|
||||
|
||||
status = "SAFE"
|
||||
msi = indices.get('MSI', 0)
|
||||
if msi >= 50: status = "DANGER"
|
||||
elif msi >= 30: status = "CAUTION"
|
||||
|
||||
color = "🟢" if status == "SAFE" else "🔴" if status == "DANGER" else "🟡"
|
||||
msg = f"{color} <b>Market Risk: {status}</b>\n\n"
|
||||
|
||||
if msi >= 50:
|
||||
status = "DANGER"
|
||||
elif msi >= 30:
|
||||
status = "CAUTION"
|
||||
|
||||
msg = f"<b>Market Risk: {status}</b>\n\n"
|
||||
|
||||
if 'MSI' in indices:
|
||||
msg += f"🌡️ <b>Stress Index:</b> <code>{indices['MSI']}</code>\n"
|
||||
|
||||
msg += f"<b>Stress Index:</b> <code>{indices['MSI']}</code>\n"
|
||||
|
||||
for k, v in indices.items():
|
||||
if k != "MSI":
|
||||
icon = "🔺" if v.get('change', 0) > 0 else "🔻"
|
||||
msg += f"{icon} <b>{k}</b>: {v.get('price', 0)} ({v.get('change', 0)}%)\n"
|
||||
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"
|
||||
|
||||
await update.message.reply_text(msg, parse_mode="HTML")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
await update.message.reply_text(f"❌ Error: {e}")
|
||||
await update.message.reply_text(f"Error: {e}")
|
||||
|
||||
async def system_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""/system: 시스템 리소스 상태"""
|
||||
if not self.refresh_bot_instance():
|
||||
await update.message.reply_text("⚠️ 메인 봇이 실행 중이 아닙니다.")
|
||||
await update.message.reply_text("메인 봇이 실행 중이 아닙니다.")
|
||||
return
|
||||
|
||||
|
||||
import psutil
|
||||
|
||||
cpu = psutil.cpu_percent(interval=1)
|
||||
|
||||
# non-blocking CPU 측정
|
||||
cpu = psutil.cpu_percent(interval=0)
|
||||
ram = psutil.virtual_memory().percent
|
||||
|
||||
# CPU 점유율 상위 3개 프로세스 수집
|
||||
|
||||
top_processes = []
|
||||
for proc in psutil.process_iter(['pid', 'name', 'cpu_percent']):
|
||||
try:
|
||||
@@ -212,91 +226,91 @@ class TelegramBotServer:
|
||||
top_processes.append(proc_info)
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
||||
pass
|
||||
|
||||
|
||||
top_processes.sort(key=lambda x: x.get('cpu_percent', 0), reverse=True)
|
||||
top_3 = top_processes[:3]
|
||||
|
||||
|
||||
gpu_status = self.bot_instance.ollama_monitor.get_gpu_status()
|
||||
gpu_msg = "N/A"
|
||||
if gpu_status and gpu_status.get('name') != 'N/A':
|
||||
gpu_name = gpu_status.get('name', 'GPU')
|
||||
gpu_msg = f"{gpu_name}\n Temp: {gpu_status.get('temp', 0)}°C / VRAM: {gpu_status.get('vram_used', 0)}GB / {gpu_status.get('vram_total', 0)}GB"
|
||||
|
||||
msg = "🖥️ <b>PC System Status</b>\n" \
|
||||
f"🧠 <b>CPU:</b> <code>{cpu}%</code>\n" \
|
||||
f"💾 <b>RAM:</b> <code>{ram}%</code>\n" \
|
||||
f"🎮 <b>GPU:</b> {gpu_msg}\n\n"
|
||||
|
||||
gpu_msg = f"{gpu_name}\n Temp: {gpu_status.get('temp', 0)}C / " \
|
||||
f"VRAM: {gpu_status.get('vram_used', 0)}GB / {gpu_status.get('vram_total', 0)}GB"
|
||||
|
||||
msg = "<b>PC System Status</b>\n" \
|
||||
f"<b>CPU:</b> <code>{cpu}%</code>\n" \
|
||||
f"<b>RAM:</b> <code>{ram}%</code>\n" \
|
||||
f"<b>GPU:</b> {gpu_msg}\n\n"
|
||||
|
||||
if top_3:
|
||||
msg += "⚙️ <b>Top CPU Processes:</b>\n"
|
||||
msg += "<b>Top CPU Processes:</b>\n"
|
||||
for i, proc in enumerate(top_3, 1):
|
||||
proc_name = proc.get('name', 'Unknown')
|
||||
proc_cpu = proc.get('cpu_percent', 0)
|
||||
msg += f" {i}. <code>{proc_name}</code> - {proc_cpu:.1f}%\n"
|
||||
|
||||
|
||||
await update.message.reply_text(msg, parse_mode="HTML")
|
||||
|
||||
async def ai_status_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""/ai: AI 모델 학습 상태 조회"""
|
||||
if not self.refresh_bot_instance():
|
||||
await update.message.reply_text("⚠️ 메인 봇이 실행 중이 아닙니다.")
|
||||
await update.message.reply_text("메인 봇이 실행 중이 아닙니다.")
|
||||
return
|
||||
|
||||
|
||||
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"
|
||||
|
||||
gpu_name = gpu.get('name', 'NVIDIA RTX 5070 Ti')
|
||||
msg += f"• <b>Device:</b> {gpu_name}\n"
|
||||
|
||||
|
||||
msg = "<b>AI Model Status</b>\n"
|
||||
msg += "* <b>LLM Engine:</b> Ollama (Llama 3.1)\n"
|
||||
msg += f"* <b>Device:</b> {gpu.get('name', 'GPU')}\n"
|
||||
|
||||
if gpu:
|
||||
msg += f"• <b>GPU Load:</b> <code>{gpu.get('load', 0)}%</code>\n"
|
||||
msg += f"• <b>VRAM Usage:</b> <code>{gpu.get('vram_used', 0)}GB</code> / {gpu.get('vram_total', 0)}GB"
|
||||
|
||||
msg += f"* <b>GPU Load:</b> <code>{gpu.get('load', 0)}%</code>\n"
|
||||
msg += f"* <b>VRAM Usage:</b> <code>{gpu.get('vram_used', 0)}GB</code> / {gpu.get('vram_total', 0)}GB"
|
||||
|
||||
await update.message.reply_text(msg, parse_mode="HTML")
|
||||
|
||||
async def restart_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""/restart: 텔레그램 봇 모듈만 재시작"""
|
||||
await update.message.reply_text("🔄 <b>텔레그램 인터페이스를 재시작합니다...</b>", parse_mode="HTML")
|
||||
|
||||
self.should_restart = True
|
||||
self.application.stop_running()
|
||||
"""/restart: 메인 봇에 재시작 명령 전달"""
|
||||
if self.ipc and self.ipc.send_command('restart'):
|
||||
await update.message.reply_text(
|
||||
"<b>메인 봇에 재시작 요청을 전송했습니다.</b>", parse_mode="HTML")
|
||||
else:
|
||||
# IPC 명령 실패 시 텔레그램 봇만 재시작
|
||||
await update.message.reply_text(
|
||||
"<b>텔레그램 인터페이스를 재시작합니다...</b>", parse_mode="HTML")
|
||||
self.should_restart = True
|
||||
self.application.stop_running()
|
||||
|
||||
async def stop_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""/stop: 봇 종료"""
|
||||
await update.message.reply_text("🛑 <b>텔레그램 봇을 종료합니다.</b>", parse_mode="HTML")
|
||||
|
||||
await update.message.reply_text(
|
||||
"<b>텔레그램 봇을 종료합니다.</b>", parse_mode="HTML")
|
||||
self.should_restart = False
|
||||
self.application.stop_running()
|
||||
|
||||
async def exec_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""/exec: 원격 명령어 실행 (Non-blocking)"""
|
||||
text = update.message.text.strip()
|
||||
parts = text.split(maxsplit=1)
|
||||
|
||||
|
||||
if len(parts) < 2:
|
||||
await update.message.reply_text("❌ 사용법: /exec 명령어")
|
||||
await update.message.reply_text("사용법: /exec 명령어")
|
||||
return
|
||||
|
||||
|
||||
command = parts[1]
|
||||
await update.message.reply_text(f"⚙️ 실행 중: <code>{command}</code>", parse_mode="HTML")
|
||||
|
||||
await update.message.reply_text(f"실행 중: <code>{command}</code>", parse_mode="HTML")
|
||||
|
||||
try:
|
||||
# 보안: 위험한 명령어 차단
|
||||
dangerous_keywords = ['rm', 'del', 'format', 'shutdown', 'reboot']
|
||||
if any(keyword in command.lower() for keyword in dangerous_keywords):
|
||||
await update.message.reply_text("⛔ 위험한 명령어는 실행할 수 없습니다.")
|
||||
await update.message.reply_text("위험한 명령어는 실행할 수 없습니다.")
|
||||
return
|
||||
|
||||
|
||||
import platform
|
||||
is_windows = platform.system() == 'Windows'
|
||||
|
||||
|
||||
if is_windows:
|
||||
exec_cmd = ['powershell', '-Command', command]
|
||||
else:
|
||||
exec_cmd = command
|
||||
|
||||
|
||||
def run_subprocess():
|
||||
return subprocess.run(
|
||||
exec_cmd,
|
||||
@@ -308,13 +322,13 @@ class TelegramBotServer:
|
||||
timeout=30,
|
||||
cwd=os.getcwd()
|
||||
)
|
||||
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
result = await loop.run_in_executor(None, run_subprocess)
|
||||
|
||||
|
||||
output = result.stdout.strip() if result.stdout else ""
|
||||
error_output = result.stderr.strip() if result.stderr else ""
|
||||
|
||||
|
||||
if output and error_output:
|
||||
combined = f"[STDOUT]\n{output}\n\n[STDERR]\n{error_output}"
|
||||
elif output:
|
||||
@@ -323,21 +337,19 @@ class TelegramBotServer:
|
||||
combined = f"[ERROR]\n{error_output}"
|
||||
else:
|
||||
combined = "명령어 실행 완료 (출력 없음)"
|
||||
|
||||
|
||||
if len(combined) > 3000:
|
||||
combined = combined[:3000] + "\n... (Truncated)"
|
||||
|
||||
# HTML 특수문자 이스케이프
|
||||
|
||||
combined = combined.replace("&", "&").replace("<", "<").replace(">", ">")
|
||||
await update.message.reply_text(f"<pre>{combined}</pre>", parse_mode="HTML")
|
||||
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
await update.message.reply_text("⏱️ 명령어 실행 시간 초과 (30초)")
|
||||
await update.message.reply_text("명령어 실행 시간 초과 (30초)")
|
||||
except Exception as e:
|
||||
await update.message.reply_text(f"❌ 실행 오류: {e}")
|
||||
await update.message.reply_text(f"실행 오류: {e}")
|
||||
|
||||
def run(self):
|
||||
"""봇 실행 (Handler 등록 및 Polling)"""
|
||||
handlers = [
|
||||
("start", self.start_command),
|
||||
("status", self.status_command),
|
||||
@@ -351,26 +363,26 @@ class TelegramBotServer:
|
||||
("stop", self.stop_command),
|
||||
("exec", self.exec_command)
|
||||
]
|
||||
|
||||
|
||||
for cmd, func in handlers:
|
||||
self.application.add_handler(CommandHandler(cmd, func))
|
||||
|
||||
|
||||
async def error_handler(update: object, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
if "Conflict" in str(context.error):
|
||||
print(f"⚠️ [Telegram] Conflict detected. Stopping...")
|
||||
print(f"[Telegram] Conflict detected. Stopping...")
|
||||
if self.application.running:
|
||||
await self.application.stop()
|
||||
return
|
||||
print(f"❌ [Telegram Error] {context.error}")
|
||||
print(f"[Telegram Error] {context.error}")
|
||||
|
||||
self.application.add_error_handler(error_handler)
|
||||
|
||||
print("🤖 [Telegram] Command Server Started (Standard Polling Mode).")
|
||||
|
||||
|
||||
print("[Telegram] Command Server Started (Shared Memory IPC Mode).")
|
||||
|
||||
try:
|
||||
self.application.run_polling(
|
||||
allowed_updates=Update.ALL_TYPES,
|
||||
drop_pending_updates=True
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"❌ [Telegram] Polling Error: {e}")
|
||||
print(f"[Telegram] Polling Error: {e}")
|
||||
|
||||
Reference in New Issue
Block a user