Files
ai-trade/modules/bot.py

388 lines
16 KiB
Python

import time
import os
import sys
import json
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.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
class AutoTradingBot:
def __init__(self):
# 1. 서비스 초기화
self.kis = KISClient()
self.news = NewsCollector()
# [안정성] GPU OOM 방지를 위해 워커 수 축소 (4 -> 2)
# [식별] 워커 프로세스 이름 등록
self.executor = ProcessPoolExecutor(max_workers=2, initializer=init_worker)
# 워커 프로세스 워밍업 (PID 등록 유도)
try:
list(self.executor.map(lambda x: x, range(2)))
except: pass
self.messenger = TelegramMessenger()
self.theme_manager = ThemeManager()
self.ollama_monitor = OllamaManager() # GPU 모니터링용
# 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
# [IPC] BotIPC
try:
from modules.utils.ipc import BotIPC
self.ipc = BotIPC()
except ImportError:
print("⚠️ BotIPC module not found.")
self.ipc = None
# [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
# 기록 로드
self.history_file = Config.HISTORY_FILE
self.load_trade_history()
# AI 및 하드웨어 점검
from modules.analysis.deep_learning import PricePredictor
PricePredictor.verify_hardware()
pass
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}")
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}")
def load_watchlist(self):
try:
with open(Config.WATCHLIST_FILE, "r", encoding="utf-8") as f:
return json.load(f)
except:
return {}
def send_daily_report(self):
"""장 마감 리포트"""
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"
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"
if "holdings" in balance and balance["holdings"]:
report += "\n📊 **보유 현황**\n"
for stock in balance["holdings"]:
yld = stock.get('yield', 0)
icon = "🔺" if yld > 0 else "🔻"
report += f"{icon} {stock['name']}: `{yld}%`\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:
pass
# 워커 재시작
self.executor = ProcessPoolExecutor(max_workers=2, initializer=init_worker)
print("✅ [Bot] Process Executor Restarted.")
def run_cycle(self):
now = datetime.now()
# 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
# 2. 아침 업데이트 (08:00)
if now.hour == 8 and 0 <= now.minute < 5:
if not self.watchlist_updated_today and self.watchlist_manager:
print("🌅 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}")
# 3. 리셋 (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
# 4. 시스템 감시
self.monitor.check_health()
# 5. 장 운영 시간 체크
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...")
return
print(f"⏰ [Bot] Cycle Start: {now.strftime('%H:%M:%S')}")
# 6. 거시경제 분석 (완료됨)
# macro_status = ... (Moved to top)
# 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:
action = "SELL"
reason = "Stop Loss 📉"
elif yld >= 8.0:
action = "SELL"
reason = "Take Profit 🚀"
if action == "SELL":
print(f"🚨 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.daily_trade_history.append({
"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()
# [수정] 실시간 잔고 추적용 변수 (매수 시 차감)
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)
# 결과 처리
for future in analysis_tasks:
try:
res = future.result()
ticker_name = target_dict.get(res['ticker'], 'Unknown')
print(f"📊 [{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}")
continue
print(f"🚀 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.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"📉 Selling {ticker_name} (Simulation)")
# 매도 로직 (필요 시 추가)
except BrokenProcessPool:
raise # 상위 레벨에서 처리
except Exception as e:
print(f"❌ Analysis Worker Error: {e}")
except BrokenProcessPool:
print("⚠️ [Bot] Worker Process Crashed (OOM, CUDA Error?). Restarting Executor...")
self.restart_executor()
except KeyboardInterrupt:
raise
except Exception as e:
print(f"❌ Cycle Loop Error: {e}")
def loop(self):
print(f"🤖 Bot Module Started (PID: {os.getpid()})")
self.messenger.send_message("🤖 **[Bot Started]** 리팩토링된 봇이 시작되었습니다.")
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.")
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}")