diff --git a/app/api/v1/endpoints/price.py b/app/api/v1/endpoints/price.py index 5f072b7..cde3185 100644 --- a/app/api/v1/endpoints/price.py +++ b/app/api/v1/endpoints/price.py @@ -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={ diff --git a/test_rate_limit.py b/test_rate_limit.py new file mode 100644 index 0000000..9ce5e2b --- /dev/null +++ b/test_rate_limit.py @@ -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]}") diff --git a/yfinance_plus b/yfinance_plus index df26cb7..247e8c1 160000 --- a/yfinance_plus +++ b/yfinance_plus @@ -1 +1 @@ -Subproject commit df26cb7e1923f7a7352fddc75ee0c62ee77d7881 +Subproject commit 247e8c165c2cf8ab37254f0e2f881be9f1c66dfc