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, calculate_position_size
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:
"""
[v2.0] 개선된 자동매매 봇
주요 개선사항:
1. ATR 기반 동적 손절/익절 + 트레일링 스탑
2. 변동성 기반 포지션 사이징 (1주 고정 → 동적 수량)
3. 보유종목 분석 기반 매도 (score 기반 SELL 판단)
4. 매크로 상태를 분석 워커에 전달 (동적 임계값)
5. 최고가 추적 (트레일링 스탑용)
6. 상세한 매매 로그 및 텔레그램 알림
"""
def __init__(self, ipc_lock=None, command_queue=None, shutdown_event=None):
# 1. 서비스 초기화
self.kis = KISClient()
self.news = AsyncNewsCollector()
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
# [v2.0] 트레일링 스탑용 최고가 추적
# {ticker: peak_price}
self.peak_prices = {}
# [v2.0] 최근 매크로 상태 캐싱
self.last_macro_status = None
# 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 _load_peak_prices(self):
"""트레일링 스탑용 최고가 데이터 로드"""
peak_file = os.path.join(Config.DATA_DIR, "peak_prices.json")
if os.path.exists(peak_file):
try:
with open(peak_file, "r", encoding="utf-8") as f:
self.peak_prices = json.load(f)
except Exception:
self.peak_prices = {}
def _save_peak_prices(self):
"""트레일링 스탑용 최고가 데이터 저장"""
peak_file = os.path.join(Config.DATA_DIR, "peak_prices.json")
try:
with open(peak_file, "w", encoding="utf-8") as f:
json.dump(self.peak_prices, f, indent=2)
except Exception:
pass
def _update_peak_price(self, ticker, current_price):
"""보유 종목의 최고가 갱신"""
if ticker not in self.peak_prices:
self.peak_prices[ticker] = current_price
elif current_price > self.peak_prices[ticker]:
self.peak_prices[ticker] = current_price
print(f" 📈 [Peak Updated] {ticker}: {current_price:,.0f}")
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))
deposit = int(balance.get("deposit", 0))
report = (f"📅 [Daily Closing Report]\n"
f"💰 Total Asset: {total_eval:,}원\n"
f"💵 Cash: {deposit:,}원\n"
f"📜 Trades Today: {len(self.daily_trade_history)}건\n\n")
# 매매 내역
if self.daily_trade_history:
total_profit = 0
buy_count = 0
sell_count = 0
for trade in self.daily_trade_history:
action = trade['action']
icon = "🔴" if action == "BUY" else "🔵"
qty = trade.get('qty', 0)
price = trade.get('price', 0)
reason = trade.get('reason', '')
report += f"{icon} {action} {trade['name']} {qty}주 @ {price:,.0f}원"
if reason:
report += f" ({reason})"
report += "\n"
if action == "BUY":
buy_count += 1
else:
sell_count += 1
total_profit += trade.get('profit', 0)
report += f"\n📊 매수 {buy_count}건 / 매도 {sell_count}건"
if sell_count > 0:
report += f" | 실현손익: {total_profit:,.0f}원"
report += "\n"
# 보유종목 현황
if "holdings" in balance and balance["holdings"]:
report += "\n📊 [Holdings]\n"
for stock in balance["holdings"]:
yld = float(stock.get('yield', 0))
profit_loss = int(stock.get('profit_loss', 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}% "
f"({profit_loss:+,}원)\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.")
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)
self.last_macro_status = macro_status
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]\n"
"시장 급락 감지! 매수 중단, 매도 기준 상향.\n"
f"Risk Score: {macro_status['risk_score']}")
self.is_macro_warning_sent = True
elif macro_status['status'] == 'CAUTION':
if not self.is_macro_warning_sent:
self.messenger.send_message(
"⚠️ [MARKET CAUTION]\n"
"시장 불안정. 보수적 매매 모드 전환.\n"
f"Risk Score: {macro_status['risk_score']}")
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
# 전일 최고가 초기화 (보유하지 않는 종목)
self._load_peak_prices()
# 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()
# [v2.0] 잔고 조회 및 보유종목 맵 생성
balance = self.kis.get_balance()
current_holdings = {}
total_eval = int(balance.get("total_eval", 0))
if balance and "holdings" in balance:
for stock in balance["holdings"]:
code = stock.get("code")
qty = int(stock.get("qty", 0))
if qty > 0:
current_holdings[code] = stock
# 최고가 업데이트 (트레일링 스탑용)
current_price = float(stock.get('current_price', 0))
if current_price > 0:
self._update_peak_price(code, current_price)
# [v2.0] 보유종목도 분석 대상에 포함 (watchlist에 없어도)
for code in current_holdings:
if code not in target_dict:
name = current_holdings[code].get('name', 'Unknown')
target_dict[code] = name
print(f"[Bot] Added holding to analysis: {name} ({code})")
# 분석 실행 (병렬 처리)
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)
# [v2.0] 보유 정보 전달 (분석 워커에서 동적 손절/익절 사용)
holding_info = None
if ticker in current_holdings:
h = current_holdings[ticker]
holding_info = {
'qty': int(h.get('qty', 0)),
'yield': float(h.get('yield', 0.0)),
'purchase_price': float(h.get('purchase_price', 0)),
'current_price': float(h.get('current_price', 0)),
'peak_price': self.peak_prices.get(ticker, float(h.get('current_price', 0)))
}
future = self.executor.submit(
analyze_stock_process, ticker, prices, news_data,
investor_trend, macro_status, holding_info)
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 = res['ticker']
ticker_name = target_dict.get(ticker, 'Unknown')
print(f"[Bot] [{ticker_name}] Score: {res['score']:.2f} ({res['decision']})"
f" | SL:{res.get('sl_tp', {}).get('stop_loss_pct', 'N/A')}%"
f" TP:{res.get('sl_tp', {}).get('take_profit_pct', 'N/A')}%")
# ===== 매수 처리 =====
if res['decision'] == "BUY":
if is_crash:
print(f"[Bot] [Skip Buy] Market DANGER mode - {ticker_name}")
continue
current_price = float(res['current_price'])
if current_price <= 0:
continue
# [v2.0] 포지션 사이징 (동적 수량)
qty = calculate_position_size(
total_capital=total_eval if total_eval > 0 else tracking_deposit,
current_price=current_price,
volatility=res.get('volatility', 2.0),
score=res['score'],
ai_confidence=res.get('ai_confidence', 0.5)
)
if qty <= 0:
print(f"[Bot] [Skip Buy] Position size = 0 ({ticker_name})")
continue
required_amount = current_price * qty
# 예수금 확인
if tracking_deposit < required_amount:
# 수량 줄여서 재시도
qty = int(tracking_deposit / current_price)
if qty <= 0:
print(f"[Bot] [Skip Buy] 예수금 부족 ({ticker_name}): "
f"필요 {required_amount:,.0f} > 잔고 {tracking_deposit:,.0f}")
continue
required_amount = current_price * qty
print(f"[Bot] Buying {ticker_name} {qty}ea @ ~{current_price:,.0f}")
order = self.kis.buy_stock(ticker, qty)
if order.get("status"):
reason = res.get('decision_reason', '')
sl_tp = res.get('sl_tp', {})
msg = (f"🔴 [BUY] {ticker_name} {qty}주\n"
f" Price: {current_price:,.0f}원\n"
f" Score: {res['score']:.2f}\n"
f" SL: {sl_tp.get('stop_loss_pct', -5):.1f}%"
f" | TP: {sl_tp.get('take_profit_pct', 8):.1f}%"
f" | Trail: {sl_tp.get('trailing_stop_pct', 3):.1f}%")
if reason:
msg += f"\n Reason: {reason}"
self.messenger.send_message(msg)
self.daily_trade_history.append({
"action": "BUY", "name": ticker_name,
"qty": qty, "price": current_price,
"score": res['score'],
"reason": reason
})
self.save_trade_history()
tracking_deposit -= required_amount
# 최고가 초기 설정
self.peak_prices[ticker] = current_price
self._save_peak_prices()
# ===== 매도 처리 (v2.0 - 분석 기반 매도) =====
elif res['decision'] == "SELL" and ticker in current_holdings:
h = current_holdings[ticker]
qty = int(h.get('qty', 0))
yld = float(h.get('yield', 0.0))
profit_loss = int(h.get('profit_loss', 0))
if qty > 0:
print(f"[Bot] Selling {ticker_name} {qty}ea (Yield: {yld:.1f}%)")
sell_res = self.kis.sell_stock(ticker, qty)
if sell_res and sell_res.get("status"):
reason = res.get('decision_reason', 'AI Signal')
msg = (f"🔵 [SELL] {ticker_name} {qty}주\n"
f" Yield: {yld:.1f}%\n"
f" P&L: {profit_loss:+,}원\n"
f" Reason: {reason}")
self.messenger.send_message(msg)
self.daily_trade_history.append({
"action": "SELL", "name": ticker_name,
"qty": qty, "price": float(h.get('current_price', 0)),
"yield": yld, "profit": profit_loss,
"reason": reason
})
self.save_trade_history()
# 최고가 기록 삭제
if ticker in self.peak_prices:
del self.peak_prices[ticker]
self._save_peak_prices()
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()}) [v2.0]")
self.messenger.send_message(
"🚀 [Bot Started v2.0]\n"
"개선사항: 동적 손절/익절, 트레일링 스탑, 포지션 사이징, 분석 기반 매도")
# 최고가 데이터 로드
self._load_peak_prices()
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
while True:
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}")
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.")