feat: yfinance rate limit 처리 강화

- yfinance_plus: 세션 풀 다양화 (chrome/edge/safari/firefox 핑거프린트 6개)
  + 모듈 싱글톤으로 process-wide rate limiter 공유
  + Adaptive throttling (rate limit 감지 시 0.3s→최대 5s 자동 증가)
  + EnhancedTicker별 전용 세션으로 스레드 race condition 해결
- price.py: rate limit 에러 감지 시 HTTP 500 → HTTP 429 + Retry-After: 30
- test_rate_limit.py: rate limit 발생 조건 측정 스크립트 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
I Luk Kim 5 months ago
parent c4565159b1
commit fb692fe592

@ -266,6 +266,17 @@ async def get_price_data(
}
)
except Exception as e:
err_msg = str(e).lower()
if "rate limit" in err_msg or "too many requests" in err_msg or "429" in err_msg:
raise HTTPException(
status_code=429,
detail={
"error_type": ErrorType.RATE_LIMIT_ERROR,
"message": "Yahoo Finance rate limit exceeded. Retry after a short delay.",
"detail": {"error": str(e)}
},
headers={"Retry-After": "30"}
)
raise HTTPException(
status_code=500,
detail={
@ -526,8 +537,19 @@ async def get_latest_price(
)
return PriceDataPoint.model_validate(latest_price)
except Exception as e:
err_msg = str(e).lower()
if "rate limit" in err_msg or "too many requests" in err_msg or "429" in err_msg:
raise HTTPException(
status_code=429,
detail={
"error_type": ErrorType.RATE_LIMIT_ERROR,
"message": "Yahoo Finance rate limit exceeded. Retry after a short delay.",
"detail": {"error": str(e)}
},
headers={"Retry-After": "30"}
)
raise HTTPException(
status_code=500,
detail={

@ -0,0 +1,78 @@
#!/usr/bin/env python3
"""
yfinance_plus rate limit baseline test.
Measures when/how often rate limits occur with current implementation.
"""
import sys
import os
sys.path.insert(0, os.path.dirname(__file__))
import yfinance_plus as yf
import time
import logging
# Show WARNING+ so we see rate limit retry messages
logging.basicConfig(
level=logging.WARNING,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s"
)
# Also show yfinance_plus warnings
logging.getLogger("yfinance_plus").setLevel(logging.WARNING)
TICKERS = [
"AAPL", "MSFT", "GOOGL", "AMZN", "META", "NVDA", "TSLA", "JPM", "V", "JNJ",
"WMT", "PG", "MA", "UNH", "HD", "DIS", "BAC", "XOM", "PFE", "CSCO",
"INTC", "VZ", "CMCSA", "KO", "PEP", "ABT", "MRK", "TMO", "AVGO", "COST",
"MMC", "ACN", "LIN", "TXN", "MDT", "NEE", "UPS", "MS", "BLK", "ISRG",
]
print(f"=== yfinance_plus Rate Limit Baseline Test ===")
print(f"Tickers: {len(TICKERS)}, Period: 1y, No deliberate delay between tickers\n")
results = []
t_total_start = time.time()
for i, ticker in enumerate(TICKERS):
t0 = time.time()
status = "OK"
rows = 0
error = ""
try:
data = yf.Ticker(ticker).history(period="1y")
rows = len(data) if data is not None else 0
except Exception as e:
status = "FAIL"
error = str(e)[:100]
elapsed = time.time() - t0
results.append((i + 1, ticker, status, rows, elapsed, error))
if status == "OK":
print(f"[{i+1:2d}/{len(TICKERS)}] {ticker:6s}: OK ({rows:3d} rows, {elapsed:.2f}s)")
else:
print(f"[{i+1:2d}/{len(TICKERS)}] {ticker:6s}: FAIL ({error}, {elapsed:.2f}s)")
total_elapsed = time.time() - t_total_start
# Summary
ok_count = sum(1 for r in results if r[2] == "OK")
fail_count = len(results) - ok_count
fail_tickers = [(r[1], r[5]) for r in results if r[2] == "FAIL"]
print(f"\n{'='*60}")
print(f"결과: {ok_count}/{len(TICKERS)} 성공, {fail_count} 실패")
print(f"총 소요 시간: {total_elapsed:.1f}초 (평균 {total_elapsed/len(TICKERS):.2f}s/ticker)")
if fail_tickers:
print(f"\n실패 목록:")
for t, e in fail_tickers:
print(f" {t}: {e}")
# Timing analysis
times = [r[4] for r in results if r[2] == "OK"]
if times:
print(f"\n처리 시간 분포 (성공):")
print(f" min={min(times):.2f}s, max={max(times):.2f}s, avg={sum(times)/len(times):.2f}s")
slow = [r for r in results if r[2] == "OK" and r[4] > 5.0]
if slow:
print(f" 5초 초과 (retry 발생 가능): {[(r[1], f'{r[4]:.1f}s') for r in slow]}")

@ -1 +1 @@
Subproject commit df26cb7e1923f7a7352fddc75ee0c62ee77d7881
Subproject commit 247e8c165c2cf8ab37254f0e2f881be9f1c66dfc
Loading…
Cancel
Save