import asyncio import os 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 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 Exception: pass class AutoTradingBot: def __init__(self, ipc_lock=None, command_queue=None, shutdown_event=None): # 1. 서비스 초기화 self.kis = KISClient() 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(1))) except Exception: pass self.messenger = TelegramMessenger() self.theme_manager = ThemeManager() self.ollama_monitor = OllamaManager() # 2. 유틸리티 초기화 self.monitor = SystemMonitor(self.messenger, self.ollama_monitor) # 3. 상태 변수 self.daily_trade_history = [] self.discovered_stocks = set() self.is_macro_warning_sent = False self.watchlist_updated_today = False self.report_sent = False # 4. 프로세스 관리 self.shutdown_event = shutdown_event # 5. IPC (Shared Memory) try: from modules.utils.ipc import SharedIPC self.ipc = SharedIPC(lock=ipc_lock, command_queue=command_queue) except ImportError: print("[Bot] SharedIPC module not found.") self.ipc = None # 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() # 8. AI 하드웨어 점검 from modules.analysis.deep_learning import PricePredictor PricePredictor.verify_hardware() # 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: 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"[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 Exception: return {} def send_daily_report(self): if self.report_sent: return print("[Bot] Generating Daily Report...") balance = self.kis.get_balance() total_eval = int(balance.get("total_eval", 0)) report = f"📅 [Daily Closing Report]\n" \ f"💰 Total Asset: {total_eval:,}원\n" \ f"📜 Trades Today: {len(self.daily_trade_history)}건\n\n" if self.daily_trade_history: for trade in self.daily_trade_history: action = trade['action'] icon = "🔴" if action == "BUY" else "🔵" report += f"{icon} {action} {trade['name']} {trade['qty']}주\n" if "holdings" in balance and balance["holdings"]: report += "\n📊 [Holdings]\n" for stock in balance["holdings"]: 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']}: {yld_str}%\n" self.messenger.send_message(report) self.report_sent = True def restart_executor(self): print("[Bot] Restarting Process Executor...") try: self.executor.shutdown(wait=False) except Exception: pass self.executor = ProcessPoolExecutor(max_workers=1, initializer=init_worker) print("[Bot] Process Executor Restarted.") 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() # 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.is_macro_warning_sent = True else: if self.is_macro_warning_sent: self.messenger.send_message("🌤️ [MARKET RECOVERY] 시장 안정화.") self.is_macro_warning_sent = False # 2. IPC 상태 업데이트 if self.ipc: try: balance = self.kis.get_balance() gpu_status = self.ollama_monitor.get_gpu_status() watchlist = self.load_watchlist() self.ipc.write_status({ 'balance': balance, 'gpu': gpu_status, 'watchlist': watchlist, 'discovered_stocks': list(self.discovered_stocks), 'is_macro_warning': self.is_macro_warning_sent, 'macro_indices': macro_status['indicators'], 'themes': {} }) except Exception: pass # 3. 아침 업데이트 (08:00) if now.hour == 8 and 0 <= now.minute < 5: if not self.watchlist_updated_today and self.watchlist_manager: print("[Bot] Morning Update...") try: summary = self.watchlist_manager.update_watchlist_daily() self.messenger.send_message(summary) self.watchlist_updated_today = True except Exception as e: self.messenger.send_message(f"Update Failed: {e}") # 4. 리셋 (09:00) if now.hour == 9 and now.minute < 5: self.daily_trade_history = [] self.save_trade_history() self.report_sent = False self.discovered_stocks.clear() self.watchlist_updated_today = False # 5. 시스템 감시 (3분 간격) self.monitor.check_health() # 6. 장 운영 시간 체크 if not (9 <= now.hour < 15 or (now.hour == 15 and now.minute < 30)): if now.hour == 15 and now.minute >= 40: self.send_daily_report() print("[Bot] Market Closed. Waiting...") return print(f"[Bot] Cycle Start: {now.strftime('%H:%M:%S')}") # 7. 종목 분석 및 매매 target_dict = self.load_watchlist() # 보유 종목 리스크 관리 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 if qty <= 0: continue action = None reason = "" if yld <= -5.0: action = "SELL" reason = "Stop Loss" elif yld >= 8.0: action = "SELL" reason = "Take Profit" if action == "SELL": 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: {reason}\n" f" Qty: {qty}\n" f" Yield: {yld}%") self.daily_trade_history.append({ "action": "SELL", "name": name, "qty": qty, "price": stock.get('current_price'), "yield": yld }) self.save_trade_history() # 분석 실행 (병렬 처리) analysis_tasks = [] 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 # 외인 수급 분석 investor_trend = self.kis.get_investor_trend(ticker) 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 = await loop.run_in_executor(None, future.result) ticker_name = target_dict.get(res['ticker'], 'Unknown') print(f"[Bot] [{ticker_name}] Score: {res['score']:.2f} ({res['decision']})") if res['decision'] == "BUY": if is_crash: continue 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" Price: {current_price:,.0f}원") self.daily_trade_history.append({ "action": "BUY", "name": ticker_name, "qty": qty, "price": current_price }) self.save_trade_history() tracking_deposit -= required_amount elif res['decision'] == "SELL": print(f"[Bot] Selling {ticker_name} (Simulation)") except BrokenProcessPool: raise except Exception as e: print(f"[Bot] Analysis Worker Error: {e}") except BrokenProcessPool: print("[Bot] Worker Process Crashed. Restarting Executor...") self.restart_executor() except KeyboardInterrupt: raise except Exception as 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] 리팩토링된 봇이 시작되었습니다.") loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) try: while True: # shutdown 시그널 체크 if self.shutdown_event and self.shutdown_event.is_set(): print("[Bot] Shutdown signal received.") break 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.")