매매 성과 평가지표 시스템 구현
- modules/utils/performance_db.py 신규: 일별 자산 스냅샷(16:00~16:30) 및 매매 기록 영구 저장 (PerformanceDB 클래스) - modules/analysis/evaluator.py 신규: Sharpe/Sortino/MDD/Alpha 등 16개 지표 산출, S~F 등급 시스템, Ollama 5명 전문가 패널, 텔레그램 HTML 주간 보고서 (PerformanceEvaluator 클래스) - modules/bot.py 수정: BUY/SELL 시 perf_db 기록 강화, 금요일 15:35 주간 평가 자동 실행, IPC 'evaluate' 명령 처리 - modules/services/telegram_bot/server.py 수정: /evaluate 명령어 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
159
modules/bot.py
159
modules/bot.py
@@ -13,6 +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.utils.performance_db import PerformanceDB
|
||||
from modules.strategy.process import analyze_stock_process, calculate_position_size
|
||||
|
||||
try:
|
||||
@@ -47,10 +48,6 @@ class AutoTradingBot:
|
||||
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()
|
||||
@@ -95,6 +92,12 @@ class AutoTradingBot:
|
||||
self.history_file = Config.HISTORY_FILE
|
||||
self.load_trade_history()
|
||||
|
||||
# 7-1. 성과 DB 및 평가 플래그
|
||||
self.perf_db = PerformanceDB()
|
||||
self.weekly_eval_sent = False
|
||||
self._snapshot_taken_today = False
|
||||
self._pending_evaluate = False
|
||||
|
||||
# 8. AI 하드웨어 점검
|
||||
from modules.analysis.deep_learning import PricePredictor
|
||||
PricePredictor.verify_hardware()
|
||||
@@ -130,6 +133,49 @@ class AutoTradingBot:
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
def _take_daily_snapshot(self, macro_status, balance):
|
||||
"""일별 자산 스냅샷을 perf_db에 저장 (09:05~09:15 호출)."""
|
||||
try:
|
||||
total_eval_snap = int(balance.get("total_eval", 0))
|
||||
deposit_snap = int(balance.get("deposit", 0))
|
||||
holdings_count_snap = len([
|
||||
h for h in balance.get("holdings", [])
|
||||
if int(h.get("qty", 0)) > 0
|
||||
])
|
||||
|
||||
# KOSPI 현재가 (macro_status 지표에서 추출)
|
||||
kospi_close = None
|
||||
try:
|
||||
indicators = macro_status.get("indicators", {})
|
||||
kospi_price = float(indicators.get("KOSPI", {}).get("price", 0))
|
||||
if kospi_price > 0:
|
||||
kospi_close = kospi_price
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.perf_db.save_daily_snapshot(
|
||||
total_eval_snap, deposit_snap, holdings_count_snap, kospi_close)
|
||||
self._snapshot_taken_today = True
|
||||
except Exception as e:
|
||||
print(f"[Bot] Daily snapshot error: {e}")
|
||||
|
||||
async def _run_weekly_evaluation(self):
|
||||
"""주간 성과 평가 실행 후 텔레그램으로 전송."""
|
||||
try:
|
||||
from modules.analysis.evaluator import PerformanceEvaluator
|
||||
evaluator = PerformanceEvaluator()
|
||||
loop = asyncio.get_running_loop()
|
||||
# Ollama 호출이 동기 블로킹이므로 executor에서 실행
|
||||
report = await loop.run_in_executor(None, evaluator.generate_weekly_report)
|
||||
if len(report) > 4000:
|
||||
report = report[:4000] + "\n... (일부 생략)"
|
||||
self.messenger.send_message(report)
|
||||
self.weekly_eval_sent = True
|
||||
print("[Bot] Weekly evaluation report sent.")
|
||||
except Exception as e:
|
||||
print(f"[Bot] Weekly evaluation error: {e}")
|
||||
self.messenger.send_message(f"[Bot] 주간 평가 오류: {e}")
|
||||
|
||||
def _load_peak_prices(self):
|
||||
"""트레일링 스탑용 최고가 데이터 로드"""
|
||||
peak_file = os.path.join(Config.DATA_DIR, "peak_prices.json")
|
||||
@@ -251,12 +297,20 @@ class AutoTradingBot:
|
||||
except Exception as e:
|
||||
self.messenger.send_message(f"Watchlist update failed: {e}")
|
||||
|
||||
elif command == 'evaluate':
|
||||
self._pending_evaluate = True
|
||||
|
||||
async def run_cycle(self):
|
||||
now = datetime.now()
|
||||
|
||||
# 0. 명령 큐 폴링
|
||||
self._process_commands()
|
||||
|
||||
# 0-1. 즉시 평가 요청 처리 (IPC 'evaluate' 명령)
|
||||
if self._pending_evaluate:
|
||||
self._pending_evaluate = False
|
||||
await self._run_weekly_evaluation()
|
||||
|
||||
# 1. 거시경제 분석
|
||||
macro_status = MacroAnalyzer.get_macro_status(self.kis)
|
||||
self.last_macro_status = macro_status
|
||||
@@ -316,6 +370,8 @@ class AutoTradingBot:
|
||||
self.daily_trade_history = []
|
||||
self.save_trade_history()
|
||||
self.report_sent = False
|
||||
self.weekly_eval_sent = False
|
||||
self._snapshot_taken_today = False
|
||||
self.discovered_stocks.clear()
|
||||
self.watchlist_updated_today = False
|
||||
# 전일 최고가 초기화 (보유하지 않는 종목)
|
||||
@@ -328,9 +384,28 @@ class AutoTradingBot:
|
||||
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()
|
||||
# 일별 스냅샷 (16:00~16:30, 당일 최종 포트폴리오 가치 기록)
|
||||
if now.hour == 16 and now.minute <= 30 and not self._snapshot_taken_today:
|
||||
try:
|
||||
balance_snap = self.kis.get_balance()
|
||||
self._take_daily_snapshot(macro_status, balance_snap)
|
||||
except Exception as e:
|
||||
print(f"[Bot] Snapshot error: {e}")
|
||||
# 주간 평가 (금요일 15:35~15:45, 장 마감 직후)
|
||||
if (now.weekday() == 4 and now.hour == 15
|
||||
and 35 <= now.minute <= 45 and not self.weekly_eval_sent):
|
||||
await self._run_weekly_evaluation()
|
||||
# 장 외 시간에는 서킷 브레이커도 리셋
|
||||
self.monitor.reset_circuit()
|
||||
print("[Bot] Market Closed. Waiting...")
|
||||
return
|
||||
|
||||
# [서킷 브레이커] CPU 과부하 시 분석 사이클 일시 중단
|
||||
if self.monitor.is_cpu_critical():
|
||||
print("[Bot] ⛔ CPU Circuit Breaker 발동 중. 분석 사이클 스킵.")
|
||||
return
|
||||
|
||||
cycle_start_time = time.time()
|
||||
print(f"[Bot] Cycle Start: {now.strftime('%H:%M:%S')}")
|
||||
|
||||
# 7. 종목 분석 및 매매
|
||||
@@ -365,14 +440,30 @@ class AutoTradingBot:
|
||||
|
||||
tracking_deposit = int(balance.get("deposit", 0))
|
||||
|
||||
# [v3.0] 비동기 OHLCV + 투자자 동향 배치 조회
|
||||
tickers_list = list(target_dict.keys())
|
||||
ohlcv_batch = {}
|
||||
investor_batch = {}
|
||||
|
||||
if self.kis_async and tickers_list:
|
||||
try:
|
||||
print(f"[Bot] 비동기 OHLCV 배치 조회: {len(tickers_list)}종목")
|
||||
ohlcv_batch = await self.kis_async.get_daily_ohlcv_batch(tickers_list)
|
||||
investor_batch = await self.kis_async.get_investor_trends_batch(tickers_list)
|
||||
except Exception as e:
|
||||
print(f"[Bot] 비동기 배치 조회 실패: {e} -> 동기 fallback")
|
||||
ohlcv_batch = {}
|
||||
investor_batch = {}
|
||||
|
||||
try:
|
||||
for ticker, name in target_dict.items():
|
||||
prices = self.kis.get_daily_price(ticker)
|
||||
if not prices:
|
||||
# OHLCV 데이터 획득 (배치 결과 우선, 실패 시 동기 fallback)
|
||||
ohlcv_data = ohlcv_batch.get(ticker)
|
||||
if not ohlcv_data or not ohlcv_data.get('close'):
|
||||
ohlcv_data = self.kis.get_daily_ohlcv(ticker)
|
||||
if not ohlcv_data or not ohlcv_data.get('close'):
|
||||
continue
|
||||
|
||||
investor_trend = self.kis.get_investor_trend(ticker)
|
||||
|
||||
# [v2.0] 보유 정보 전달 (분석 워커에서 동적 손절/익절 사용)
|
||||
holding_info = None
|
||||
if ticker in current_holdings:
|
||||
@@ -385,16 +476,22 @@ class AutoTradingBot:
|
||||
'peak_price': self.peak_prices.get(ticker, float(h.get('current_price', 0)))
|
||||
}
|
||||
|
||||
# investor_trend fallback
|
||||
investor_trend = investor_batch.get(ticker)
|
||||
if investor_trend is None:
|
||||
investor_trend = self.kis.get_investor_trend(ticker)
|
||||
|
||||
future = self.executor.submit(
|
||||
analyze_stock_process, ticker, prices, news_data,
|
||||
analyze_stock_process, ticker, ohlcv_data, news_data,
|
||||
investor_trend, macro_status, holding_info)
|
||||
analysis_tasks.append(future)
|
||||
|
||||
# 결과 처리
|
||||
loop = asyncio.get_event_loop()
|
||||
loop = asyncio.get_running_loop()
|
||||
for future in analysis_tasks:
|
||||
try:
|
||||
res = await loop.run_in_executor(None, future.result)
|
||||
# 240초 타임아웃: LSTM 학습 + Ollama 추론 시간 고려
|
||||
res = await loop.run_in_executor(None, lambda f=future: f.result(240))
|
||||
ticker = res['ticker']
|
||||
ticker_name = target_dict.get(ticker, 'Unknown')
|
||||
print(f"[Bot] [{ticker_name}] Score: {res['score']:.2f} ({res['decision']})"
|
||||
@@ -458,6 +555,24 @@ class AutoTradingBot:
|
||||
"reason": reason
|
||||
})
|
||||
self.save_trade_history()
|
||||
|
||||
# 성과 DB 기록
|
||||
pred = res.get("prediction") or {}
|
||||
self.perf_db.save_trade_record(
|
||||
action="BUY", ticker=ticker, name=ticker_name,
|
||||
qty=qty, price=current_price,
|
||||
scores_dict={
|
||||
"tech": res.get("tech", 0.0),
|
||||
"sentiment": res.get("sentiment", 0.0),
|
||||
"lstm_score": res.get("lstm_score", 0.0),
|
||||
"score": res.get("score", 0.0),
|
||||
"ai_confidence": res.get("ai_confidence", 0.5),
|
||||
"prediction_change": pred.get("change_rate", 0.0)
|
||||
},
|
||||
reason=reason,
|
||||
macro_state=macro_status.get("status", "SAFE")
|
||||
)
|
||||
|
||||
tracking_deposit -= required_amount
|
||||
|
||||
# 최고가 초기 설정
|
||||
@@ -484,14 +599,18 @@ class AutoTradingBot:
|
||||
f" Reason: {reason}")
|
||||
|
||||
self.messenger.send_message(msg)
|
||||
sell_price = float(h.get('current_price', 0))
|
||||
self.daily_trade_history.append({
|
||||
"action": "SELL", "name": ticker_name,
|
||||
"qty": qty, "price": float(h.get('current_price', 0)),
|
||||
"qty": qty, "price": sell_price,
|
||||
"yield": yld, "profit": profit_loss,
|
||||
"reason": reason
|
||||
})
|
||||
self.save_trade_history()
|
||||
|
||||
# 성과 DB 매도 결과 기록
|
||||
self.perf_db.close_trade(ticker, sell_price, yld)
|
||||
|
||||
# 최고가 기록 삭제
|
||||
if ticker in self.peak_prices:
|
||||
del self.peak_prices[ticker]
|
||||
@@ -510,11 +629,21 @@ class AutoTradingBot:
|
||||
except Exception as e:
|
||||
print(f"[Bot] Cycle Loop Error: {e}")
|
||||
|
||||
# 사이클 소요시간 로깅 (120초 초과 시 경고)
|
||||
cycle_elapsed = time.time() - cycle_start_time
|
||||
if cycle_elapsed > 120:
|
||||
print(f"[Bot] ⚠️ 사이클 소요 {cycle_elapsed:.0f}초 (120초 초과) → LSTM 쿨다운 활성화 권장")
|
||||
else:
|
||||
print(f"[Bot] Cycle Done: {cycle_elapsed:.1f}초")
|
||||
|
||||
def loop(self):
|
||||
print(f"[Bot] Module Started (PID: {os.getpid()}) [v2.0]")
|
||||
print(f"[Bot] Module Started (PID: {os.getpid()}) [v3.0]")
|
||||
self.messenger.send_message(
|
||||
"🚀 [Bot Started v2.0]\n"
|
||||
"개선사항: 동적 손절/익절, 트레일링 스탑, 포지션 사이징, 분석 기반 매도")
|
||||
"🚀 <b>[Bot Started v3.0]</b>\n"
|
||||
f"✅ LSTM 쿨다운: {Config.LSTM_COOLDOWN//60}분\n"
|
||||
f"✅ AI 모델: {Config.OLLAMA_MODEL}\n"
|
||||
f"✅ CPU 서킷브레이커: {Config.CPU_CIRCUIT_BREAKER_THRESHOLD}% 기준\n"
|
||||
"✅ 동적 손절/익절, 트레일링 스탑, 포지션 사이징")
|
||||
|
||||
# 최고가 데이터 로드
|
||||
self._load_peak_prices()
|
||||
|
||||
Reference in New Issue
Block a user