Files
ai-trade/modules/bot.py

405 lines
15 KiB
Python

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"📅 <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:
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📊 <b>[Holdings]</b>\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']}: <code>{yld_str}%</code>\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("🚨 <b>[MARKET CRASH ALERT]</b> 시장 급락 감지! 매수 중단.")
self.is_macro_warning_sent = True
else:
if self.is_macro_warning_sent:
self.messenger.send_message("🌤️ <b>[MARKET RECOVERY]</b> 시장 안정화.")
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"🔵 <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
})
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"🔴 <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
})
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.")