주식 트레이드 강화 전략 추가

This commit is contained in:
2026-02-17 01:38:35 +09:00
parent 9dbf6e6791
commit 4d41405ac4
6 changed files with 1352 additions and 434 deletions

View File

@@ -13,7 +13,7 @@ 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
from modules.strategy.process import analyze_stock_process, calculate_position_size
try:
from theme_manager import ThemeManager
@@ -31,11 +31,21 @@ def init_worker():
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()
# GPU 경합 방지: 워커 1개만 사용 (LSTM 학습이 GPU 독점)
self.executor = ProcessPoolExecutor(max_workers=1, initializer=init_worker)
try:
list(self.executor.map(lambda x: x, range(1)))
@@ -56,6 +66,13 @@ class AutoTradingBot:
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
@@ -113,6 +130,33 @@ class AutoTradingBot:
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
@@ -120,20 +164,46 @@ class AutoTradingBot:
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"
deposit = int(balance.get("deposit", 0))
report = (f"📅 <b>[Daily Closing Report]</b>\n"
f"💰 <b>Total Asset:</b> <code>{total_eval:,}원</code>\n"
f"💵 <b>Cash:</b> <code>{deposit:,}원</code>\n"
f"📜 <b>Trades Today:</b> <code>{len(self.daily_trade_history)}건</code>\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 "🔵"
report += f"{icon} <b>{action}</b> {trade['name']} {trade['qty']}\n"
qty = trade.get('qty', 0)
price = trade.get('price', 0)
reason = trade.get('reason', '')
report += f"{icon} <b>{action}</b> {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" | 실현손익: <code>{total_profit:,.0f}원</code>"
report += "\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))
profit_loss = int(stock.get('profit_loss', 0))
if yld > 0:
icon = "🔴"
yld_str = f"+{yld}"
@@ -143,8 +213,9 @@ class AutoTradingBot:
else:
icon = ""
yld_str = f"{yld}"
report += f"{icon} {stock['name']}: <code>{yld_str}%</code>\n"
report += (f"{icon} {stock['name']}: <code>{yld_str}%</code> "
f"(<code>{profit_loss:+,}원</code>)\n")
self.messenger.send_message(report)
self.report_sent = True
@@ -170,7 +241,6 @@ class AutoTradingBot:
if command == 'restart':
self.messenger.send_message("[Bot] Restart requested via Telegram.")
# executor 재시작
self.restart_executor()
elif command == 'update_watchlist':
@@ -189,11 +259,23 @@ class AutoTradingBot:
# 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("🚨 <b>[MARKET CRASH ALERT]</b> 시장 급락 감지! 매수 중단.")
self.messenger.send_message(
"🚨 <b>[MARKET CRASH ALERT]</b>\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(
"⚠️ <b>[MARKET CAUTION]</b>\n"
"시장 불안정. 보수적 매매 모드 전환.\n"
f"Risk Score: {macro_status['risk_score']}")
self.is_macro_warning_sent = True
else:
if self.is_macro_warning_sent:
@@ -236,6 +318,8 @@ class AutoTradingBot:
self.report_sent = False
self.discovered_stocks.clear()
self.watchlist_updated_today = False
# 전일 최고가 초기화 (보유하지 않는 종목)
self._load_peak_prices()
# 5. 시스템 감시 (3분 간격)
self.monitor.check_health()
@@ -252,52 +336,33 @@ class AutoTradingBot:
# 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")
name = stock.get("name")
qty = int(stock.get("qty", 0))
yld = float(stock.get("yield", 0.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)
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()
# [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:
@@ -306,11 +371,23 @@ class AutoTradingBot:
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)
analyze_stock_process, ticker, prices, news_data,
investor_trend, macro_status, holding_info)
analysis_tasks.append(future)
# 결과 처리
@@ -318,41 +395,107 @@ class AutoTradingBot:
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']})")
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
qty = 1
# [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:
print(f"[Bot] [Skip Buy] 예수금 부족 ({ticker_name}): "
f"필요 {required_amount:,.0f} > 잔고 {tracking_deposit:,.0f}")
continue
# 수량 줄여서 재시도
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")
order = self.kis.buy_stock(res['ticker'], qty)
print(f"[Bot] Buying {ticker_name} {qty}ea @ ~{current_price:,.0f}")
order = self.kis.buy_stock(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>")
reason = res.get('decision_reason', '')
sl_tp = res.get('sl_tp', {})
msg = (f"🔴 <b>[BUY]</b> {ticker_name} {qty}\n"
f" Price: <code>{current_price:,.0f}원</code>\n"
f" Score: <code>{res['score']:.2f}</code>\n"
f" SL: <code>{sl_tp.get('stop_loss_pct', -5):.1f}%</code>"
f" | TP: <code>{sl_tp.get('take_profit_pct', 8):.1f}%</code>"
f" | Trail: <code>{sl_tp.get('trailing_stop_pct', 3):.1f}%</code>")
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
"qty": qty, "price": current_price,
"score": res['score'],
"reason": reason
})
self.save_trade_history()
tracking_deposit -= required_amount
elif res['decision'] == "SELL":
print(f"[Bot] Selling {ticker_name} (Simulation)")
# 최고가 초기 설정
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"🔵 <b>[SELL]</b> {ticker_name} {qty}\n"
f" Yield: <code>{yld:.1f}%</code>\n"
f" P&L: <code>{profit_loss:+,}원</code>\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
@@ -368,15 +511,19 @@ class AutoTradingBot:
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()}) [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:
# shutdown 시그널 체크
if self.shutdown_event and self.shutdown_event.is_set():
print("[Bot] Shutdown signal received.")
break
@@ -387,7 +534,6 @@ class AutoTradingBot:
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