Files
kis_bot/remove/legacy_root/trend_divergence.py
Your Name 6d2a706a48 커밋 1 — 실매 가격 TTL 구멍 (본체)
왜: 체결이 없어도 마지막가는 유지인데, TTL로 None 만들고 매도/EOD를 건너뛰어 8/5 돌파·금요일 leftover가 남음. 호가필터 TTL 구멍과 같은 병.

넣을 파일

신규: kis_trader/engine/live_sell_price.py
kis_trader/strategies/base.py (_ws_last_quote, _resolve_sell_price)
전략: momentum.py scalping.py tail_catch.py breakout.py range_break.py dart_strategy.py updow_strategy.py updown_feed.py us_momentum.py
WS: ws_manager.py kis_ws.py kiwoom_ws.py ls_ws.py kis_ws_overseas.py
kis_trader/web/live_config_schema.py (WS_PRICE_MAX_AGE_SEC 기본 0)
database.py (키 주석 + legacy/ sys.path)
kis_trader/execution/order_manager.py (잔고 있는데 40240000 ghost_purge 금지 — 같은 EOD 사고)
EOD가 min_hold에 안 막히게 손본 momentum_hts_logic.py / scalping_engine.py / tail_engine.py (이 대화에서 손본 부분만 확인 후)
문서: docs/like_mcp.md/db_erd.md code_architecture.md (가격 TTL 문구)
메시지 초안

fix: 매수·매도 현재가를 TTL로 버리지 않음 (마지막 RAM)
횡보·체결 공백을 죽은 캐시로 오인해 None 처리하면 손절·EOD가 스킵된다.
호가필터와 같이 나이는 무시하고 마지막 체결가를 유지한다. EOD는 매수가 폴백.
영향: 실매 O / 백테·옵투나 봉 경로 거의 무관 (엔진 식 변경 아님)

커밋 2 — 루트 정리 (remove/ vs legacy/)
왜: 루트 단독봇·테스트는 지울 보관함으로. 웹·알람이 아직 쓰는 모듈은 remove에 두면 나중에 폴더째 삭제 때 깨짐.

넣을 파일

이동: 미사용 → remove/legacy_root/ (래퍼, ETF/키움 옛봇, 테스트, kiwoom_rest_api 등)
이동: 사용 중 → legacy/ (holding_bot kis_holding_ver1 news_analyzer kis_long_ver1/2)
신규: kis_trader/utils/legacy_root.py legacy/README.md remove/README.md
import 경로: backtest_web.py mm_butler.py mm_remote.py updow_holding_cfg.py dbband_stock_cfg.py param_search_updow*.py dbband_param_search.py param_search_apply_snapshot.py verify_three_paths.py
docs/like_mcp.md/code_architecture.md 수동 노트
메시지 초안

chore: 미사용 루트는 remove/, 웹·알람 구모듈은 legacy/
remove는 나중에 통째 삭제 예정. holding_bot·news_analyzer·kis_long은
ensure_legacy_root로 legacy/만 본다.
빼기: scratch/set_ws_price_max_age_zero.py (일회성)
2026-08-18 00:13:17 +09:00

249 lines
11 KiB
Python

#!/usr/bin/env python3
"""
구글 트렌드 괴리율 분석 모듈
- 검색량 급증 vs 주가 미반영 = 단타 기회 포착
- 단타 전략에 활용: 검색량이 폭발하는데 주가는 아직 안 오른 종목
"""
import logging
import time
import random # 랜덤 딜레이를 위한 모듈 추가
from typing import Dict, Optional, List
import math
logger = logging.getLogger("TrendDivergence")
# pandas 임포트 (NaN 체크용)
try:
import pandas as pd
PANDAS_AVAILABLE = True
except ImportError:
PANDAS_AVAILABLE = False
pd = None
# yfinance 임포트 (의존성 충돌 대비)
try:
import yfinance as yf
YFINANCE_AVAILABLE = True
except ImportError:
YFINANCE_AVAILABLE = False
logger.warning("⚠️ yfinance 미설치! pip install 'yfinance<0.2.40'")
except Exception as e:
YFINANCE_AVAILABLE = False
logger.warning(f"⚠️ yfinance 임포트 실패 (의존성 충돌 가능): {e}")
# pytrends 임포트
try:
from pytrends.request import TrendReq
PTRENDS_AVAILABLE = True
except ImportError:
PTRENDS_AVAILABLE = False
logger.warning("⚠️ pytrends 미설치! pip install pytrends")
class TrendDivergenceAnalyzer:
"""구글 트렌드 괴리율 분석기"""
def __init__(self):
self.pytrends = None
self.rate_limit_hit = False # 429 에러 발생 시 플래그
self.last_request_time = 0 # 마지막 요청 시간 추적
if PTRENDS_AVAILABLE:
try:
# 구글의 봇 차단을 우회하기 위해 최신 크롬 브라우저처럼 User-Agent(신분증) 위장
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
}
# requests_args 매개변수를 통해 헤더 정보 함께 전송
self.pytrends = TrendReq(hl='ko-KR', tz=540, requests_args={'headers': headers})
logger.info("✅ 구글 트렌드 API 초기화 완료 (User-Agent 우회 적용)")
except Exception as e:
logger.error(f"❌ 구글 트렌드 초기화 실패: {e}")
self.pytrends = None
def get_trend_score(self, keyword: str, stock_code: str = None) -> Optional[float]:
"""
검색량 급증 점수 계산 (0.0 ~ 1.0)
:param keyword: 검색 키워드 (예: "삼성전자", "갤럭시")
:param stock_code: 종목 코드 (예: "005930.KS") - 주가 비교용
:return: 괴리율 점수 (None이면 데이터 없음)
"""
if not self.pytrends:
return None
if not keyword or not keyword.strip():
return None
try:
# ✅ Rate Limit 방지: 429 에러 발생 시 더 긴 대기
if self.rate_limit_hit:
wait_time = random.uniform(60.0, 120.0) # 1~2분 대기
logger.warning(f"⏳ Rate Limit 복구 대기 중... ({wait_time:.0f}초)")
time.sleep(wait_time)
self.rate_limit_hit = False # 재시도
# ✅ 요청 간 최소 간격 보장 (5초 이상)
current_time = time.time()
if self.last_request_time > 0:
elapsed = current_time - self.last_request_time
if elapsed < 5.0:
sleep_time = 5.0 - elapsed + random.uniform(1.0, 3.0)
time.sleep(sleep_time)
# 1. 구글 트렌드 데이터 가져오기 (최근 7일)
# ✅ 개선: timeframe을 'now 7-d'로 변경 (최신 데이터), geo='KR' 추가
self.pytrends.build_payload([keyword], cat=0, timeframe='now 7-d', geo='KR')
# ✅ 개선: 네트워크 타임아웃 방지 (5~8초 랜덤 대기로 증가)
time.sleep(random.uniform(5.0, 8.0))
self.last_request_time = time.time() # 요청 시간 기록
trend_data = self.pytrends.interest_over_time()
if trend_data.empty:
logger.debug(f"트렌드 데이터 없음: {keyword}")
return None
if keyword not in trend_data.columns:
logger.debug(f"키워드 컬럼 없음: {keyword}")
return None
# 데이터가 충분한지 확인 (최소 3일 필요)
if len(trend_data) < 3:
logger.debug(f"데이터 부족 ({len(trend_data)}일): {keyword}")
return None
# 최근 3일 평균 vs 지난 7일 평균 비교
recent_avg = trend_data[keyword].tail(3).mean()
long_term_avg = trend_data[keyword].mean()
# NaN 체크 (pandas 있으면 pd.isna, 없으면 수동 체크)
if PANDAS_AVAILABLE and pd is not None:
if pd.isna(recent_avg) or pd.isna(long_term_avg):
logger.debug(f"NaN 값 발견: {keyword}")
return None
else:
# pandas 없으면 간단한 체크 (NaN은 float('nan')이므로 자기 자신과 비교)
if recent_avg is None or long_term_avg is None:
logger.debug(f"None 값 발견: {keyword}")
return None
if isinstance(recent_avg, float) and math.isnan(recent_avg):
logger.debug(f"NaN 값 발견 (recent_avg): {keyword}")
return None
if isinstance(long_term_avg, float) and math.isnan(long_term_avg):
logger.debug(f"NaN 값 발견 (long_term_avg): {keyword}")
return None
if long_term_avg == 0:
logger.debug(f"장기 평균 0: {keyword}")
return None
# 검색량 급증 비율
surge_ratio = recent_avg / long_term_avg if long_term_avg > 0 else 1.0
# 2. 주가 데이터와 비교 (괴리율 계산)
price_change_pct = 0.0
if stock_code and YFINANCE_AVAILABLE:
try:
stock = yf.Ticker(stock_code)
hist = stock.history(period="7d")
if not hist.empty and len(hist) >= 2:
# 최근 3일 평균 vs 지난 7일 평균
recent_price = hist['Close'].tail(3).mean()
long_price = hist['Close'].mean()
price_change_pct = ((recent_price - long_price) / long_price) * 100
except Exception as e:
logger.debug(f"주가 조회 실패 ({stock_code}): {e}")
elif stock_code and not YFINANCE_AVAILABLE:
logger.debug(f"yfinance 미사용 - 주가 비교 스킵 ({stock_code})")
# 3. 괴리율 점수 계산
# 검색량이 2배 이상 급증했는데 주가는 5% 미만 오름 = 괴리율 발생
if surge_ratio >= 2.0 and price_change_pct < 5.0:
# 괴리율이 클수록 점수 높음 (최대 1.0)
divergence_score = min(1.0, (surge_ratio - 1.0) * 0.3)
logger.info(f"🔍 [{keyword}] 검색량 {surge_ratio:.1f}배 급증, 주가 {price_change_pct:.1f}% → 괴리율 점수 {divergence_score:.2f}")
return divergence_score
else:
return 0.0
except Exception as e:
error_str = str(e)
# ✅ 개선: 400 에러는 데이터 없음으로 간주 (로그 레벨 낮춤)
if "400" in error_str or "Bad Request" in error_str:
logger.debug(f"검색량 부족 (400): {keyword}")
return None
# ✅ 개선: 429 에러는 Rate Limit으로 처리 (더 긴 대기 필요)
elif "429" in error_str or "Too Many Requests" in error_str:
self.rate_limit_hit = True
logger.warning(f"⚠️ Rate Limit 도달 (429): {keyword} - 구글 트렌드 분석 일시 중단")
# 429 에러 발생 시 나머지 분석 중단
return None
else:
logger.error(f"❌ 트렌드 분석 실패 ({keyword}): {e}")
return None
def analyze_stocks(self, stock_list: List[Dict]) -> Dict[str, float]:
"""
종목 리스트에 대한 트렌드 괴리율 점수 계산
:param stock_list: [{'code': '005930', 'name': '삼성전자'}, ...]
:return: {code: score} 딕셔너리
"""
results = {}
if not self.pytrends:
logger.warning("⚠️ 구글 트렌드 API 사용 불가")
return results
if not stock_list or not isinstance(stock_list, list):
logger.debug("종목 리스트가 비어있거나 잘못된 형식")
return results
try:
# ✅ 개선: 최대 5개로 제한 (429 에러 방지를 위해 더 줄임)
max_stocks = 5 if not self.rate_limit_hit else 0
if max_stocks == 0:
logger.warning("⚠️ Rate Limit 상태 - 구글 트렌드 분석 스킵")
return results
for idx, stock in enumerate(stock_list[:max_stocks], 1):
# Rate Limit 발생 시 즉시 중단
if self.rate_limit_hit:
logger.warning(f"⚠️ Rate Limit 발생 - {idx-1}개 분석 후 중단")
break
if not isinstance(stock, dict):
continue
code = stock.get('code', '')
name = stock.get('name', '')
if not name or not code:
continue
# 한국 주식 티커 형식
ticker = f"{code}.KS" if len(str(code)) == 6 else None
# 종목명으로 검색량 분석
try:
logger.debug(f"🔍 [{idx}/{max_stocks}] {name} 분석 중...")
score = self.get_trend_score(keyword=name, stock_code=ticker)
if score is not None and score > 0:
results[code] = score
except Exception as e:
logger.debug(f"종목 분석 실패 ({name} {code}): {e}")
continue
# get_trend_score 내부에서 이미 딜레이 처리하므로 여기서는 추가 딜레이 불필요
except Exception as e:
logger.error(f"❌ 종목 리스트 분석 중 에러: {e}")
# ✅ 개선: 분석 완료 후 충분한 쿨다운 (다음 배치까지 여유)
if results:
logger.info(f"✅ 트렌드 괴리율 발견: {len(results)}개 종목")
return results