반복적인 IPC 오류 해결, 봇 오류 해결, 인증 오류 해결, 서버 자원 할당 오류 해결, 코드 리팩토링

This commit is contained in:
2026-02-14 18:03:13 +09:00
parent 4fd0aa91bc
commit 9dbf6e6791
15 changed files with 1452 additions and 847 deletions

View File

@@ -1,172 +1,203 @@
import time
import asyncio
import os
import sys
import json
import time
from concurrent.futures import ProcessPoolExecutor
from concurrent.futures.process import BrokenProcessPool
from datetime import datetime
# 모듈 임포트
from modules.config import Config
from modules.services.kis import KISClient
from modules.services.news import NewsCollector
from modules.services.news import AsyncNewsCollector
from modules.services.ollama import OllamaManager
from modules.services.telegram import TelegramMessenger
from modules.analysis.macro import MacroAnalyzer
from modules.utils.monitor import SystemMonitor
from modules.strategy.process import analyze_stock_process
# 기존 코드와의 호환성을 위해 상대 경로나 절대 경로로 임포트
# (리팩토링 과도기에는 일부 파일은 그대로 있을 수 있음)
try:
from theme_manager import ThemeManager
except ImportError:
# 템플릿용 더미
class ThemeManager:
def get_themes(self, code): return []
# 워커 프로세스 초기화 함수
def init_worker():
try:
from modules.utils.process_tracker import ProcessTracker
ProcessTracker.register("Trading Bot Worker")
except: pass
except Exception:
pass
class AutoTradingBot:
def __init__(self):
def __init__(self, ipc_lock=None, command_queue=None, shutdown_event=None):
# 1. 서비스 초기화
self.kis = KISClient()
self.news = NewsCollector()
# [안정성] GPU OOM 방지를 위해 워커 수 축소 (4 -> 2)
# [식별] 워커 프로세스 이름 등록
self.executor = ProcessPoolExecutor(max_workers=2, initializer=init_worker)
# 워커 프로세스 워밍업 (PID 등록 유도)
self.news = AsyncNewsCollector()
# GPU 경합 방지: 워커 1개만 사용 (LSTM 학습이 GPU 독점)
self.executor = ProcessPoolExecutor(max_workers=1, initializer=init_worker)
try:
list(self.executor.map(lambda x: x, range(2)))
except: pass
self.messenger = TelegramMessenger()
list(self.executor.map(lambda x: x, range(1)))
except Exception:
pass
self.messenger = TelegramMessenger()
self.theme_manager = ThemeManager()
self.ollama_monitor = OllamaManager() # GPU 모니터링용
self.ollama_monitor = OllamaManager()
# 2. 유틸리티 초기화
self.monitor = SystemMonitor(self.messenger, self.ollama_monitor)
# 3. 상태 변수
self.daily_trade_history = []
self.daily_trade_history = []
self.discovered_stocks = set()
self.is_macro_warning_sent = False
self.watchlist_updated_today = False
self.report_sent = False
# [IPC] BotIPC
# 4. 프로세스 관리
self.shutdown_event = shutdown_event
# 5. IPC (Shared Memory)
try:
from modules.utils.ipc import BotIPC
self.ipc = BotIPC()
from modules.utils.ipc import SharedIPC
self.ipc = SharedIPC(lock=ipc_lock, command_queue=command_queue)
except ImportError:
print("⚠️ BotIPC module not found.")
print("[Bot] SharedIPC module not found.")
self.ipc = None
# [Watchlist Manager]
# 6. Watchlist Manager
try:
from watchlist_manager import WatchlistManager
self.watchlist_manager = WatchlistManager(self.kis, watchlist_file=Config.WATCHLIST_FILE)
except ImportError:
self.watchlist_manager = None
# 기록 로드
# 7. 기록 로드
self.history_file = Config.HISTORY_FILE
self.load_trade_history()
# AI 하드웨어 점검
# 8. AI 하드웨어 점검
from modules.analysis.deep_learning import PricePredictor
PricePredictor.verify_hardware()
pass
# 9. KIS 비동기 클라이언트
try:
from modules.services.kis import KISAsyncClient
self.kis_async = KISAsyncClient(self.kis)
except ImportError:
self.kis_async = None
def load_trade_history(self):
"""파일에서 금일 매매 기록 로드"""
if os.path.exists(self.history_file):
try:
with open(self.history_file, "r", encoding="utf-8") as f:
self.daily_trade_history = json.load(f)
except Exception as e:
print(f"⚠️ Failed to load history: {e}")
except Exception:
self.daily_trade_history = []
else:
self.daily_trade_history = []
def save_trade_history(self):
"""매매 기록 저장"""
try:
with open(self.history_file, "w", encoding="utf-8") as f:
json.dump(self.daily_trade_history, f, ensure_ascii=False, indent=2)
except Exception as e:
print(f"⚠️ Failed to save history: {e}")
print(f"[Bot] Failed to save history: {e}")
def load_watchlist(self):
try:
with open(Config.WATCHLIST_FILE, "r", encoding="utf-8") as f:
return json.load(f)
except:
except Exception:
return {}
def send_daily_report(self):
"""장 마감 리포트"""
if self.report_sent: return
print("📝 [Bot] Generating Daily Report...")
if self.report_sent:
return
print("[Bot] Generating Daily Report...")
balance = self.kis.get_balance()
total_eval = 0
if "total_eval" in balance:
total_eval = int(balance.get("total_eval", 0))
report = f"📅 **[Daily Closing Report]**\n" \
f"────────────────\n" \
f"💰 **총 자산**: `{total_eval:,}`\n" \
f"📜 **오늘의 매매**: `{len(self.daily_trade_history)}`\n\n"
total_eval = int(balance.get("total_eval", 0))
report = f"📅 <b>[Daily Closing Report]</b>\n" \
f"💰 <b>Total Asset:</b> <code>{total_eval:,}</code>\n" \
f"📜 <b>Trades Today:</b> <code>{len(self.daily_trade_history)}</code>\n\n"
if self.daily_trade_history:
for trade in self.daily_trade_history:
icon = "🔴" if trade['action'] == "BUY" else "🔵"
report += f"{icon} {trade['name']} {trade['qty']}\n"
action = trade['action']
icon = "🔴" if action == "BUY" else "🔵"
report += f"{icon} <b>{action}</b> {trade['name']} {trade['qty']}\n"
if "holdings" in balance and balance["holdings"]:
report += "\n📊 **보유 현황**\n"
report += "\n📊 <b>[Holdings]</b>\n"
for stock in balance["holdings"]:
yld = stock.get('yield', 0)
icon = "🔺" if yld > 0 else "🔻"
report += f"{icon} {stock['name']}: `{yld}%`\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}"
report += f"{icon} {stock['name']}: <code>{yld_str}%</code>\n"
self.messenger.send_message(report)
self.report_sent = True
def restart_executor(self):
"""프로세스 풀 복구"""
print("🔄 [Bot] Restarting Process Executor...")
print("[Bot] Restarting Process Executor...")
try:
self.executor.shutdown(wait=False)
except:
except Exception:
pass
# 워커 재시작
self.executor = ProcessPoolExecutor(max_workers=2, initializer=init_worker)
print("✅ [Bot] Process Executor Restarted.")
self.executor = ProcessPoolExecutor(max_workers=1, initializer=init_worker)
print("[Bot] Process Executor Restarted.")
def run_cycle(self):
def _process_commands(self):
"""IPC command queue 폴링 및 처리"""
if not self.ipc:
return
commands = self.ipc.poll_commands()
for cmd in commands:
command = cmd.get('command', '')
print(f"[Bot] Received command: {command}")
if command == 'restart':
self.messenger.send_message("[Bot] Restart requested via Telegram.")
# executor 재시작
self.restart_executor()
elif command == 'update_watchlist':
if self.watchlist_manager:
try:
summary = self.watchlist_manager.update_watchlist_daily()
self.messenger.send_message(f"[Watchlist Updated]\n{summary}")
except Exception as e:
self.messenger.send_message(f"Watchlist update failed: {e}")
async def run_cycle(self):
now = datetime.now()
# 1. 거시경제 분석 (우선 순위 상향)
# 0. 명령 큐 폴링
self._process_commands()
# 1. 거시경제 분석
macro_status = MacroAnalyzer.get_macro_status(self.kis)
is_crash = False
if macro_status['status'] == 'DANGER':
is_crash = True
if not self.is_macro_warning_sent:
self.messenger.send_message("🚨 **[MARKET CRASH ALERT]** 시장 급락 감지! 매수 중단.")
self.messenger.send_message("🚨 <b>[MARKET CRASH ALERT]</b> 시장 급락 감지! 매수 중단.")
self.is_macro_warning_sent = True
else:
if self.is_macro_warning_sent:
self.messenger.send_message("🌤️ **[MARKET RECOVERY]** 시장 안정화.")
self.messenger.send_message("🌤️ <b>[MARKET RECOVERY]</b> 시장 안정화.")
self.is_macro_warning_sent = False
# 2. IPC 상태 업데이트
@@ -181,16 +212,16 @@ class AutoTradingBot:
'watchlist': watchlist,
'discovered_stocks': list(self.discovered_stocks),
'is_macro_warning': self.is_macro_warning_sent,
'macro_indices': macro_status['indicators'], # [수정] 거시경제 지표 전달
'macro_indices': macro_status['indicators'],
'themes': {}
})
except Exception:
pass
# 2. 아침 업데이트 (08:00)
# 3. 아침 업데이트 (08:00)
if now.hour == 8 and 0 <= now.minute < 5:
if not self.watchlist_updated_today and self.watchlist_manager:
print("🌅 Morning Update...")
print("[Bot] Morning Update...")
try:
summary = self.watchlist_manager.update_watchlist_daily()
self.messenger.send_message(summary)
@@ -198,7 +229,7 @@ class AutoTradingBot:
except Exception as e:
self.messenger.send_message(f"Update Failed: {e}")
# 3. 리셋 (09:00)
# 4. 리셋 (09:00)
if now.hour == 9 and now.minute < 5:
self.daily_trade_history = []
self.save_trade_history()
@@ -206,182 +237,168 @@ class AutoTradingBot:
self.discovered_stocks.clear()
self.watchlist_updated_today = False
# 4. 시스템 감시
# 5. 시스템 감시 (3분 간격)
self.monitor.check_health()
# 5. 장 운영 시간 체크
# 6. 장 운영 시간 체크
if not (9 <= now.hour < 15 or (now.hour == 15 and now.minute < 30)):
# 장 마감 리포트 (15:40)
if now.hour == 15 and now.minute >= 40:
self.send_daily_report()
print("💤 Market Closed. Waiting...")
print("[Bot] Market Closed. Waiting...")
return
print(f"[Bot] Cycle Start: {now.strftime('%H:%M:%S')}")
# 6. 거시경제 분석 (완료됨)
# macro_status = ... (Moved to top)
print(f"[Bot] Cycle Start: {now.strftime('%H:%M:%S')}")
# 7. 종목 분석 및 매매
target_dict = self.load_watchlist()
# (Discovery 로직 생략 - 필요시 추가)
# 보유 종목 리스크 관리
balance = self.kis.get_balance()
current_holdings = {}
if balance and "holdings" in balance:
for stock in balance["holdings"]:
code = stock.get("code")
name = stock.get("name")
qty = int(stock.get("qty", 0))
yld = float(stock.get("yield", 0.0))
current_holdings[code] = stock
# 보유 수량이 0 이하라면 스킵 (오류 방지)
if qty <= 0:
continue
action = None
reason = ""
# 손절/익절 로직
if yld <= -5.0:
if yld <= -5.0:
action = "SELL"
reason = "Stop Loss 📉"
elif yld >= 8.0:
reason = "Stop Loss"
elif yld >= 8.0:
action = "SELL"
reason = "Take Profit 🚀"
reason = "Take Profit"
if action == "SELL":
print(f"🚨 Risk Management: {reason} - {name} (Qty: {qty}, Yield: {yld}%)")
# 전량 매도
print(f"[Bot] Risk Management: {reason} - {name} (Qty: {qty}, Yield: {yld}%)")
res = self.kis.sell_stock(code, qty)
if res and res.get("status"):
self.messenger.send_message(f"🛡️ **[Risk SELL]** {name}\n"
f" • 사유: {reason}\n"
f" • 수량: {qty}\n"
f" • 수익률: {yld}%")
self.messenger.send_message(
f"🔵 <b>[Risk SELL]</b> {name}\n"
f" Reason: {reason}\n"
f" Qty: {qty}\n"
f" Yield: <code>{yld}%</code>")
self.daily_trade_history.append({
"action": "SELL",
"name": name,
"qty": qty,
"price": stock.get('current_price'),
"yield": yld
"action": "SELL", "name": name, "qty": qty,
"price": stock.get('current_price'), "yield": yld
})
self.save_trade_history()
else:
print(f"❌ Sell Failed for {name}: {res}")
# 분석 실행 (병렬 처리)
analysis_tasks = []
news_data = self.news.get_market_news()
# [수정] 실시간 잔고 추적용 변수 (매수 시 차감)
news_data = await self.news.get_market_news_async()
# 실시간 잔고 추적용 변수 (매수 시 차감)
tracking_deposit = int(balance.get("deposit", 0))
try:
for ticker, name in target_dict.items():
prices = self.kis.get_daily_price(ticker)
if not prices: continue
# [신규] 외인 수급 분석
if not prices:
continue
# 외인 수급 분석
investor_trend = self.kis.get_investor_trend(ticker)
future = self.executor.submit(analyze_stock_process, ticker, prices, news_data, investor_trend)
future = self.executor.submit(
analyze_stock_process, ticker, prices, news_data, investor_trend)
analysis_tasks.append(future)
# 결과 처리
loop = asyncio.get_event_loop()
for future in analysis_tasks:
try:
res = future.result()
res = await loop.run_in_executor(None, future.result)
ticker_name = target_dict.get(res['ticker'], 'Unknown')
print(f"📊 [{ticker_name}] Score: {res['score']:.2f} ({res['decision']})")
print(f"[Bot] [{ticker_name}] Score: {res['score']:.2f} ({res['decision']})")
if res['decision'] == "BUY":
if is_crash: continue
# 매수 로직 (예수금 체크 추가)
current_qty = 0
if res['ticker'] in current_holdings:
current_qty = current_holdings[res['ticker']]['qty']
current_price = float(res['current_price'])
if current_price <= 0: continue
# 매수 수량 결정 (기본 1주, 추후 금액 기반으로 변경 가능)
qty = 1
required_amount = current_price * qty
# 예수금 확인
if tracking_deposit < required_amount:
print(f"💰 [Skip Buy] 예수금 부족 ({ticker_name}): 필요 {required_amount:,.0f} > 잔고 {tracking_deposit:,.0f}")
if is_crash:
continue
print(f"🚀 Buying {ticker_name} {qty}ea")
# 실제 주문
current_price = float(res['current_price'])
if current_price <= 0:
continue
qty = 1
required_amount = current_price * qty
# 예수금 확인
if tracking_deposit < required_amount:
print(f"[Bot] [Skip Buy] 예수금 부족 ({ticker_name}): "
f"필요 {required_amount:,.0f} > 잔고 {tracking_deposit:,.0f}")
continue
print(f"[Bot] Buying {ticker_name} {qty}ea")
order = self.kis.buy_stock(res['ticker'], qty)
if order.get("status"):
self.messenger.send_message(f"🚀 **[BUY]** {ticker_name} {qty}\n"
f" • 가격: {current_price:,.0f}")
self.messenger.send_message(
f"🔴 <b>[BUY]</b> {ticker_name} {qty}\n"
f" Price: <code>{current_price:,.0f}원</code>")
self.daily_trade_history.append({
"action": "BUY",
"name": ticker_name,
"qty": qty,
"price": current_price
"action": "BUY", "name": ticker_name,
"qty": qty, "price": current_price
})
self.save_trade_history()
# [중요] 가상 잔고 차감 (연속 매수 시 초과 방지)
tracking_deposit -= required_amount
elif res['decision'] == "SELL":
print(f"📉 Selling {ticker_name} (Simulation)")
# 매도 로직 (필요 시 추가)
print(f"[Bot] Selling {ticker_name} (Simulation)")
except BrokenProcessPool:
raise # 상위 레벨에서 처리
raise
except Exception as e:
print(f" Analysis Worker Error: {e}")
print(f"[Bot] Analysis Worker Error: {e}")
except BrokenProcessPool:
print("⚠️ [Bot] Worker Process Crashed (OOM, CUDA Error?). Restarting Executor...")
print("[Bot] Worker Process Crashed. Restarting Executor...")
self.restart_executor()
except KeyboardInterrupt:
raise
except Exception as e:
print(f" Cycle Loop Error: {e}")
print(f"[Bot] Cycle Loop Error: {e}")
def loop(self):
print(f"🤖 Bot Module Started (PID: {os.getpid()})")
self.messenger.send_message("🤖 **[Bot Started]** 리팩토링된 봇이 시작되었습니다.")
print(f"[Bot] Module Started (PID: {os.getpid()})")
self.messenger.send_message("[Bot Started] 리팩토링된 봇이 시작되었습니다.")
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
while True:
try:
self.run_cycle()
except Exception as e:
print(f"⚠️ Loop Error: {e}")
self.messenger.send_message(f"⚠️ Loop Error: {e}")
time.sleep(60)
except KeyboardInterrupt:
print("🛑 [Bot] Stopped by User.")
finally:
print("🛑 [Bot] Shutting down executor...")
self.executor.shutdown(wait=False)
print("✅ [Bot] Executor shutdown complete.")
# shutdown 시그널 체크
if self.shutdown_event and self.shutdown_event.is_set():
print("[Bot] Shutdown signal received.")
break
def start_telegram_command_server(self):
"""텔레그램 봇 프로세스 실행 (독립 프로세스)"""
script = os.path.join(os.getcwd(), "modules", "services", "telegram_bot", "runner.py")
if os.path.exists(script):
import subprocess
subprocess.Popen([sys.executable, script], creationflags=subprocess.CREATE_NEW_PROCESS_GROUP)
print("🚀 Telegram Command Server Started")
else:
print(f"⚠️ Telegram Bot Runner not found: {script}")
try:
loop.run_until_complete(self.run_cycle())
except Exception as e:
print(f"[Bot] Loop Error: {e}")
self.messenger.send_message(f"[Bot] Loop Error: {e}")
# 비동기 sleep (shutdown 체크하면서 대기)
for _ in range(60):
if self.shutdown_event and self.shutdown_event.is_set():
break
time.sleep(1)
except KeyboardInterrupt:
print("[Bot] Stopped by User.")
finally:
print("[Bot] Shutting down executor...")
self.executor.shutdown(wait=False)
if self.ipc:
self.ipc.cleanup()
loop.close()
print("[Bot] Executor shutdown complete.")