공휴일 OHLCV 버그 수정 + 진입 신호 진단 로그 추가
- OHLCV 조회를 단일일→7일 범위로 변경해 공휴일(대체공휴일 등) 자동 처리 (5/25 대체공휴일로 전 종목 목표가 0개 → 오늘 하루 종일 0건 원인) - 목표가 계산 결과 DEBUG→INFO 레벨 격상 (종목별 목표가·시가 표시) - 목표가 제외 이유 INFO 로그 추가 (전일 데이터 없음 / 거래대금 미달) - check_entries에 5분마다 진단 로그 추가 (신호 거절 이유 전 종목 출력) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+39
-9
@@ -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")
|
||||
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("[신호진단] 전 종목 신호 없음 (유니버스 비어있거나 모두 필터됨)")
|
||||
|
||||
# ─────────────────────────────────────────
|
||||
# 청산 체크
|
||||
# ─────────────────────────────────────────
|
||||
|
||||
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user