feat(v3.2): DailyLedger + RiskGate + news_snapshot + backtest_runner
- DailyLedger: 당일 매수 회계 + 연속 손절 카운터 + 매수 신호 점수 한 객체로 집약 (bot.py 정리) - RiskGate: 테마당 동시 보유 + 노출 비율 상한 검증 (포트폴리오 레벨) - news_snapshot: 뉴스 SQLite 영구 저장 + 사후 감성 재검증 인프라 - backtest_runner: 전 종목 KIS 일봉 기반 백테스트 (Sharpe/MDD/Calmar) - bot.py 274 line 정리 (DailyLedger 분리) - backtest.py 173 line 재작성 (v3.2 next-bar 체결 + 거래세) - daily_launcher.py 폐기 (warmup_and_restart 통합) - .gitignore: .claude/ 제외 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
274
modules/bot.py
274
modules/bot.py
@@ -9,12 +9,15 @@ from datetime import datetime, timedelta
|
||||
from modules.config import Config
|
||||
from modules.services.kis import KISClient
|
||||
from modules.services.news import AsyncNewsCollector
|
||||
from modules.services.news_snapshot import NewsSnapshotStore
|
||||
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
|
||||
from modules.strategy.process import analyze_stock_process
|
||||
from modules.strategy.risk_gate import PortfolioRiskGate, RiskConfig
|
||||
from modules.strategy.daily_ledger import DailyLedger
|
||||
from modules.analysis.ensemble import get_ensemble
|
||||
|
||||
try:
|
||||
@@ -44,14 +47,24 @@ class AutoTradingBot:
|
||||
5. 최고가 추적 (트레일링 스탑용)
|
||||
6. 상세한 매매 로그 및 텔레그램 알림
|
||||
"""
|
||||
def __init__(self, ipc_lock=None, command_queue=None, shutdown_event=None, eod_event=None):
|
||||
def __init__(self, ipc_lock=None, command_queue=None, shutdown_event=None):
|
||||
# 1. 서비스 초기화
|
||||
self.kis = KISClient()
|
||||
self.news = AsyncNewsCollector()
|
||||
self.news_snapshot = NewsSnapshotStore("data/news_snapshots.db")
|
||||
self.news = AsyncNewsCollector(snapshot_store=self.news_snapshot)
|
||||
self.executor = ProcessPoolExecutor(max_workers=1, initializer=init_worker)
|
||||
|
||||
self.messenger = TelegramMessenger()
|
||||
self.theme_manager = ThemeManager()
|
||||
# 포트폴리오 리스크 게이트 (v3.2) — 테마 집중/동시보유 한도 검증
|
||||
self.risk_gate = PortfolioRiskGate(
|
||||
theme_lookup=lambda t: self.theme_manager.get_themes(t),
|
||||
config=RiskConfig(
|
||||
max_total_holdings=Config.MAX_TOTAL_HOLDINGS,
|
||||
max_tickers_per_theme=Config.MAX_TICKERS_PER_THEME,
|
||||
max_theme_exposure_ratio=Config.MAX_THEME_EXPOSURE_RATIO,
|
||||
),
|
||||
)
|
||||
self.ollama_monitor = OllamaManager()
|
||||
|
||||
# 2. 유틸리티 초기화
|
||||
@@ -71,23 +84,11 @@ class AutoTradingBot:
|
||||
# [v2.0] 최근 매크로 상태 캐싱
|
||||
self.last_macro_status = None
|
||||
|
||||
# [v2.1] 연속 손절 안전장치
|
||||
# 당일 손절 횟수가 임계치 초과 시 매수 일시 중단
|
||||
self._consecutive_stop_losses_today = 0
|
||||
self._buy_paused_until = None # datetime or None
|
||||
|
||||
# [v3.1] 사이클 간 당일 매수 금액 추적 (KIS T+2 미차감 문제 보완)
|
||||
self._today_buy_total = 0 # 당일 누적 매수 집행 금액 (원)
|
||||
self._today_buy_date = None # 날짜 리셋용
|
||||
|
||||
# [v3.1] 앙상블 학습용 매수 당시 신호 점수 보관 {ticker: {tech, sentiment, lstm}}
|
||||
# 매도 시 실현 수익률과 함께 ensemble.record_trade()에 전달
|
||||
self._buy_scores: dict = {}
|
||||
# [v3.2] 당일 상태 집약 (연속손절/당일매수/신호점수/플래그)
|
||||
self.ledger = DailyLedger()
|
||||
|
||||
# 4. 프로세스 관리
|
||||
self.shutdown_event = shutdown_event
|
||||
self.eod_event = eod_event # EOD 셧다운 시그널 (→ main_server 자동 종료)
|
||||
self._eod_shutdown_done = False # 당일 EOD 처리 완료 여부
|
||||
|
||||
# KRX 캘린더 (장 운영 여부 판단)
|
||||
from modules.utils.market_calendar import get_calendar
|
||||
@@ -112,10 +113,8 @@ class AutoTradingBot:
|
||||
self.history_file = Config.HISTORY_FILE
|
||||
self.load_trade_history()
|
||||
|
||||
# 7-1. 성과 DB 및 평가 플래그
|
||||
# 7-1. 성과 DB 및 수동 평가 요청 플래그 (주간/스냅샷 플래그는 ledger로 이관)
|
||||
self.perf_db = PerformanceDB()
|
||||
self.weekly_eval_sent = False
|
||||
self._snapshot_taken_today = False
|
||||
self._pending_evaluate = False
|
||||
|
||||
# 8. AI 하드웨어 점검
|
||||
@@ -175,90 +174,10 @@ class AutoTradingBot:
|
||||
|
||||
self.perf_db.save_daily_snapshot(
|
||||
total_eval_snap, deposit_snap, holdings_count_snap, kospi_close)
|
||||
self._snapshot_taken_today = True
|
||||
self.ledger.snapshot_taken = True
|
||||
except Exception as e:
|
||||
print(f"[Bot] Daily snapshot error: {e}")
|
||||
|
||||
async def _end_of_day_shutdown(self):
|
||||
"""
|
||||
[EOD] 장 마감 후 전체 학습 상태 저장 + 봇 프로세스 종료
|
||||
|
||||
저장 항목:
|
||||
1. 앙상블 가중치 & 매매 히스토리 (ensemble_history.json)
|
||||
2. 트레일링 스탑 최고가 (peak_prices.json)
|
||||
3. 일일 거래 기록 (daily_trade_history.json)
|
||||
4. 일별 자산 스냅샷 (perf_db)
|
||||
5. EOD 마커 파일 (data/.eod_date → Watchdog 재시작 차단)
|
||||
"""
|
||||
print("[Bot] ===== EOD 상태 저장 시작 =====")
|
||||
|
||||
# 1. 앙상블 가중치 강제 저장
|
||||
try:
|
||||
ensemble = get_ensemble()
|
||||
ensemble._save()
|
||||
print("[Bot] [EOD] 앙상블 가중치 저장 완료")
|
||||
except Exception as e:
|
||||
print(f"[Bot] [EOD] 앙상블 저장 오류: {e}")
|
||||
|
||||
# 2. 트레일링 스탑 최고가 저장
|
||||
try:
|
||||
self._save_peak_prices()
|
||||
print("[Bot] [EOD] 최고가 데이터 저장 완료")
|
||||
except Exception as e:
|
||||
print(f"[Bot] [EOD] 최고가 저장 오류: {e}")
|
||||
|
||||
# 3. 일일 거래 기록 저장
|
||||
try:
|
||||
self.save_trade_history()
|
||||
print(f"[Bot] [EOD] 거래 기록 저장 완료 ({len(self.daily_trade_history)}건)")
|
||||
except Exception as e:
|
||||
print(f"[Bot] [EOD] 거래 기록 저장 오류: {e}")
|
||||
|
||||
# 4. 일별 자산 스냅샷 (미완료 시)
|
||||
if not self._snapshot_taken_today:
|
||||
try:
|
||||
balance_snap = self.kis.get_balance()
|
||||
macro_cached = self.last_macro_status or {"indicators": {}}
|
||||
self._take_daily_snapshot(macro_cached, balance_snap)
|
||||
print("[Bot] [EOD] 자산 스냅샷 저장 완료")
|
||||
except Exception as e:
|
||||
print(f"[Bot] [EOD] 스냅샷 저장 오류: {e}")
|
||||
|
||||
# 5. EOD 마커 파일 기록 (Watchdog 재시작 차단)
|
||||
try:
|
||||
from pathlib import Path
|
||||
import datetime as _dt
|
||||
eod_file = Path(Config.DATA_DIR) / ".eod_date"
|
||||
eod_file.parent.mkdir(exist_ok=True)
|
||||
eod_file.write_text(str(_dt.date.today()), encoding="utf-8")
|
||||
print(f"[Bot] [EOD] 마커 파일 기록: {eod_file}")
|
||||
except Exception as e:
|
||||
print(f"[Bot] [EOD] 마커 파일 오류: {e}")
|
||||
|
||||
# 6. 텔레그램 알림
|
||||
try:
|
||||
today_trades = len(self.daily_trade_history)
|
||||
try:
|
||||
nxt = self._calendar.next_trading_open()
|
||||
next_str = nxt.strftime('%m/%d(%a) %H:%M')
|
||||
except Exception:
|
||||
next_str = "미정"
|
||||
self.messenger.send_message(
|
||||
f"[장 마감] EOD 상태 저장 완료\n"
|
||||
f"오늘 매매: {today_trades}건\n"
|
||||
f"다음 거래일: {next_str} KST 자동 시작"
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"[Bot] [EOD] 알림 오류: {e}")
|
||||
|
||||
print("[Bot] ===== EOD 상태 저장 완료 =====")
|
||||
|
||||
# 7. 종료 시그널
|
||||
if self.eod_event:
|
||||
self.eod_event.set() # main_server → 서버 프로세스 자동 종료
|
||||
if self.shutdown_event:
|
||||
self.shutdown_event.set() # 텔레그램 봇 등 자식 프로세스 종료
|
||||
|
||||
async def _run_weekly_evaluation(self):
|
||||
"""주간 성과 평가 실행 후 텔레그램으로 전송."""
|
||||
try:
|
||||
@@ -270,7 +189,7 @@ class AutoTradingBot:
|
||||
if len(report) > 4000:
|
||||
report = report[:4000] + "\n... (일부 생략)"
|
||||
self.messenger.send_message(report)
|
||||
self.weekly_eval_sent = True
|
||||
self.ledger.weekly_eval_sent = True
|
||||
print("[Bot] Weekly evaluation report sent.")
|
||||
except Exception as e:
|
||||
print(f"[Bot] Weekly evaluation error: {e}")
|
||||
@@ -465,22 +384,16 @@ class AutoTradingBot:
|
||||
except Exception as e:
|
||||
self.messenger.send_message(f"Update Failed: {e}")
|
||||
|
||||
# 4. 리셋 (09:00)
|
||||
# 4. 리셋 (09:00) — 일별 상태는 ledger.reset_if_new_day가 통합 관리
|
||||
if now.hour == 9 and now.minute < 5:
|
||||
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
|
||||
# 전일 최고가 초기화 (보유하지 않는 종목)
|
||||
self._load_peak_prices()
|
||||
# [v3.1] 당일 매수 추적 리셋
|
||||
self._today_buy_total = 0
|
||||
self._today_buy_date = now.date()
|
||||
self._buy_scores.clear() # 미매도 종목 신호 점수도 초기화
|
||||
print(f"[Bot] 일일 매수 추적 리셋 (날짜: {now.date()})")
|
||||
if self.ledger.reset_if_new_day(now):
|
||||
print(f"[Bot] 일일 장부 리셋 (날짜: {now.date()})")
|
||||
|
||||
# 5. 시스템 감시 (3분 간격)
|
||||
self.monitor.check_health()
|
||||
@@ -490,7 +403,7 @@ class AutoTradingBot:
|
||||
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:
|
||||
if now.hour == 16 and now.minute <= 30 and not self.ledger.snapshot_taken:
|
||||
try:
|
||||
balance_snap = self.kis.get_balance()
|
||||
self._take_daily_snapshot(macro_status, balance_snap)
|
||||
@@ -498,21 +411,12 @@ class AutoTradingBot:
|
||||
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):
|
||||
and 35 <= now.minute <= 45 and not self.ledger.weekly_eval_sent):
|
||||
await self._run_weekly_evaluation()
|
||||
|
||||
# [EOD 셧다운] 장 마감 후 Config.EOD_SHUTDOWN_BUFFER_MIN 분 경과 시 저장 후 종료
|
||||
eod_buffer = now.hour == 15 and now.minute >= (30 + Config.EOD_SHUTDOWN_BUFFER_MIN)
|
||||
eod_buffer = eod_buffer or (now.hour >= 16) # 16시 이후도 포함
|
||||
if eod_buffer and not self._eod_shutdown_done:
|
||||
self._eod_shutdown_done = True
|
||||
await self._end_of_day_shutdown()
|
||||
return
|
||||
|
||||
# 장 외 시간에는 서킷 브레이커도 리셋
|
||||
self.monitor.reset_circuit()
|
||||
if not self._eod_shutdown_done:
|
||||
print("[Bot] Market Closed. Waiting...")
|
||||
print("[Bot] Market Closed. Waiting...")
|
||||
return
|
||||
|
||||
# [서킷 브레이커] CPU 과부하 시 분석 사이클 일시 중단
|
||||
@@ -554,27 +458,15 @@ class AutoTradingBot:
|
||||
news_data = await self.news.get_market_news_async()
|
||||
|
||||
raw_deposit = int(balance.get("deposit", 0))
|
||||
# 날짜 전환 안전망 (09:00 리셋 블록에서 누락됐을 가능성 대비)
|
||||
self.ledger.reset_if_new_day(now)
|
||||
|
||||
# [v3.1] 사이클 간 누적 매수금액 추적 (KIS 모의투자 T+2 미차감 보완)
|
||||
# KIS API의 dnca_tot_amt(예수금)는 당일 매수를 즉시 차감하지 않아
|
||||
# 매 사이클마다 전체 잔고처럼 보이는 문제를 방지
|
||||
today = now.date()
|
||||
if self._today_buy_date != today:
|
||||
# 날짜 변경 시 리셋 (09:00 리셋 블록에서 이미 처리되지만 안전망으로 이중 체크)
|
||||
self._today_buy_total = 0
|
||||
self._today_buy_date = today
|
||||
|
||||
# KIS가 제공하는 금일매수금액이 있으면 그것을 우선 사용 (더 정확)
|
||||
kis_today_buy = int(balance.get("today_buy_amt", 0))
|
||||
if kis_today_buy > 0:
|
||||
# KIS 값이 유효하면 로컬 추적값과 최댓값으로 사용 (둘 다 참조)
|
||||
effective_today_buy = max(kis_today_buy, self._today_buy_total)
|
||||
else:
|
||||
effective_today_buy = self._today_buy_total
|
||||
|
||||
# 실제 사용 가능한 예수금 = KIS 예수금 - 당일 이미 집행한 매수금액
|
||||
effective_today_buy = self.ledger.effective_today_buy(kis_today_buy)
|
||||
tracking_deposit = self.ledger.available_deposit(
|
||||
raw_deposit, Config.MAX_DAILY_BUY_RATIO, kis_today_buy
|
||||
)
|
||||
max_daily_buy = int(raw_deposit * Config.MAX_DAILY_BUY_RATIO)
|
||||
tracking_deposit = max(0, min(raw_deposit, max_daily_buy) - effective_today_buy)
|
||||
|
||||
print(f"[Bot] 예수금: {raw_deposit:,}원 | 당일매수: {effective_today_buy:,}원 | "
|
||||
f"사용가능: {tracking_deposit:,}원 (한도 {max_daily_buy:,}원)")
|
||||
@@ -654,14 +546,10 @@ class AutoTradingBot:
|
||||
continue
|
||||
|
||||
# [v2.1] 연속 손절 후 매수 일시 중단 체크
|
||||
if self._buy_paused_until and datetime.now() < self._buy_paused_until:
|
||||
if self.ledger.is_buy_paused(datetime.now()):
|
||||
print(f"[Bot] [Skip Buy] 연속 손절 매수 중단 중 (재개: "
|
||||
f"{self._buy_paused_until.strftime('%H:%M')}) - {ticker_name}")
|
||||
f"{self.ledger.buy_paused_until.strftime('%H:%M')}) - {ticker_name}")
|
||||
continue
|
||||
elif self._buy_paused_until and datetime.now() >= self._buy_paused_until:
|
||||
self._buy_paused_until = None
|
||||
self._consecutive_stop_losses_today = 0
|
||||
print("[Bot] 매수 일시 중단 해제")
|
||||
|
||||
current_price = float(res['current_price'])
|
||||
if current_price <= 0:
|
||||
@@ -676,6 +564,31 @@ class AutoTradingBot:
|
||||
|
||||
required_amount = current_price * qty
|
||||
|
||||
# [v3.2] 포트폴리오 리스크 게이트 검증 (테마 집중/동시보유 상한)
|
||||
risk_holdings = [
|
||||
{"ticker": c, "eval_amount": int(float(h.get("current_price", 0))
|
||||
* int(h.get("qty", 0)))}
|
||||
for c, h in current_holdings.items()
|
||||
]
|
||||
risk_dec = self.risk_gate.evaluate_buy(
|
||||
ticker=ticker,
|
||||
candidate_amount=int(required_amount),
|
||||
current_holdings=risk_holdings,
|
||||
total_capital=max(total_eval, 1),
|
||||
)
|
||||
if not risk_dec.allowed:
|
||||
print(f"[Bot] [Skip Buy] RiskGate: {risk_dec.reason} ({ticker_name})")
|
||||
continue
|
||||
if risk_dec.max_allowed_amount < required_amount:
|
||||
new_qty = int(risk_dec.max_allowed_amount / current_price)
|
||||
if new_qty <= 0:
|
||||
print(f"[Bot] [Skip Buy] RiskGate 부분허용 금액 부족 ({ticker_name})")
|
||||
continue
|
||||
print(f"[Bot] RiskGate 부분허용: qty {qty}→{new_qty} "
|
||||
f"({risk_dec.reason})")
|
||||
qty = new_qty
|
||||
required_amount = current_price * qty
|
||||
|
||||
# 예수금 확인 (tracking_deposit는 당일 누적 매수 차감 후 가용액)
|
||||
if tracking_deposit < required_amount:
|
||||
qty = int(tracking_deposit / current_price)
|
||||
@@ -727,19 +640,16 @@ class AutoTradingBot:
|
||||
)
|
||||
|
||||
tracking_deposit -= required_amount
|
||||
# [v3.1] 사이클 간 추적 (KIS T+2 미차감 보완)
|
||||
self._today_buy_total += required_amount
|
||||
self.ledger.record_buy(
|
||||
ticker, int(required_amount),
|
||||
{"tech": res.get("tech", 0.5),
|
||||
"sentiment": res.get("sentiment", 0.5),
|
||||
"lstm": res.get("lstm_score", 0.5)},
|
||||
)
|
||||
buys_this_cycle += 1
|
||||
print(f"[Bot] 당일 누적 매수: {self._today_buy_total:,}원 "
|
||||
print(f"[Bot] 당일 누적 매수: {self.ledger.today_buy_total:,}원 "
|
||||
f"(잔여 예수금: {tracking_deposit:,}원)")
|
||||
|
||||
# [v3.1] 앙상블 학습용 매수 신호 점수 보관 (매도 시 record_trade에 활용)
|
||||
self._buy_scores[ticker] = {
|
||||
"tech": res.get("tech", 0.5),
|
||||
"sentiment": res.get("sentiment", 0.5),
|
||||
"lstm": res.get("lstm_score", 0.5),
|
||||
}
|
||||
|
||||
# 최고가 초기 설정
|
||||
self.peak_prices[ticker] = current_price
|
||||
self._save_peak_prices()
|
||||
@@ -777,7 +687,7 @@ class AutoTradingBot:
|
||||
self.perf_db.close_trade(ticker, sell_price, yld)
|
||||
|
||||
# [v3.1] 앙상블 학습 데이터 기록 (매수 시 저장한 신호 점수 + 실현 수익률)
|
||||
buy_sig = self._buy_scores.pop(ticker, None)
|
||||
buy_sig = self.ledger.pop_buy_scores(ticker)
|
||||
if buy_sig is not None:
|
||||
try:
|
||||
get_ensemble().record_trade(
|
||||
@@ -793,22 +703,17 @@ class AutoTradingBot:
|
||||
except Exception as _ee:
|
||||
print(f"[Bot] [Ensemble] record_trade 실패: {_ee}")
|
||||
|
||||
# [v2.1] 손절 횟수 추적 → 연속 3회 손절 시 매수 30분 일시 중단
|
||||
if yld < 0:
|
||||
self._consecutive_stop_losses_today += 1
|
||||
if self._consecutive_stop_losses_today >= 3:
|
||||
self._buy_paused_until = datetime.now() + timedelta(minutes=30)
|
||||
warn_msg = (
|
||||
f"⛔ <b>[매수 일시 중단]</b> 당일 손절 "
|
||||
f"{self._consecutive_stop_losses_today}회 → "
|
||||
f"30분간 매수 정지 (재개: "
|
||||
f"{self._buy_paused_until.strftime('%H:%M')})"
|
||||
)
|
||||
self.messenger.send_message(warn_msg)
|
||||
print(f"[Bot] 연속 손절 {self._consecutive_stop_losses_today}회 → 매수 30분 중단")
|
||||
else:
|
||||
# 수익 실현 시 연속 손절 카운터 리셋
|
||||
self._consecutive_stop_losses_today = 0
|
||||
# [v2.1] 손절 횟수 추적 → 연속 N회 손절 시 매수 일시 중단
|
||||
triggered = self.ledger.record_sell_outcome(yld, datetime.now())
|
||||
if triggered:
|
||||
warn_msg = (
|
||||
f"⛔ <b>[매수 일시 중단]</b> 당일 손절 "
|
||||
f"{self.ledger.consecutive_stop_losses}회 → "
|
||||
f"{self.ledger.stop_loss_pause_minutes}분간 매수 정지 "
|
||||
f"(재개: {self.ledger.buy_paused_until.strftime('%H:%M')})"
|
||||
)
|
||||
self.messenger.send_message(warn_msg)
|
||||
print(f"[Bot] 연속 손절 {self.ledger.consecutive_stop_losses}회 → 매수 일시 중단")
|
||||
|
||||
# 최고가 기록 삭제
|
||||
if ticker in self.peak_prices:
|
||||
@@ -838,27 +743,6 @@ class AutoTradingBot:
|
||||
def loop(self):
|
||||
print(f"[Bot] Module Started (PID: {os.getpid()}) [v3.1]")
|
||||
|
||||
# [캘린더 체크] 오늘이 휴장일이면 알림 후 즉시 EOD 종료
|
||||
if not self._calendar.is_trading_day():
|
||||
summary = self._calendar.status_summary()
|
||||
print(f"[Bot] 오늘은 휴장일 ({summary}) — 봇을 시작하지 않습니다.")
|
||||
self.messenger.send_message(
|
||||
f"[Bot] 오늘은 휴장일입니다.\n{summary}"
|
||||
)
|
||||
# EOD 마커 기록 후 종료
|
||||
try:
|
||||
from pathlib import Path
|
||||
import datetime as _dt
|
||||
eod_file = Path(Config.DATA_DIR) / ".eod_date"
|
||||
eod_file.write_text(str(_dt.date.today()), encoding="utf-8")
|
||||
except Exception:
|
||||
pass
|
||||
if self.eod_event:
|
||||
self.eod_event.set()
|
||||
if self.shutdown_event:
|
||||
self.shutdown_event.set()
|
||||
return
|
||||
|
||||
_llm_label = (
|
||||
f"Gemini ({Config.GEMINI_MODEL})"
|
||||
if Config.GEMINI_API_KEY
|
||||
|
||||
Reference in New Issue
Block a user