From 7a003a536025cd332cad00d32a6f6df7843c1d1c Mon Sep 17 00:00:00 2001 From: I Luk Kim Date: Wed, 8 Apr 2026 09:42:30 -0700 Subject: [PATCH] =?UTF-8?q?fix:=20screener=20yf.screen()=20rate=20limit/40?= =?UTF-8?q?1=20retry=20=EC=B2=98=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 429/rate limit 및 401/unauthorized 발생 시 최대 3회 재시도 (delay 3s, 6s). 모든 retry 소진 후 RuntimeError로 변환하여 503 응답. Co-Authored-By: Claude Sonnet 4.6 --- app/services/screener_service.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/app/services/screener_service.py b/app/services/screener_service.py index 3b1d07b..6b0c2d1 100644 --- a/app/services/screener_service.py +++ b/app/services/screener_service.py @@ -153,7 +153,34 @@ class ScreenerService: def _screen_sync(self, query, offset: int, size: int, sort_field: str, sort_asc: bool) -> dict: """Synchronous yfinance screen() call — must run in executor.""" import yfinance as yf - return yf.screen(query, offset=offset, size=size, sortField=sort_field, sortAsc=sort_asc) + + max_retries = 3 + for attempt in range(max_retries): + try: + return yf.screen(query, offset=offset, size=size, sortField=sort_field, sortAsc=sort_asc) + except Exception as e: + err = str(e).lower() + is_retriable = ( + "too many requests" in err + or "rate limit" in err + or "429" in err + or "401" in err + or "unauthorized" in err + ) + if is_retriable and attempt < max_retries - 1: + delay = (attempt + 1) * 3 # 3s, 6s + logger.warning( + "yfinance screen() error '%s' (attempt %d/%d), retrying in %.1fs", + str(e)[:80], attempt + 1, max_retries, delay, + ) + time.sleep(delay) + continue + if is_retriable: + raise RuntimeError( + "Yahoo Finance is rate limiting this server. " + "Please try again in 30–60 seconds." + ) + raise async def screen_stocks( self,