From c09cc7ec25e86d37e1f9dd2d0f908b5292adf8e1 Mon Sep 17 00:00:00 2001 From: Youssef Aitousarrah Date: Tue, 7 Apr 2026 16:36:57 -0700 Subject: [PATCH] fix(y_finance): make suppress_yfinance_warnings thread-safe The previous implementation redirected sys.stderr to /dev/null using a context manager. This is not thread-safe: 8 concurrent scanner threads each mutate sys.stderr, and when one thread's context manager closes the devnull file, another thread that captured devnull as its saved stderr attempts to write to the closed fd and raises "I/O operation on closed file". This corrupted sys.stderr state caused _fetch_batch_prices to fail and all per-ticker get_stock_price fallback calls to return None, resulting in every candidate being dropped with "no data available". Fix by suppressing at the Python logging level instead of redirecting sys.stderr. Logger.setLevel() is protected by internal locks and is safe to call from concurrent threads. Co-Authored-By: Claude Sonnet 4.6 --- tradingagents/dataflows/y_finance.py | 35 ++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/tradingagents/dataflows/y_finance.py b/tradingagents/dataflows/y_finance.py index 6a635bd7..5dcd5ab8 100644 --- a/tradingagents/dataflows/y_finance.py +++ b/tradingagents/dataflows/y_finance.py @@ -1,5 +1,4 @@ import os -import sys import warnings from contextlib import contextmanager from datetime import datetime, timedelta @@ -20,17 +19,39 @@ logger = get_logger(__name__) @contextmanager def suppress_yfinance_warnings(): - """Suppress yfinance stderr warnings about delisted tickers.""" + """Suppress yfinance log and warning output in a thread-safe way. + + Previous implementation redirected sys.stderr to /dev/null, but that is + NOT thread-safe: concurrent scanner threads each mutate the process-global + sys.stderr, causing race conditions where one thread closes a file descriptor + that another thread is still writing to ("I/O operation on closed file"). + + This implementation suppresses at the Python logging level, which is + protected by internal locks and therefore safe to call from many threads. + """ + import logging + + yf_logger_names = [ + "yfinance", + "yfinance.base", + "yfinance.utils", + "peewee", + "urllib3.connectionpool", + "urllib3", + ] + saved_levels = {} + for name in yf_logger_names: + lgr = logging.getLogger(name) + saved_levels[name] = lgr.level + lgr.setLevel(logging.CRITICAL) + with warnings.catch_warnings(): warnings.filterwarnings("ignore") - # Redirect stderr to devnull temporarily - old_stderr = sys.stderr - sys.stderr = open(os.devnull, "w") try: yield finally: - sys.stderr.close() - sys.stderr = old_stderr + for name, level in saved_levels.items(): + logging.getLogger(name).setLevel(level) def get_ticker_info(symbol: str) -> dict: