Spaces:
Sleeping
Fix ML/intraday predictions getting stuck forever on Yahoo hangs
Browse filesget_intraday_bars() serialized all callers on a single global lock and
called yf.download() with no timeout. yf.download has no built-in
timeout, so one stalled Yahoo connection wedged the lock forever,
freezing intraday ORB/VWAP context (every AI INTRADAY forecast) and the
/api/ml-predict today_high fetch for every ticker for the rest of the
process's life - exactly the '\''ML gets stuck and never loads'\'' symptom
(reproduced: 9 concurrent /api/ml-predict calls all timed out >120s).
- intraday_live.get_intraday_bars: bound the lock wait (20s) and the
actual yf.download() call (15s, via a worker thread) so a stall
degrades to None instead of hanging every future caller forever.
- ml_predictor/infer.py _indices(): same unbounded yf.download() risk
for Nifty/VIX; now routed through data_sources._yf_download_timed
(15s bound), the same pattern already used elsewhere in the codebase.
- intraday_live.py +32 -4
- ml_predictor/infer.py +8 -2
|
@@ -15,6 +15,7 @@ Documented ORB hit rates on NSE:
|
|
| 15 |
Individual NSE stocks at ORB+0.5× extension: 68-70%
|
| 16 |
BANKNIFTY 30-min ORB: 73% documented
|
| 17 |
"""
|
|
|
|
| 18 |
import datetime
|
| 19 |
import threading
|
| 20 |
import pandas as pd
|
|
@@ -32,23 +33,48 @@ except ImportError:
|
|
| 32 |
# actual download so each call gets its own ticker's data.
|
| 33 |
_YF_DOWNLOAD_LOCK = threading.Lock()
|
| 34 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
# NSE market open time (IST = UTC+5:30)
|
| 36 |
_NSE_OPEN_UTC = datetime.time(3, 45) # 09:15 IST
|
| 37 |
_NSE_CLOSE_UTC = datetime.time(10, 0) # 15:30 IST
|
| 38 |
|
| 39 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
def get_intraday_bars(ticker: str, interval: str = "15m", period: str = "5d") -> pd.DataFrame | None:
|
| 41 |
"""
|
| 42 |
Download intraday OHLCV bars from yfinance.
|
| 43 |
interval: "1m" (last 7d) | "5m" | "15m" | "30m" (last 60d)
|
| 44 |
-
Returns None on failure.
|
| 45 |
"""
|
| 46 |
if not _HAS_YF:
|
| 47 |
return None
|
|
|
|
|
|
|
| 48 |
try:
|
| 49 |
-
|
| 50 |
-
df = yf.download(ticker, period=period, interval=interval,
|
| 51 |
-
auto_adjust=True, progress=False, group_by="ticker")
|
| 52 |
if df is None or df.empty:
|
| 53 |
return None
|
| 54 |
# When group_by="ticker" is honoured, the outer column level is the ticker —
|
|
@@ -62,6 +88,8 @@ def get_intraday_bars(ticker: str, interval: str = "15m", period: str = "5d") ->
|
|
| 62 |
return df
|
| 63 |
except Exception:
|
| 64 |
return None
|
|
|
|
|
|
|
| 65 |
|
| 66 |
|
| 67 |
def compute_orb(bars: pd.DataFrame, orb_minutes: int = 15) -> dict:
|
|
|
|
| 15 |
Individual NSE stocks at ORB+0.5× extension: 68-70%
|
| 16 |
BANKNIFTY 30-min ORB: 73% documented
|
| 17 |
"""
|
| 18 |
+
import concurrent.futures
|
| 19 |
import datetime
|
| 20 |
import threading
|
| 21 |
import pandas as pd
|
|
|
|
| 33 |
# actual download so each call gets its own ticker's data.
|
| 34 |
_YF_DOWNLOAD_LOCK = threading.Lock()
|
| 35 |
|
| 36 |
+
# yf.download() has no built-in timeout — a hung/stalled Yahoo connection blocks the
|
| 37 |
+
# calling thread indefinitely. Because every caller serializes on _YF_DOWNLOAD_LOCK, one
|
| 38 |
+
# hung call previously wedged this lock forever, freezing intraday context (and the
|
| 39 |
+
# /api/ml-predict "today_high" fetch) for every ticker for the rest of the process's life.
|
| 40 |
+
# Bound both the lock wait and the download itself so a stall degrades to None instead.
|
| 41 |
+
_YF_LOCK_TIMEOUT = 20 # max seconds to wait for the shared download lock
|
| 42 |
+
_YF_DOWNLOAD_TIMEOUT = 15 # max seconds for the actual yf.download() call
|
| 43 |
+
|
| 44 |
# NSE market open time (IST = UTC+5:30)
|
| 45 |
_NSE_OPEN_UTC = datetime.time(3, 45) # 09:15 IST
|
| 46 |
_NSE_CLOSE_UTC = datetime.time(10, 0) # 15:30 IST
|
| 47 |
|
| 48 |
|
| 49 |
+
def _yf_download_bounded(ticker: str, period: str, interval: str):
|
| 50 |
+
"""Run yf.download in a worker thread with a hard wall-clock timeout.
|
| 51 |
+
|
| 52 |
+
If the download doesn't finish in time, the worker thread is abandoned
|
| 53 |
+
(shutdown(wait=False)) so the caller is never blocked past the timeout.
|
| 54 |
+
"""
|
| 55 |
+
ex = concurrent.futures.ThreadPoolExecutor(max_workers=1)
|
| 56 |
+
try:
|
| 57 |
+
fut = ex.submit(yf.download, ticker, period=period, interval=interval,
|
| 58 |
+
auto_adjust=True, progress=False, group_by="ticker")
|
| 59 |
+
return fut.result(timeout=_YF_DOWNLOAD_TIMEOUT)
|
| 60 |
+
except concurrent.futures.TimeoutError:
|
| 61 |
+
return None
|
| 62 |
+
finally:
|
| 63 |
+
ex.shutdown(wait=False)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
def get_intraday_bars(ticker: str, interval: str = "15m", period: str = "5d") -> pd.DataFrame | None:
|
| 67 |
"""
|
| 68 |
Download intraday OHLCV bars from yfinance.
|
| 69 |
interval: "1m" (last 7d) | "5m" | "15m" | "30m" (last 60d)
|
| 70 |
+
Returns None on failure (including a timed-out lock wait or download).
|
| 71 |
"""
|
| 72 |
if not _HAS_YF:
|
| 73 |
return None
|
| 74 |
+
if not _YF_DOWNLOAD_LOCK.acquire(timeout=_YF_LOCK_TIMEOUT):
|
| 75 |
+
return None # another call is stuck holding the lock — fail soft, don't pile up
|
| 76 |
try:
|
| 77 |
+
df = _yf_download_bounded(ticker, period, interval)
|
|
|
|
|
|
|
| 78 |
if df is None or df.empty:
|
| 79 |
return None
|
| 80 |
# When group_by="ticker" is honoured, the outer column level is the ticker —
|
|
|
|
| 88 |
return df
|
| 89 |
except Exception:
|
| 90 |
return None
|
| 91 |
+
finally:
|
| 92 |
+
_YF_DOWNLOAD_LOCK.release()
|
| 93 |
|
| 94 |
|
| 95 |
def compute_orb(bars: pd.DataFrame, orb_minutes: int = 15) -> dict:
|
|
@@ -244,8 +244,14 @@ class MLPredictor:
|
|
| 244 |
return self._idx_cache["nifty"], self._idx_cache["vix"]
|
| 245 |
nifty = vix = None
|
| 246 |
try:
|
| 247 |
-
|
| 248 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
nifty = raw["Close"]["^NSEI"].dropna()
|
| 250 |
vix = raw["Close"]["^INDIAVIX"].dropna()
|
| 251 |
except Exception:
|
|
|
|
| 244 |
return self._idx_cache["nifty"], self._idx_cache["vix"]
|
| 245 |
nifty = vix = None
|
| 246 |
try:
|
| 247 |
+
# yf.download has no built-in timeout — a stalled Yahoo connection would
|
| 248 |
+
# otherwise block this request forever. _yf_download_timed bounds it (15s)
|
| 249 |
+
# via a worker thread the same way data_sources.py's own fetches are bounded.
|
| 250 |
+
from data_sources import _yf_download_timed
|
| 251 |
+
raw = _yf_download_timed(["^NSEI", "^INDIAVIX"], timeout=15, period="1y",
|
| 252 |
+
auto_adjust=True, progress=False)
|
| 253 |
+
if raw is None:
|
| 254 |
+
raise ValueError("index download timed out")
|
| 255 |
nifty = raw["Close"]["^NSEI"].dropna()
|
| 256 |
vix = raw["Close"]["^INDIAVIX"].dropna()
|
| 257 |
except Exception:
|