239 lines
7.8 KiB
Python
239 lines
7.8 KiB
Python
"""
|
|
매일 아침 장 시작 전(9시) 실행되는 배치 분석 스크립트
|
|
|
|
전체 거래 종목을 분석하여 DB에 저장
|
|
- 차트 데이터 조회
|
|
- 기술적 지표 계산
|
|
- 매매 신호 생성
|
|
- DB 저장
|
|
|
|
실행: python batch_daily_analysis.py
|
|
Cron: 0 9 * * 1-5 cd /path/to/auto-stock/backend && python batch_daily_analysis.py
|
|
"""
|
|
|
|
import asyncio
|
|
import sys
|
|
from datetime import datetime, date
|
|
from pathlib import Path
|
|
|
|
# 프로젝트 루트를 Python 경로에 추가
|
|
sys.path.insert(0, str(Path(__file__).parent))
|
|
|
|
from sqlalchemy import select, delete
|
|
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
|
from sqlalchemy.orm import sessionmaker
|
|
|
|
from app.core.config import settings
|
|
from app.models.models import StockAnalysis
|
|
from app.services.korea_investment_api import KoreaInvestmentAPI
|
|
from app.api.endpoints.recommendations import analyze_stock_with_indicators
|
|
import logging
|
|
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# 데이터베이스 연결
|
|
engine = create_async_engine(
|
|
settings.DATABASE_URL,
|
|
echo=False,
|
|
pool_pre_ping=True,
|
|
pool_size=10,
|
|
max_overflow=20
|
|
)
|
|
|
|
AsyncSessionLocal = sessionmaker(
|
|
engine,
|
|
class_=AsyncSession,
|
|
expire_on_commit=False
|
|
)
|
|
|
|
|
|
async def get_all_tradable_stocks() -> list[tuple[str, str]]:
|
|
"""거래 가능한 전체 종목 조회 (안전한 재시도 적용)"""
|
|
kis_api = KoreaInvestmentAPI()
|
|
|
|
logger.info("Fetching all tradable stocks from KIS API...")
|
|
|
|
stocks = []
|
|
|
|
# KOSPI 상위 종목 조회
|
|
try:
|
|
# 재시도 로직이 내장되어 있으므로 sleep 제거 (쓰로틀링은 자동 적용)
|
|
kospi_result = await kis_api.get_volume_rank("J")
|
|
if kospi_result and kospi_result.get("output"):
|
|
for item in kospi_result["output"]:
|
|
stocks.append((
|
|
item["mksc_shrn_iscd"],
|
|
item["hts_kor_isnm"]
|
|
))
|
|
logger.info(f"✅ Found {len(stocks)} KOSPI stocks")
|
|
except Exception as e:
|
|
logger.error(f"❌ Failed to fetch KOSPI stocks: {e}")
|
|
|
|
# KOSDAQ 상위 종목 조회
|
|
try:
|
|
kosdaq_result = await kis_api.get_volume_rank("Q")
|
|
if kosdaq_result and kosdaq_result.get("output"):
|
|
kospi_count = len(stocks)
|
|
for item in kosdaq_result["output"]:
|
|
stocks.append((
|
|
item["mksc_shrn_iscd"],
|
|
item["hts_kor_isnm"]
|
|
))
|
|
logger.info(f"✅ Found {len(stocks) - kospi_count} KOSDAQ stocks")
|
|
except Exception as e:
|
|
logger.error(f"❌ Failed to fetch KOSDAQ stocks: {e}")
|
|
|
|
logger.info(f"📊 Total tradable stocks: {len(stocks)}")
|
|
return stocks
|
|
|
|
|
|
async def analyze_and_save_stock(
|
|
session: AsyncSession,
|
|
stock_code: str,
|
|
stock_name: str,
|
|
analysis_date: datetime
|
|
) -> bool:
|
|
"""종목 분석 후 DB 저장"""
|
|
try:
|
|
# 종목 분석 (기존 함수 재사용)
|
|
result = await analyze_stock_with_indicators(stock_code, stock_name)
|
|
|
|
if not result:
|
|
logger.warning(f"No analysis result for {stock_code}")
|
|
return False
|
|
|
|
# DB에 저장
|
|
analysis = StockAnalysis(
|
|
stock_code=stock_code,
|
|
stock_name=stock_name,
|
|
analysis_date=analysis_date,
|
|
current_price=result.current_price,
|
|
change_rate=result.change_rate,
|
|
volume=result.volume,
|
|
rsi=result.rsi,
|
|
ma5=result.ma5,
|
|
ma20=result.ma20,
|
|
ma60=result.ma60,
|
|
stochastic_k=result.stochastic_k,
|
|
stochastic_d=result.stochastic_d,
|
|
bollinger_upper=result.bollinger_upper,
|
|
bollinger_lower=result.bollinger_lower,
|
|
bollinger_middle=result.bollinger_middle,
|
|
signals=result.signals,
|
|
score=result.score
|
|
)
|
|
|
|
session.add(analysis)
|
|
await session.commit()
|
|
|
|
logger.info(f"✅ Saved analysis for {stock_code} ({stock_name}): score={result.score}")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"❌ Failed to analyze {stock_code}: {e}")
|
|
await session.rollback()
|
|
return False
|
|
|
|
|
|
async def batch_analyze_all_stocks(limit: int = None):
|
|
"""전체 종목 배치 분석 (안전한 재시도 및 쓰로틀링 적용)"""
|
|
start_time = datetime.now()
|
|
analysis_date = date.today()
|
|
|
|
logger.info(f"🚀 Starting daily batch analysis for {analysis_date}")
|
|
logger.info(f"⚙️ Rate Limiting: {settings.API_MIN_REQUEST_INTERVAL}초 간격, 최대 재시도 {settings.API_MAX_RETRIES}회")
|
|
|
|
# KIS API 인스턴스
|
|
kis_api = KoreaInvestmentAPI()
|
|
|
|
async with AsyncSessionLocal() as session:
|
|
# 1. 오늘 날짜 기존 데이터 삭제
|
|
logger.info("🗑️ Deleting old analysis data...")
|
|
await session.execute(
|
|
delete(StockAnalysis).where(
|
|
StockAnalysis.analysis_date >= analysis_date
|
|
)
|
|
)
|
|
await session.commit()
|
|
logger.info("✅ Old data deleted")
|
|
|
|
# 2. 전체 종목 조회
|
|
all_stocks = await get_all_tradable_stocks()
|
|
|
|
if limit:
|
|
all_stocks = all_stocks[:limit]
|
|
logger.info(f"🧪 Limited to {limit} stocks for testing")
|
|
|
|
# 3. 배치 처리 (쓰로틀링은 KIS API 클래스에서 자동 처리)
|
|
success_count = 0
|
|
fail_count = 0
|
|
|
|
for i, (stock_code, stock_name) in enumerate(all_stocks, 1):
|
|
logger.info(f"[{i}/{len(all_stocks)}] Analyzing {stock_code} ({stock_name})...")
|
|
|
|
success = await analyze_and_save_stock(
|
|
session,
|
|
stock_code,
|
|
stock_name,
|
|
analysis_date
|
|
)
|
|
|
|
if success:
|
|
success_count += 1
|
|
else:
|
|
fail_count += 1
|
|
|
|
# 30개마다 통계 출력
|
|
if i % 30 == 0:
|
|
kis_api.log_monitor_stats()
|
|
|
|
# 4. 결과 요약 및 최종 통계
|
|
end_time = datetime.now()
|
|
duration = (end_time - start_time).total_seconds()
|
|
|
|
logger.info("=" * 60)
|
|
logger.info(f"✅ Batch analysis completed!")
|
|
logger.info(f"📊 Success: {success_count} / Total: {len(all_stocks)}")
|
|
logger.info(f"❌ Failed: {fail_count}")
|
|
logger.info(f"⏱️ Duration: {duration:.1f} seconds ({duration/60:.1f} minutes)")
|
|
logger.info(f"📅 Analysis Date: {analysis_date}")
|
|
|
|
# API 통계 출력
|
|
stats = kis_api.get_monitor_stats()
|
|
logger.info(f"📡 API 통계:")
|
|
logger.info(f" - 총 호출: {stats['total_calls']}건")
|
|
logger.info(f" - 오류: {stats['total_errors']}건 ({stats['error_rate']:.1%})")
|
|
logger.info(f" - 재시도: {stats['total_retries']}건")
|
|
logger.info(f" - 평균 응답 시간: {stats['avg_duration']:.2f}초")
|
|
logger.info("=" * 60)
|
|
|
|
|
|
async def main():
|
|
"""메인 실행 함수"""
|
|
import argparse
|
|
|
|
parser = argparse.ArgumentParser(description='Daily Stock Analysis Batch')
|
|
parser.add_argument('--limit', type=int, help='Limit number of stocks (for testing)')
|
|
parser.add_argument('--test', action='store_true', help='Test mode (analyze 10 stocks)')
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.test:
|
|
logger.info("🧪 TEST MODE: Analyzing 10 stocks only")
|
|
await batch_analyze_all_stocks(limit=10)
|
|
elif args.limit:
|
|
logger.info(f"📊 Custom limit: {args.limit} stocks")
|
|
await batch_analyze_all_stocks(limit=args.limit)
|
|
else:
|
|
logger.info("📊 Full analysis: All stocks")
|
|
await batch_analyze_all_stocks()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|