Khanna, Videh Rakesh Rakesh commited on
Commit
c055932
·
1 Parent(s): f1c3844

Fix ML/intraday predictions getting stuck forever on Yahoo hangs

Browse files

get_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.

Files changed (2) hide show
  1. intraday_live.py +32 -4
  2. ml_predictor/infer.py +8 -2
intraday_live.py CHANGED
@@ -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
- with _YF_DOWNLOAD_LOCK:
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:
ml_predictor/infer.py CHANGED
@@ -244,8 +244,14 @@ class MLPredictor:
244
  return self._idx_cache["nifty"], self._idx_cache["vix"]
245
  nifty = vix = None
246
  try:
247
- import yfinance as yf
248
- raw = yf.download(["^NSEI", "^INDIAVIX"], period="1y", auto_adjust=True, progress=False)
 
 
 
 
 
 
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: