공휴일 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_ctx_mtime : float = 0.0
|
||||||
self._midday_pos_mult : float = 1.0 # midday position_size_multiplier
|
self._midday_pos_mult : float = 1.0 # midday position_size_multiplier
|
||||||
self._midday_loaded : bool = False
|
self._midday_loaded : bool = False
|
||||||
|
self._last_diag : float = 0.0 # 신호 진단 로그 마지막 시각
|
||||||
|
|
||||||
mode = "모의투자" if self.kis.is_mock else "실거래"
|
mode = "모의투자" if self.kis.is_mock else "실거래"
|
||||||
dry = " [DRY_RUN]" if os.getenv("DRY_RUN","true")=="true" else ""
|
dry = " [DRY_RUN]" if os.getenv("DRY_RUN","true")=="true" else ""
|
||||||
@@ -262,12 +263,11 @@ class StockBot:
|
|||||||
self.universe = tickers
|
self.universe = tickers
|
||||||
logger.info(f"유니버스: {len(tickers)}종목 (ETF 제외)")
|
logger.info(f"유니버스: {len(tickers)}종목 (ETF 제외)")
|
||||||
|
|
||||||
# 전일 날짜 계산
|
# 최근 7일 범위 조회 → 공휴일·대체공휴일 자동 처리
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
today = datetime.now()
|
today = datetime.now()
|
||||||
# 월요일이면 금요일로
|
start_dt = (today - timedelta(days=7)).strftime("%Y%m%d")
|
||||||
offset = 3 if today.weekday() == 0 else 1
|
end_dt = (today - timedelta(days=1)).strftime("%Y%m%d")
|
||||||
prev_date = (today - timedelta(days=offset)).strftime("%Y%m%d")
|
|
||||||
|
|
||||||
for ticker in self.universe:
|
for ticker in self.universe:
|
||||||
# 이미 전일 데이터 있으면 skip
|
# 이미 전일 데이터 있으면 skip
|
||||||
@@ -276,11 +276,11 @@ class StockBot:
|
|||||||
try:
|
try:
|
||||||
ohlcv = await self.kis.get_ohlcv_daily(
|
ohlcv = await self.kis.get_ohlcv_daily(
|
||||||
ticker,
|
ticker,
|
||||||
start=prev_date,
|
start=start_dt,
|
||||||
end=prev_date,
|
end=end_dt,
|
||||||
)
|
)
|
||||||
if ohlcv:
|
if ohlcv:
|
||||||
prev = ohlcv[-1]
|
prev = ohlcv[-1] # 가장 최근 거래일
|
||||||
self.strategy.set_prev_data(
|
self.strategy.set_prev_data(
|
||||||
ticker,
|
ticker,
|
||||||
high = prev["high"],
|
high = prev["high"],
|
||||||
@@ -288,6 +288,8 @@ class StockBot:
|
|||||||
amount= prev.get("amount",
|
amount= prev.get("amount",
|
||||||
prev.get("volume", 0) * prev.get("close", 0))
|
prev.get("volume", 0) * prev.get("close", 0))
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
logger.warning(f"전일 OHLCV 없음 {ticker} ({start_dt}~{end_dt})")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"전일 데이터 실패 {ticker}: {e}")
|
logger.warning(f"전일 데이터 실패 {ticker}: {e}")
|
||||||
await asyncio.sleep(1.1) # 초당 2.5건으로 제한
|
await asyncio.sleep(1.1) # 초당 2.5건으로 제한
|
||||||
@@ -302,16 +304,20 @@ class StockBot:
|
|||||||
async def calc_targets(self):
|
async def calc_targets(self):
|
||||||
"""당일 시가 기반 목표가 계산"""
|
"""당일 시가 기반 목표가 계산"""
|
||||||
logger.info("목표가 계산 시작")
|
logger.info("목표가 계산 시작")
|
||||||
|
valid_count = 0
|
||||||
for ticker in self.universe:
|
for ticker in self.universe:
|
||||||
try:
|
try:
|
||||||
price_info = await self.kis.get_price(ticker)
|
price_info = await self.kis.get_price(ticker)
|
||||||
self.strategy.set_today_open(ticker, price_info["open"])
|
self.strategy.set_today_open(ticker, price_info["open"])
|
||||||
target = self.strategy.get_target(ticker)
|
target = self.strategy.get_target(ticker)
|
||||||
|
name = self.ticker_names.get(ticker, ticker)
|
||||||
if target > 0:
|
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)
|
await asyncio.sleep(1.1)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"시가 수집 실패 {ticker}: {e}")
|
logger.warning(f"시가 수집 실패 {ticker}: {e}")
|
||||||
|
logger.info(f"목표가 계산 완료: {valid_count}/{len(self.universe)}종목 유효")
|
||||||
|
|
||||||
# ─────────────────────────────────────────
|
# ─────────────────────────────────────────
|
||||||
# 메인 매매 루프 (09:00~14:50)
|
# 메인 매매 루프 (09:00~14:50)
|
||||||
@@ -398,15 +404,27 @@ class StockBot:
|
|||||||
# lunch_trade_allowed=false이면 점심 세션 진입 차단
|
# lunch_trade_allowed=false이면 점심 세션 진입 차단
|
||||||
if self._midday_loaded and not self.strategy.context.get("lunch_trade_allowed", True):
|
if self._midday_loaded and not self.strategy.context.get("lunch_trade_allowed", True):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
_now_ts = datetime.now().timestamp()
|
||||||
|
_do_diag = (_now_ts - self._last_diag) >= 300 # 5분마다 진단 로그
|
||||||
|
_diag = []
|
||||||
|
|
||||||
for ticker in self.universe:
|
for ticker in self.universe:
|
||||||
if ticker in self.positions:
|
if ticker in self.positions:
|
||||||
|
if _do_diag:
|
||||||
|
_diag.append(f"{ticker}:보유중")
|
||||||
continue
|
continue
|
||||||
if ticker in self.sl_tickers:
|
if ticker in self.sl_tickers:
|
||||||
|
if _do_diag:
|
||||||
|
_diag.append(f"{ticker}:SL차단")
|
||||||
continue # 당일 SL 종목 재진입 차단
|
continue # 당일 SL 종목 재진입 차단
|
||||||
if len(self.positions) >= MAX_POSITIONS:
|
if len(self.positions) >= MAX_POSITIONS:
|
||||||
break
|
break
|
||||||
# 목표가 미계산 종목 스킵 (불필요한 API 호출 방지)
|
# 목표가 미계산 종목 스킵 (불필요한 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
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -421,6 +439,11 @@ class StockBot:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not signal["signal"]:
|
if not signal["signal"]:
|
||||||
|
if _do_diag:
|
||||||
|
_diag.append(
|
||||||
|
f"{name}({ticker}):{signal['reason']}"
|
||||||
|
f"[현재가{current:,}/목표가{target:,.0f}]"
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
balance = await self.kis.get_balance()
|
balance = await self.kis.get_balance()
|
||||||
@@ -470,6 +493,13 @@ class StockBot:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"진입 체크 오류 {ticker}: {type(e).__name__}: {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):
|
def set_today_open(self, ticker: str, open_price: float):
|
||||||
"""당일 시가로 목표가 계산"""
|
"""당일 시가로 목표가 계산"""
|
||||||
prev = self.prev_data.get(ticker)
|
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
|
return
|
||||||
|
|
||||||
prev_range = prev["high"] - prev["low"]
|
prev_range = prev["high"] - prev["low"]
|
||||||
|
|||||||
Reference in New Issue
Block a user