diff --git a/app/execution/kis_client.py b/app/execution/kis_client.py index 08722c5..99727ce 100644 --- a/app/execution/kis_client.py +++ b/app/execution/kis_client.py @@ -226,8 +226,10 @@ class KISClient: dry_run = os.getenv("DRY_RUN", "true").lower() == "true" if dry_run: - logger.info(f"[DRY_RUN] 매수 {ticker} {qty}주 @ {price or '시장가'}") - return {"dry_run": True, "ticker": ticker, "qty": qty} + price_info = await self.get_price(ticker) + current = price_info["current"] + logger.info(f"[DRY_RUN] 매수 {ticker} {qty}주 @ {current:,}원") + return {"dry_run": True, "ticker": ticker, "qty": qty, "entry_price": current} # 모의/실거래 TR 구분 tr_id = "VTTC0802U" if self.is_mock else "TTTC0802U" @@ -259,8 +261,10 @@ class KISClient: dry_run = os.getenv("DRY_RUN", "true").lower() == "true" if dry_run: - logger.info(f"[DRY_RUN] 매도 {ticker} {qty}주 @ {price or '시장가'}") - return {"dry_run": True, "ticker": ticker, "qty": qty} + price_info = await self.get_price(ticker) + current = price_info["current"] + logger.info(f"[DRY_RUN] 매도 {ticker} {qty}주 @ {current:,}원") + return {"dry_run": True, "ticker": ticker, "qty": qty, "exit_price": current} tr_id = "VTTC0801U" if self.is_mock else "TTTC0801U" diff --git a/app/main.py b/app/main.py index 54263eb..da3dd93 100644 --- a/app/main.py +++ b/app/main.py @@ -77,12 +77,13 @@ from app.config import ( class StockBot: def __init__(self): self.kis = KISClient() - self.executor = OrderExecutor(self.kis) - self.strategy = VolatilityBreakout() - self.positions = {} # ticker → {name, entry, qty, tp1_done, entry_time} - self.universe = [] # 감시 종목 리스트 - self.risk = None # RiskManager (잔고 확인 후 초기화) - self.running = False + self.executor = OrderExecutor(self.kis) + self.strategy = VolatilityBreakout() + self.positions = {} # ticker → {name, entry, qty, tp1_done, entry_time} + self.universe = [] # 감시 종목 리스트 + self.ticker_names = {} # ticker → 종목명 캐시 + self.risk = None # RiskManager (잔고 확인 후 초기화) + self.running = False mode = "모의투자" if self.kis.is_mock else "실거래" dry = " [DRY_RUN]" if os.getenv("DRY_RUN","true")=="true" else "" @@ -109,12 +110,28 @@ class StockBot: # 유니버스 갱신 (08:30) # ───────────────────────────────────────── + # ETF/ETN/인버스/레버리지 종목 필터 + _ETF_KEYWORDS = ('인버스', '레버리지', '선물', 'KODEX', 'TIGER', 'KBSTAR', + 'HANARO', 'ARIRANG', 'KOSEF', 'SOL', 'ACE', 'RISE', 'PLUS') + + @staticmethod + def _is_etf(ticker: str, name: str) -> bool: + if ticker.startswith('Q') or len(ticker) != 6: # ETN or 비정상 코드 + return True + return any(kw in name for kw in StockBot._ETF_KEYWORDS) + async def update_universe(self): """종목 풀 갱신 + 전일 데이터 수집""" logger.info("유니버스 갱신 시작") try: - rank = await self.kis.get_volume_rank(top_n=MAX_UNIVERSE) - tickers = [r["ticker"] for r in rank] + # ETF 필터 후 MAX_UNIVERSE 확보 위해 여유분 요청 + rank = await self.kis.get_volume_rank(top_n=MAX_UNIVERSE + 20) + + # 종목명 캐시 갱신 + ETF 필터 + for r in rank: + self.ticker_names[r["ticker"]] = r["name"] + tickers = [r["ticker"] for r in rank + if not self._is_etf(r["ticker"], r["name"])] ctx = self.strategy.context blacklist = ctx.get("blacklist_tickers", []) @@ -127,7 +144,7 @@ class StockBot: )[:MAX_UNIVERSE] self.universe = tickers - logger.info(f"유니버스: {len(tickers)}종목") + logger.info(f"유니버스: {len(tickers)}종목 (ETF 제외)") # 전일 날짜 계산 from datetime import timedelta @@ -246,7 +263,7 @@ class StockBot: try: price_info = await self.kis.get_price(ticker) # rate limiter가 자동 throttle current = price_info["current"] - name = price_info.get("name", ticker) + name = self.ticker_names.get(ticker, ticker) signal = self.strategy.check_entry( ticker=ticker,