From 96b710baf0bb4e202578a89cdb70ef8979482c0d Mon Sep 17 00:00:00 2001 From: jongjae Date: Tue, 26 May 2026 14:10:54 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B3=B5=ED=9C=B4=EC=9D=BC=20OHLCV=20=EB=B2=84?= =?UTF-8?q?=EA=B7=B8=20=EC=88=98=EC=A0=95=20+=20=EC=A7=84=EC=9E=85=20?= =?UTF-8?q?=EC=8B=A0=ED=98=B8=20=EC=A7=84=EB=8B=A8=20=EB=A1=9C=EA=B7=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - OHLCV 조회를 단일일→7일 범위로 변경해 공휴일(대체공휴일 등) 자동 처리 (5/25 대체공휴일로 전 종목 목표가 0개 → 오늘 하루 종일 0건 원인) - 목표가 계산 결과 DEBUG→INFO 레벨 격상 (종목별 목표가·시가 표시) - 목표가 제외 이유 INFO 로그 추가 (전일 데이터 없음 / 거래대금 미달) - check_entries에 5분마다 진단 로그 추가 (신호 거절 이유 전 종목 출력) Co-Authored-By: Claude Sonnet 4.6 --- app/main.py | 50 +++++++++++++++++++++++------ app/strategy/volatility_breakout.py | 9 +++++- 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/app/main.py b/app/main.py index 5293c3e..3c9f713 100644 --- a/app/main.py +++ b/app/main.py @@ -98,6 +98,7 @@ class StockBot: self._midday_ctx_mtime : float = 0.0 self._midday_pos_mult : float = 1.0 # midday position_size_multiplier self._midday_loaded : bool = False + self._last_diag : float = 0.0 # 신호 진단 로그 마지막 시각 mode = "모의투자" if self.kis.is_mock else "실거래" dry = " [DRY_RUN]" if os.getenv("DRY_RUN","true")=="true" else "" @@ -262,12 +263,11 @@ class StockBot: self.universe = tickers logger.info(f"유니버스: {len(tickers)}종목 (ETF 제외)") - # 전일 날짜 계산 + # 최근 7일 범위 조회 → 공휴일·대체공휴일 자동 처리 from datetime import timedelta - today = datetime.now() - # 월요일이면 금요일로 - offset = 3 if today.weekday() == 0 else 1 - prev_date = (today - timedelta(days=offset)).strftime("%Y%m%d") + today = datetime.now() + start_dt = (today - timedelta(days=7)).strftime("%Y%m%d") + end_dt = (today - timedelta(days=1)).strftime("%Y%m%d") for ticker in self.universe: # 이미 전일 데이터 있으면 skip @@ -276,11 +276,11 @@ class StockBot: try: ohlcv = await self.kis.get_ohlcv_daily( ticker, - start=prev_date, - end=prev_date, + start=start_dt, + end=end_dt, ) if ohlcv: - prev = ohlcv[-1] + prev = ohlcv[-1] # 가장 최근 거래일 self.strategy.set_prev_data( ticker, high = prev["high"], @@ -288,6 +288,8 @@ class StockBot: amount= prev.get("amount", prev.get("volume", 0) * prev.get("close", 0)) ) + else: + logger.warning(f"전일 OHLCV 없음 {ticker} ({start_dt}~{end_dt})") except Exception as e: logger.warning(f"전일 데이터 실패 {ticker}: {e}") await asyncio.sleep(1.1) # 초당 2.5건으로 제한 @@ -302,16 +304,20 @@ class StockBot: async def calc_targets(self): """당일 시가 기반 목표가 계산""" logger.info("목표가 계산 시작") + valid_count = 0 for ticker in self.universe: try: price_info = await self.kis.get_price(ticker) self.strategy.set_today_open(ticker, price_info["open"]) target = self.strategy.get_target(ticker) + name = self.ticker_names.get(ticker, ticker) if target > 0: - logger.debug(f"{ticker} 목표가: {target:,.0f}원") + logger.info(f"목표가: {name}({ticker}) {target:,.0f}원 [시가 {price_info['open']:,}]") + valid_count += 1 await asyncio.sleep(1.1) except Exception as e: logger.warning(f"시가 수집 실패 {ticker}: {e}") + logger.info(f"목표가 계산 완료: {valid_count}/{len(self.universe)}종목 유효") # ───────────────────────────────────────── # 메인 매매 루프 (09:00~14:50) @@ -398,15 +404,27 @@ class StockBot: # lunch_trade_allowed=false이면 점심 세션 진입 차단 if self._midday_loaded and not self.strategy.context.get("lunch_trade_allowed", True): return + + _now_ts = datetime.now().timestamp() + _do_diag = (_now_ts - self._last_diag) >= 300 # 5분마다 진단 로그 + _diag = [] + for ticker in self.universe: if ticker in self.positions: + if _do_diag: + _diag.append(f"{ticker}:보유중") continue if ticker in self.sl_tickers: + if _do_diag: + _diag.append(f"{ticker}:SL차단") continue # 당일 SL 종목 재진입 차단 if len(self.positions) >= MAX_POSITIONS: break # 목표가 미계산 종목 스킵 (불필요한 API 호출 방지) - if self.strategy.get_target(ticker) <= 0: + target = self.strategy.get_target(ticker) + if target <= 0: + if _do_diag: + _diag.append(f"{ticker}:목표가없음") continue try: @@ -421,6 +439,11 @@ class StockBot: ) if not signal["signal"]: + if _do_diag: + _diag.append( + f"{name}({ticker}):{signal['reason']}" + f"[현재가{current:,}/목표가{target:,.0f}]" + ) continue balance = await self.kis.get_balance() @@ -470,6 +493,13 @@ class StockBot: except Exception as e: logger.error(f"진입 체크 오류 {ticker}: {type(e).__name__}: {e}") + if _do_diag: + self._last_diag = _now_ts + if _diag: + logger.info(f"[신호진단] {' | '.join(_diag)}") + else: + logger.info("[신호진단] 전 종목 신호 없음 (유니버스 비어있거나 모두 필터됨)") + # ───────────────────────────────────────── # 청산 체크 # ───────────────────────────────────────── diff --git a/app/strategy/volatility_breakout.py b/app/strategy/volatility_breakout.py index 1916b89..2c99ade 100644 --- a/app/strategy/volatility_breakout.py +++ b/app/strategy/volatility_breakout.py @@ -98,7 +98,14 @@ class VolatilityBreakout: def set_today_open(self, ticker: str, open_price: float): """당일 시가로 목표가 계산""" prev = self.prev_data.get(ticker) - if not prev or prev["amount"] < MIN_TRADE_AMOUNT: + if not prev: + logger.info(f"목표가 제외({ticker}): 전일 데이터 없음") + return + if prev["amount"] < MIN_TRADE_AMOUNT: + logger.info( + f"목표가 제외({ticker}): 전일 거래대금 {prev['amount']/1e8:.0f}억" + f" < 기준 {MIN_TRADE_AMOUNT/1e8:.0f}억" + ) return prev_range = prev["high"] - prev["low"]