Jitendra12421 commited on
Commit
14589e4
·
verified ·
1 Parent(s): fed3916

Upload 39 files

Browse files
__pycache__/__init__.cpython-311.pyc ADDED
Binary file (211 Bytes). View file
 
__pycache__/app.cpython-311.pyc CHANGED
Binary files a/__pycache__/app.cpython-311.pyc and b/__pycache__/app.cpython-311.pyc differ
 
app.py CHANGED
@@ -12,22 +12,25 @@ from fastapi.middleware.cors import CORSMiddleware
12
  from fastapi import FastAPI
13
 
14
  sys.path.insert(0, str(Path(__file__).resolve().parent))
15
- from nifty_backend.runtime import (
16
- CLOSE_REFRESH_READY,
17
- IST,
18
- TPLUS1_READY,
19
- close_refresh_due,
20
- dashboard_payload,
21
- is_trading_day,
22
- latest_saved_prediction,
23
- latest_tplus1_prediction,
24
- next_trading_day,
25
- refresh_daily_data,
26
- refresh_first5_prediction,
27
- refresh_market_close_data,
28
- seconds_until_next_ist_run,
29
- warm_dashboard_payload_cache,
30
- )
 
 
 
31
 
32
 
33
  app = FastAPI(title="NIFTY 50 Forecaster Backend")
@@ -40,14 +43,15 @@ app.add_middleware(
40
  )
41
 
42
 
43
- market_status = "Waiting for next session"
44
- close_refresh_lock = threading.Lock()
45
- MARKET_OPEN = time(9, 15)
46
- FIRST5_READY = time(9, 20)
47
- MARKET_CLOSE = time(15, 30)
 
48
 
49
 
50
- def refresh_market_close_data_if_due() -> dict:
51
  if not close_refresh_due():
52
  return {"status": "skipped", "reason": "close refresh is not due"}
53
  if not close_refresh_lock.acquire(blocking=False):
@@ -55,8 +59,39 @@ def refresh_market_close_data_if_due() -> dict:
55
  try:
56
  info = refresh_market_close_data()
57
  return {"status": "refreshed", **info}
58
- finally:
59
- close_refresh_lock.release()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
 
61
 
62
  def latest_prediction_date(payload: dict | None = None) -> date | None:
@@ -68,7 +103,7 @@ def latest_prediction_date(payload: dict | None = None) -> date | None:
68
  return None
69
 
70
 
71
- def current_market_state(now: datetime | None = None) -> dict:
72
  global market_status
73
  now = now or datetime.now(IST)
74
  today = now.date()
@@ -78,11 +113,8 @@ def current_market_state(now: datetime | None = None) -> dict:
78
  market_is_open_for_t5 = trading_day and FIRST5_READY <= current_time < MARKET_CLOSE
79
  market_is_open_for_tplus1 = trading_day and TPLUS1_READY <= current_time < MARKET_CLOSE
80
  has_current_first5 = market_is_open_for_t5 and latest_date == today
81
- try:
82
- tplus1_latest_date = date.fromisoformat(str(latest_tplus1_prediction().get("input_date"))[:10])
83
- except Exception:
84
- tplus1_latest_date = None
85
- has_current_tplus1 = market_is_open_for_tplus1 and tplus1_latest_date == today
86
  next_session = today if trading_day and current_time < MARKET_CLOSE else next_trading_day(today + timedelta(days=1))
87
 
88
  if not trading_day:
@@ -107,11 +139,48 @@ def current_market_state(now: datetime | None = None) -> dict:
107
  else:
108
  status = "Prediction Pending"
109
  detail = "No current-session prediction has been generated yet."
110
- else:
111
- status = "Market Closed"
112
- detail = "Trading session has ended."
113
-
114
- return {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  "market_status": status,
116
  "market_detail": detail,
117
  "server_time_ist": now.isoformat(),
@@ -119,12 +188,16 @@ def current_market_state(now: datetime | None = None) -> dict:
119
  "session_date": today.isoformat(),
120
  "next_session_date": next_session.isoformat(),
121
  "latest_prediction_date": latest_date.isoformat() if latest_date else None,
122
- "t5_available": has_current_first5,
123
- "market_is_open_for_t5": market_is_open_for_t5,
124
- "tplus1_available": has_current_tplus1,
125
- "market_is_open_for_tplus1": market_is_open_for_tplus1,
126
- "latest_tplus1_prediction_date": tplus1_latest_date.isoformat() if tplus1_latest_date else None,
127
- }
 
 
 
 
128
 
129
 
130
  def attach_market_state(payload: dict) -> dict:
@@ -137,9 +210,7 @@ def attach_market_state(payload: dict) -> dict:
137
  tplus1_latest = payload.get("tplus1_latest") or {}
138
  t5_available = bool(state["t5_available"] and t5_latest.get("prediction"))
139
  tplus1_available = bool(state["tplus1_available"] and tplus1_latest.get("prediction"))
140
- market_closed = state["market_status"] == "Market Closed"
141
- unavailable_reason = "Market Closed" if market_closed else state["market_status"]
142
- tomorrow_available = bool(tomorrow_latest.get("prediction"))
143
  refresh_phase = payload.get("data_status", {}).get("refresh_phase")
144
  if refresh_phase in {"waiting_second_payload", "refreshing"}:
145
  tomorrow_status = "WAITING FOR SECOND PAYLOAD"
@@ -163,12 +234,12 @@ def attach_market_state(payload: dict) -> dict:
163
  "validation_accuracy": tomorrow_latest.get("validation_accuracy"),
164
  "test_accuracy": tomorrow_latest.get("test_accuracy"),
165
  },
166
- "t5": {
167
- "available": t5_available,
168
- "status": "Ready" if t5_available else unavailable_reason,
169
- "reason": None if t5_available else state["market_detail"],
170
- "input_date": t5_latest.get("input_date"),
171
- "prediction": t5_latest.get("prediction") if t5_available else None,
172
  "prob_up": t5_latest.get("prob_up") if t5_available else None,
173
  "confidence": t5_latest.get("confidence") if t5_available else None,
174
  "threshold": t5_latest.get("threshold") if t5_available else None,
@@ -176,12 +247,12 @@ def attach_market_state(payload: dict) -> dict:
176
  "validation_accuracy": (payload.get("summary") or {}).get("validation_accuracy"),
177
  "test_accuracy": (payload.get("summary") or {}).get("test_accuracy"),
178
  },
179
- "tplus1": {
180
- "available": tplus1_available,
181
- "status": "Ready" if tplus1_available else unavailable_reason,
182
- "reason": None if tplus1_available else state["market_detail"],
183
- "target_date": tplus1_latest.get("target_date") or state["next_session_date"],
184
- "input_date": tplus1_latest.get("input_date"),
185
  "prediction": tplus1_latest.get("prediction") if tplus1_available else None,
186
  "prob_up": tplus1_latest.get("prob_up") if tplus1_available else None,
187
  "confidence": tplus1_latest.get("confidence") if tplus1_available else None,
@@ -194,7 +265,7 @@ def attach_market_state(payload: dict) -> dict:
194
  return payload
195
 
196
 
197
- async def daily_ist_refresh_loop() -> None:
198
  global market_status
199
  while True:
200
  # Wait until 9:00 AM IST
@@ -222,14 +293,22 @@ async def daily_ist_refresh_loop() -> None:
222
  print(f"[scheduler] first5 refresh failed: {exc}", flush=True)
223
  market_status = "Prediction Failed"
224
 
225
- try:
226
- await asyncio.to_thread(refresh_daily_data)
227
- except Exception as exc:
228
- print(f"[scheduler] daily refresh failed: {exc}", flush=True)
229
-
230
- await asyncio.sleep(seconds_until_next_ist_run(CLOSE_REFRESH_READY))
231
- print("[scheduler] 3:45 PM IST - Refreshing close data", flush=True)
232
- try:
 
 
 
 
 
 
 
 
233
  info = await asyncio.to_thread(refresh_market_close_data_if_due)
234
  print(f"[scheduler] close refresh result: {info}", flush=True)
235
  except Exception as exc:
@@ -257,24 +336,44 @@ async def refresh_current_session_once() -> None:
257
  print(f"[startup] daily refresh failed: {exc}", flush=True)
258
 
259
 
260
- async def refresh_market_close_once_if_due() -> None:
261
  try:
262
  info = await asyncio.to_thread(refresh_market_close_data_if_due)
263
  if info.get("status") == "refreshed":
264
  print(f"[startup] close refresh result: {info}", flush=True)
265
  except Exception as exc:
266
- print(f"[startup] close refresh failed: {exc}", flush=True)
267
-
268
-
269
- async def warm_dashboard_payload_cache_once() -> None:
270
- try:
271
- await asyncio.to_thread(warm_dashboard_payload_cache)
272
- except Exception as exc:
273
- print(f"[startup] dashboard payload warmup failed: {exc}", flush=True)
274
-
275
-
276
- @app.on_event("startup")
277
- async def start_scheduler() -> None:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278
  global market_status
279
  # Initialize correct status on startup based on current time
280
  now = datetime.now(IST).time()
@@ -292,10 +391,12 @@ async def start_scheduler() -> None:
292
  else:
293
  market_status = "Prediction Pending"
294
 
295
- asyncio.create_task(refresh_current_session_once())
296
- asyncio.create_task(refresh_market_close_once_if_due())
297
- asyncio.create_task(warm_dashboard_payload_cache_once())
298
- asyncio.create_task(daily_ist_refresh_loop())
 
 
299
 
300
 
301
  @app.get("/health")
@@ -308,18 +409,27 @@ def root() -> dict[str, str]:
308
  return {"service": "NIFTY 50 Forecaster Backend", "status": "ok"}
309
 
310
 
311
- @app.get("/dashboard")
312
- def dashboard() -> dict:
313
- return attach_market_state(dashboard_payload())
314
 
315
 
316
  @app.get("/cron/keepalive")
317
- def cron_keepalive(background_tasks: BackgroundTasks) -> dict:
318
- close_refresh = {"status": "not_checked"}
319
- if close_refresh_due():
320
- background_tasks.add_task(refresh_market_close_data_if_due)
321
- close_refresh = {"status": "scheduled"}
322
- return {"status": "awake", "market": current_market_state(), "close_refresh": close_refresh}
 
 
 
 
 
 
 
 
 
323
 
324
 
325
  @app.get("/prediction/latest")
 
12
  from fastapi import FastAPI
13
 
14
  sys.path.insert(0, str(Path(__file__).resolve().parent))
15
+ from nifty_backend.runtime import (
16
+ CLOSE_REFRESH_READY,
17
+ IST,
18
+ STALE_CHECK_INTERVAL_SECONDS,
19
+ TPLUS1_READY,
20
+ close_refresh_due,
21
+ dashboard_payload,
22
+ is_trading_day,
23
+ latest_saved_prediction,
24
+ latest_tplus1_prediction,
25
+ next_trading_day,
26
+ refresh_daily_data,
27
+ refresh_first5_prediction,
28
+ refresh_market_close_data,
29
+ refresh_stale_data_once,
30
+ refresh_tplus1_prediction,
31
+ seconds_until_next_ist_run,
32
+ warm_dashboard_payload_cache,
33
+ )
34
 
35
 
36
  app = FastAPI(title="NIFTY 50 Forecaster Backend")
 
43
  )
44
 
45
 
46
+ market_status = "Waiting for next session"
47
+ close_refresh_lock = threading.Lock()
48
+ tplus1_refresh_lock = threading.Lock()
49
+ MARKET_OPEN = time(9, 15)
50
+ FIRST5_READY = time(9, 20)
51
+ MARKET_CLOSE = time(15, 30)
52
 
53
 
54
+ def refresh_market_close_data_if_due() -> dict:
55
  if not close_refresh_due():
56
  return {"status": "skipped", "reason": "close refresh is not due"}
57
  if not close_refresh_lock.acquire(blocking=False):
 
59
  try:
60
  info = refresh_market_close_data()
61
  return {"status": "refreshed", **info}
62
+ finally:
63
+ close_refresh_lock.release()
64
+
65
+
66
+ def latest_tplus1_prediction_date(payload: dict | None = None) -> date | None:
67
+ try:
68
+ latest = payload if payload is not None else latest_tplus1_prediction()
69
+ raw = latest.get("input_date")
70
+ return date.fromisoformat(str(raw)[:10]) if raw else None
71
+ except Exception:
72
+ return None
73
+
74
+
75
+ def tplus1_refresh_due(now: datetime | None = None, latest_date: date | None = None) -> bool:
76
+ now = now or datetime.now(IST)
77
+ if not is_trading_day(now.date()) or not (TPLUS1_READY <= now.time() < MARKET_CLOSE):
78
+ return False
79
+ latest_date = latest_date if latest_date is not None else latest_tplus1_prediction_date()
80
+ return latest_date != now.date()
81
+
82
+
83
+ def refresh_tplus1_if_due() -> dict:
84
+ now = datetime.now(IST)
85
+ latest_date = latest_tplus1_prediction_date()
86
+ if not tplus1_refresh_due(now=now, latest_date=latest_date):
87
+ return {"status": "skipped", "reason": "tplus1 refresh is not due"}
88
+ if not tplus1_refresh_lock.acquire(blocking=False):
89
+ return {"status": "skipped", "reason": "tplus1 refresh already running"}
90
+ try:
91
+ prediction = refresh_tplus1_prediction(session_date=now.date())
92
+ return {"status": "refreshed", "prediction": prediction}
93
+ finally:
94
+ tplus1_refresh_lock.release()
95
 
96
 
97
  def latest_prediction_date(payload: dict | None = None) -> date | None:
 
103
  return None
104
 
105
 
106
+ def current_market_state(now: datetime | None = None) -> dict:
107
  global market_status
108
  now = now or datetime.now(IST)
109
  today = now.date()
 
113
  market_is_open_for_t5 = trading_day and FIRST5_READY <= current_time < MARKET_CLOSE
114
  market_is_open_for_tplus1 = trading_day and TPLUS1_READY <= current_time < MARKET_CLOSE
115
  has_current_first5 = market_is_open_for_t5 and latest_date == today
116
+ tplus1_latest_date = latest_tplus1_prediction_date()
117
+ has_current_tplus1 = market_is_open_for_tplus1 and tplus1_latest_date == today
 
 
 
118
  next_session = today if trading_day and current_time < MARKET_CLOSE else next_trading_day(today + timedelta(days=1))
119
 
120
  if not trading_day:
 
139
  else:
140
  status = "Prediction Pending"
141
  detail = "No current-session prediction has been generated yet."
142
+ else:
143
+ status = "Market Closed"
144
+ detail = "Trading session has ended."
145
+
146
+ if not trading_day:
147
+ tplus1_status = "Market Closed"
148
+ tplus1_detail = f"Next trading session is {next_session.isoformat()}."
149
+ elif current_time < TPLUS1_READY:
150
+ tplus1_status = "Waiting for 2:30 PM"
151
+ tplus1_detail = "The T+1 forecast becomes available at 2:30 PM IST."
152
+ elif current_time < MARKET_CLOSE:
153
+ if has_current_tplus1:
154
+ tplus1_status = "Ready"
155
+ tplus1_detail = "Today's T+1 prediction is available."
156
+ else:
157
+ tplus1_status = "Pending"
158
+ tplus1_detail = "No current-session T+1 prediction has been generated yet."
159
+ else:
160
+ tplus1_status = "Market Closed"
161
+ tplus1_detail = "Trading session has ended."
162
+
163
+ if not trading_day:
164
+ t5_status = "Market Closed"
165
+ t5_detail = f"Next trading session is {next_session.isoformat()}."
166
+ elif current_time < FIRST5_READY:
167
+ t5_status = "Waiting for 9:20 AM"
168
+ t5_detail = "The T+5 forecast becomes available after the first five one-minute bars."
169
+ elif current_time < MARKET_CLOSE:
170
+ if market_status in {"Fetching T+5 Prediction Data...", "Prediction Failed"}:
171
+ t5_status = market_status
172
+ t5_detail = "The first-five-minute prediction job is still resolving."
173
+ elif has_current_first5:
174
+ t5_status = "Ready"
175
+ t5_detail = "Today's first-five-minute prediction is available."
176
+ else:
177
+ t5_status = "Pending"
178
+ t5_detail = "No current-session prediction has been generated yet."
179
+ else:
180
+ t5_status = "Market Closed"
181
+ t5_detail = "Trading session has ended."
182
+
183
+ return {
184
  "market_status": status,
185
  "market_detail": detail,
186
  "server_time_ist": now.isoformat(),
 
188
  "session_date": today.isoformat(),
189
  "next_session_date": next_session.isoformat(),
190
  "latest_prediction_date": latest_date.isoformat() if latest_date else None,
191
+ "t5_available": has_current_first5,
192
+ "t5_status": t5_status,
193
+ "t5_detail": t5_detail,
194
+ "market_is_open_for_t5": market_is_open_for_t5,
195
+ "tplus1_available": has_current_tplus1,
196
+ "tplus1_status": tplus1_status,
197
+ "tplus1_detail": tplus1_detail,
198
+ "market_is_open_for_tplus1": market_is_open_for_tplus1,
199
+ "latest_tplus1_prediction_date": tplus1_latest_date.isoformat() if tplus1_latest_date else None,
200
+ }
201
 
202
 
203
  def attach_market_state(payload: dict) -> dict:
 
210
  tplus1_latest = payload.get("tplus1_latest") or {}
211
  t5_available = bool(state["t5_available"] and t5_latest.get("prediction"))
212
  tplus1_available = bool(state["tplus1_available"] and tplus1_latest.get("prediction"))
213
+ tomorrow_available = bool(tomorrow_latest.get("prediction"))
 
 
214
  refresh_phase = payload.get("data_status", {}).get("refresh_phase")
215
  if refresh_phase in {"waiting_second_payload", "refreshing"}:
216
  tomorrow_status = "WAITING FOR SECOND PAYLOAD"
 
234
  "validation_accuracy": tomorrow_latest.get("validation_accuracy"),
235
  "test_accuracy": tomorrow_latest.get("test_accuracy"),
236
  },
237
+ "t5": {
238
+ "available": t5_available,
239
+ "status": "Ready" if t5_available else state["t5_status"],
240
+ "reason": None if t5_available else state["t5_detail"],
241
+ "input_date": t5_latest.get("input_date"),
242
+ "prediction": t5_latest.get("prediction") if t5_available else None,
243
  "prob_up": t5_latest.get("prob_up") if t5_available else None,
244
  "confidence": t5_latest.get("confidence") if t5_available else None,
245
  "threshold": t5_latest.get("threshold") if t5_available else None,
 
247
  "validation_accuracy": (payload.get("summary") or {}).get("validation_accuracy"),
248
  "test_accuracy": (payload.get("summary") or {}).get("test_accuracy"),
249
  },
250
+ "tplus1": {
251
+ "available": tplus1_available,
252
+ "status": "Ready" if tplus1_available else state["tplus1_status"],
253
+ "reason": None if tplus1_available else state["tplus1_detail"],
254
+ "target_date": tplus1_latest.get("target_date") or state["next_session_date"],
255
+ "input_date": tplus1_latest.get("input_date"),
256
  "prediction": tplus1_latest.get("prediction") if tplus1_available else None,
257
  "prob_up": tplus1_latest.get("prob_up") if tplus1_available else None,
258
  "confidence": tplus1_latest.get("confidence") if tplus1_available else None,
 
265
  return payload
266
 
267
 
268
+ async def daily_ist_refresh_loop() -> None:
269
  global market_status
270
  while True:
271
  # Wait until 9:00 AM IST
 
293
  print(f"[scheduler] first5 refresh failed: {exc}", flush=True)
294
  market_status = "Prediction Failed"
295
 
296
+ try:
297
+ await asyncio.to_thread(refresh_daily_data)
298
+ except Exception as exc:
299
+ print(f"[scheduler] daily refresh failed: {exc}", flush=True)
300
+
301
+ await asyncio.sleep(seconds_until_next_ist_run(TPLUS1_READY))
302
+ print("[scheduler] 2:30 PM IST - Refreshing T+1 prediction", flush=True)
303
+ try:
304
+ info = await asyncio.to_thread(refresh_tplus1_if_due)
305
+ print(f"[scheduler] tplus1 refresh result: {info}", flush=True)
306
+ except Exception as exc:
307
+ print(f"[scheduler] tplus1 refresh failed: {exc}", flush=True)
308
+
309
+ await asyncio.sleep(seconds_until_next_ist_run(CLOSE_REFRESH_READY))
310
+ print("[scheduler] 3:45 PM IST - Refreshing close data", flush=True)
311
+ try:
312
  info = await asyncio.to_thread(refresh_market_close_data_if_due)
313
  print(f"[scheduler] close refresh result: {info}", flush=True)
314
  except Exception as exc:
 
336
  print(f"[startup] daily refresh failed: {exc}", flush=True)
337
 
338
 
339
+ async def refresh_market_close_once_if_due() -> None:
340
  try:
341
  info = await asyncio.to_thread(refresh_market_close_data_if_due)
342
  if info.get("status") == "refreshed":
343
  print(f"[startup] close refresh result: {info}", flush=True)
344
  except Exception as exc:
345
+ print(f"[startup] close refresh failed: {exc}", flush=True)
346
+
347
+
348
+ async def refresh_tplus1_once_if_due() -> None:
349
+ try:
350
+ info = await asyncio.to_thread(refresh_tplus1_if_due)
351
+ if info.get("status") == "refreshed":
352
+ print(f"[startup] tplus1 refresh result: {info}", flush=True)
353
+ except Exception as exc:
354
+ print(f"[startup] tplus1 refresh failed: {exc}", flush=True)
355
+
356
+
357
+ async def warm_dashboard_payload_cache_once() -> None:
358
+ try:
359
+ await asyncio.to_thread(warm_dashboard_payload_cache)
360
+ except Exception as exc:
361
+ print(f"[startup] dashboard payload warmup failed: {exc}", flush=True)
362
+
363
+
364
+ async def stale_data_watch_loop() -> None:
365
+ while True:
366
+ try:
367
+ info = await asyncio.to_thread(refresh_stale_data_once)
368
+ if info.get("status") == "refreshed":
369
+ print(f"[stale-watch] refreshed stale data: {info}", flush=True)
370
+ except Exception as exc:
371
+ print(f"[stale-watch] stale refresh failed: {exc}", flush=True)
372
+ await asyncio.sleep(STALE_CHECK_INTERVAL_SECONDS)
373
+
374
+
375
+ @app.on_event("startup")
376
+ async def start_scheduler() -> None:
377
  global market_status
378
  # Initialize correct status on startup based on current time
379
  now = datetime.now(IST).time()
 
391
  else:
392
  market_status = "Prediction Pending"
393
 
394
+ asyncio.create_task(refresh_current_session_once())
395
+ asyncio.create_task(refresh_tplus1_once_if_due())
396
+ asyncio.create_task(refresh_market_close_once_if_due())
397
+ asyncio.create_task(warm_dashboard_payload_cache_once())
398
+ asyncio.create_task(stale_data_watch_loop())
399
+ asyncio.create_task(daily_ist_refresh_loop())
400
 
401
 
402
  @app.get("/health")
 
409
  return {"service": "NIFTY 50 Forecaster Backend", "status": "ok"}
410
 
411
 
412
+ @app.get("/dashboard")
413
+ def dashboard() -> dict:
414
+ return attach_market_state(dashboard_payload())
415
 
416
 
417
  @app.get("/cron/keepalive")
418
+ def cron_keepalive(background_tasks: BackgroundTasks) -> dict:
419
+ close_refresh = {"status": "not_checked"}
420
+ tplus1_refresh = {"status": "not_checked"}
421
+ if tplus1_refresh_due():
422
+ background_tasks.add_task(refresh_tplus1_if_due)
423
+ tplus1_refresh = {"status": "scheduled"}
424
+ if close_refresh_due():
425
+ background_tasks.add_task(refresh_market_close_data_if_due)
426
+ close_refresh = {"status": "scheduled"}
427
+ return {
428
+ "status": "awake",
429
+ "market": current_market_state(),
430
+ "tplus1_refresh": tplus1_refresh,
431
+ "close_refresh": close_refresh,
432
+ }
433
 
434
 
435
  @app.get("/prediction/latest")
data/nifty50_1d.parquet CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:b57d445258fc7ff258e04869c7b233c2d8e483db82d93917a8c28f16b5ee0d19
3
- size 78241
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:be744722b6c72c2fade81cc25551e32edcbda4737d02e6bc6ff8f0dff4b31d90
3
+ size 78275
data/nifty50_1m.parquet CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:eff0ea13a459412466def2b530b482e84e06346ee4f7203519689baf0adce32c
3
- size 18555782
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:216816fb4cb1b022029e3e1ab88b344e2c6dcfb65a51d2e1b70ab76dc3320a45
3
+ size 18580743
data/opening_direction_training_dataset.parquet CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:c21f3c5e7781b67e9849a6d07c9ec28d3f087b74be7e2472785124ceb35a34f4
3
- size 4462258
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5293909811782086d6c16ea35e8b4313dbcea6c928e6ffc07b342502dc94f466
3
+ size 4463019
models/latest_prediction.csv CHANGED
@@ -1,2 +1,2 @@
1
- input_date,first5_start,first5_end,prediction,prob_up,confidence,threshold,model_name
2
- 2026-05-21,2026-05-21 09:15:00,2026-05-21 09:19:00,UP,0.4379885393062092,0.5129885393062092,0.425,blend_extra_trees_tight_logit_overlay
 
1
+ input_date,first5_start,first5_end,prediction,prob_up,confidence,threshold,model_name,is_overridden
2
+ 2026-05-26,2026-05-26 09:15:00,2026-05-26 09:19:00,DOWN,0.6058803888947725,0.6808803888947725,0.425,blend_extra_trees_tight_logit_overlay,True
models/tomorrow_latest_prediction.csv CHANGED
@@ -1,2 +1,2 @@
1
  input_date,target_date,prediction,prob_up,confidence,threshold,model_name,source_model,validation_accuracy,test_accuracy
2
- 2026-05-25,2026-05-26,UP,0.5649926672304149,0.5649926672304149,0.543,nifty_tomorrow_direction_model,tuned_daily_forest_single,0.5780141843971631,0.6182795698924731
 
1
  input_date,target_date,prediction,prob_up,confidence,threshold,model_name,source_model,validation_accuracy,test_accuracy
2
+ 2026-05-26,2026-05-27,DOWN,0.4892079705003583,0.5107920294996418,0.543,nifty_tomorrow_direction_model,tuned_daily_forest_single,0.5780141843971631,0.6182795698924731
models/tomorrow_summary.json CHANGED
@@ -25,13 +25,13 @@
25
  "valid_end": "2025-08-17",
26
  "test_start": "2025-08-18",
27
  "test_end": "2026-05-20",
28
- "latest_forecast_date": "2026-05-25",
29
- "latest_forecast_for": "next trading session 2026-05-26",
30
- "latest_forecast_prob_up": 0.5649926672304149,
31
- "latest_forecast_signal": "UP",
32
  "feature_count": 301,
33
  "model_name": "nifty_tomorrow_direction_model",
34
  "source_model": "tuned_daily_forest_single",
35
  "target": "next trading session NIFTY 50 direction",
36
- "latest_target_date": "2026-05-26"
37
  }
 
25
  "valid_end": "2025-08-17",
26
  "test_start": "2025-08-18",
27
  "test_end": "2026-05-20",
28
+ "latest_forecast_date": "2026-05-26",
29
+ "latest_forecast_for": "next trading session 2026-05-27",
30
+ "latest_forecast_prob_up": 0.4892079705003583,
31
+ "latest_forecast_signal": "DOWN",
32
  "feature_count": 301,
33
  "model_name": "nifty_tomorrow_direction_model",
34
  "source_model": "tuned_daily_forest_single",
35
  "target": "next trading session NIFTY 50 direction",
36
+ "latest_target_date": "2026-05-27"
37
  }
models/tplus1_latest_prediction.csv CHANGED
@@ -1,2 +1,2 @@
1
  input_date,target_date,forecast_for,prediction,prob_up,confidence,threshold,model_name,decision_overlay,validation_accuracy,test_accuracy,accuracy_goal
2
- 2026-05-21,2026-05-22,next trading session after 2026-05-21,UP,0.620604654276822,0.620604654276822,0.578,logistic_regression_l1_C0.35_balanced,prev_target_mean10_le_0.4_up;m02_range_1m_ge_0.000479116_up,0.66,0.6368421052631579,0.63
 
1
  input_date,target_date,forecast_for,prediction,prob_up,confidence,threshold,model_name,decision_overlay,validation_accuracy,test_accuracy,accuracy_goal
2
+ 2026-05-26,2026-05-27,next trading session after 2026-05-26,UP,0.48291233826421653,0.5170876617357835,0.578,logistic_regression_l1_C0.35_balanced,prev_target_mean10_le_0.4_up;m02_range_1m_ge_0.000479116_up,0.66,0.6368421052631579,0.63
models/yahoo_history_cache.sqlite3 ADDED
Binary file (77.8 kB). View file
 
nifty_backend/__pycache__/runtime.cpython-311.pyc CHANGED
Binary files a/nifty_backend/__pycache__/runtime.cpython-311.pyc and b/nifty_backend/__pycache__/runtime.cpython-311.pyc differ
 
nifty_backend/__pycache__/yahoo_history_client.cpython-311.pyc ADDED
Binary file (27.8 kB). View file
 
nifty_backend/runtime.py CHANGED
@@ -11,10 +11,10 @@ from pathlib import Path
11
  from typing import Any
12
  from zoneinfo import ZoneInfo
13
 
14
- import joblib
15
- import numpy as np
16
- import pandas as pd
17
- import yfinance as yf
18
 
19
  try:
20
  import pandas_market_calendars as mcal
@@ -23,13 +23,16 @@ except ImportError: # pragma: no cover - production dependency, local fallback
23
 
24
 
25
  IST = ZoneInfo("Asia/Kolkata")
26
- YAHOO_NIFTY_SYMBOL = "^NSEI"
27
- MARKET_CLOSE = time(15, 30)
28
- CLOSE_REFRESH_READY = time(15, 45)
29
- TPLUS1_READY = time(14, 30)
30
- BACKEND_ROOT = Path(__file__).resolve().parents[1]
31
- DATA_DIR = BACKEND_ROOT / "data"
32
- MODEL_DIR = BACKEND_ROOT / "models"
 
 
 
33
  OPENING_DATASET_PATH = DATA_DIR / "opening_direction_training_dataset.parquet"
34
  NIFTY_1M_PATH = DATA_DIR / "nifty50_1m.parquet"
35
  NIFTY_1D_PATH = DATA_DIR / "nifty50_1d.parquet"
@@ -66,7 +69,8 @@ DECISION_OVERLAYS = [
66
  },
67
  ]
68
 
69
- _dashboard_payload_lock = threading.Lock()
 
70
 
71
 
72
  def utc_now_iso() -> str:
@@ -262,7 +266,7 @@ def read_training_dataset() -> pd.DataFrame:
262
  return df.sort_values("date").reset_index(drop=True)
263
 
264
 
265
- def normalize_yahoo_frame(df: pd.DataFrame) -> pd.DataFrame:
266
  if df.empty:
267
  return pd.DataFrame(columns=["date", "open", "high", "low", "close", "volume"])
268
  if isinstance(df.columns, pd.MultiIndex):
@@ -288,19 +292,81 @@ def normalize_yahoo_frame(df: pd.DataFrame) -> pd.DataFrame:
288
  for src, dst in rename.items():
289
  if src in df.columns and dst not in out.columns:
290
  out[dst] = pd.to_numeric(df[src], errors="coerce")
291
- return out.dropna(subset=["date", "open", "high", "low", "close"]).sort_values("date")
292
-
293
-
294
- def fetch_yahoo_minutes(period: str = "5d") -> pd.DataFrame:
295
- raw = yf.download(YAHOO_NIFTY_SYMBOL, period=period, interval="1m", progress=False, prepost=False, auto_adjust=False)
296
- return normalize_yahoo_frame(raw)
297
-
298
-
299
- def fetch_yahoo_daily(period: str = "1mo") -> pd.DataFrame:
300
- raw = yf.download(YAHOO_NIFTY_SYMBOL, period=period, interval="1d", progress=False, prepost=False, auto_adjust=False)
301
- out = normalize_yahoo_frame(raw)
302
- out["date"] = pd.to_datetime(out["date"], errors="coerce").dt.normalize()
303
- return out.drop_duplicates("date", keep="last")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
304
 
305
 
306
  def append_parquet_rows(path: Path, new_rows: pd.DataFrame, subset: list[str]) -> pd.DataFrame:
@@ -675,19 +741,27 @@ def _apply_tplus1_overlays(pred: np.ndarray, frame: pd.DataFrame, overlays: list
675
  return adjusted
676
 
677
 
678
- def refresh_tplus1_prediction(session_date: date | None = None) -> dict[str, Any]:
679
- if not TPLUS1_MODEL_PATH.exists():
680
- raise FileNotFoundError(f"Missing T+1 model artifact: {TPLUS1_MODEL_PATH}")
681
- payload = joblib.load(TPLUS1_MODEL_PATH)
682
- features = payload["features"]
683
- threshold = float(payload["threshold"])
684
- frame = _add_tplus1_target_features(_build_tplus1_session_features(_minute_frame_for_tplus1()))
685
- if session_date is not None:
686
- row = frame[pd.to_datetime(frame["date"], errors="coerce").dt.date == session_date].tail(1)
687
- else:
688
- row = frame.tail(1)
689
- if row.empty:
690
- raise RuntimeError("No complete 14:00-14:20 window is available for T+1 prediction.")
 
 
 
 
 
 
 
 
691
  missing = [col for col in features if col not in row.columns]
692
  if missing:
693
  raise RuntimeError(f"T+1 feature row is missing model features: {missing[:5]}")
@@ -1103,7 +1177,7 @@ def refresh_market_close_data(session_date: date | None = None) -> dict[str, Any
1103
  raise
1104
 
1105
 
1106
- def close_refresh_due(now: datetime | None = None) -> bool:
1107
  now = now or datetime.now(IST)
1108
  if not is_trading_day(now.date()) or now.time() < CLOSE_REFRESH_READY:
1109
  return False
@@ -1118,13 +1192,123 @@ def close_refresh_due(now: datetime | None = None) -> bool:
1118
  tomorrow_input = date.fromisoformat(str(tomorrow_latest.get("input_date"))[:10])
1119
  except Exception:
1120
  tomorrow_input = None
1121
- return any(
1122
- latest != now.date()
1123
- for latest in (latest_daily, latest_minutes, latest_opening, latest_opening_outcome, tomorrow_input)
1124
- )
1125
-
1126
-
1127
- def next_ist_run_at(run_time: time = time(9, 20), now: datetime | None = None) -> datetime:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1128
  now = now or datetime.now(IST)
1129
  target_day = now.date()
1130
  if now >= datetime.combine(target_day, run_time, tzinfo=IST):
 
11
  from typing import Any
12
  from zoneinfo import ZoneInfo
13
 
14
+ import joblib
15
+ import numpy as np
16
+ import pandas as pd
17
+ from nifty_backend.yahoo_history_client import YahooHistoryClient
18
 
19
  try:
20
  import pandas_market_calendars as mcal
 
23
 
24
 
25
  IST = ZoneInfo("Asia/Kolkata")
26
+ YAHOO_NIFTY_SYMBOL = "^NSEI"
27
+ MARKET_CLOSE = time(15, 30)
28
+ FIRST5_READY = time(9, 20)
29
+ CLOSE_REFRESH_READY = time(15, 45)
30
+ TPLUS1_READY = time(14, 30)
31
+ STALE_CHECK_INTERVAL_SECONDS = 5
32
+ BACKEND_ROOT = Path(__file__).resolve().parents[1]
33
+ DATA_DIR = BACKEND_ROOT / "data"
34
+ MODEL_DIR = BACKEND_ROOT / "models"
35
+ YAHOO_CACHE_PATH = MODEL_DIR / "yahoo_history_cache.sqlite3"
36
  OPENING_DATASET_PATH = DATA_DIR / "opening_direction_training_dataset.parquet"
37
  NIFTY_1M_PATH = DATA_DIR / "nifty50_1m.parquet"
38
  NIFTY_1D_PATH = DATA_DIR / "nifty50_1d.parquet"
 
69
  },
70
  ]
71
 
72
+ _dashboard_payload_lock = threading.Lock()
73
+ _stale_refresh_lock = threading.Lock()
74
 
75
 
76
  def utc_now_iso() -> str:
 
266
  return df.sort_values("date").reset_index(drop=True)
267
 
268
 
269
+ def normalize_yahoo_frame(df: pd.DataFrame) -> pd.DataFrame:
270
  if df.empty:
271
  return pd.DataFrame(columns=["date", "open", "high", "low", "close", "volume"])
272
  if isinstance(df.columns, pd.MultiIndex):
 
292
  for src, dst in rename.items():
293
  if src in df.columns and dst not in out.columns:
294
  out[dst] = pd.to_numeric(df[src], errors="coerce")
295
+ return out.dropna(subset=["date", "open", "high", "low", "close"]).sort_values("date")
296
+
297
+
298
+ @lru_cache(maxsize=1)
299
+ def yahoo_history_client() -> YahooHistoryClient:
300
+ return YahooHistoryClient(cache_path=YAHOO_CACHE_PATH)
301
+
302
+
303
+ def period_start(period: str, *, end: datetime) -> datetime:
304
+ text = str(period).strip().lower()
305
+ units = {
306
+ "d": "days",
307
+ "wk": "weeks",
308
+ "mo": "months",
309
+ "y": "years",
310
+ }
311
+ for suffix, unit in units.items():
312
+ if text.endswith(suffix):
313
+ raw_value = text[: -len(suffix)]
314
+ if not raw_value.isdigit():
315
+ break
316
+ value = int(raw_value)
317
+ if unit == "days":
318
+ return end - timedelta(days=value)
319
+ if unit == "weeks":
320
+ return end - timedelta(weeks=value)
321
+ if unit == "months":
322
+ return end - timedelta(days=value * 31)
323
+ if unit == "years":
324
+ return end - timedelta(days=value * 366)
325
+ raise ValueError(f"Unsupported Yahoo period: {period!r}")
326
+
327
+
328
+ def yahoo_history_to_ohlcv(frame: pd.DataFrame, *, daily: bool) -> pd.DataFrame:
329
+ if frame.empty:
330
+ return pd.DataFrame(columns=["date", "open", "high", "low", "close", "volume"])
331
+ out = frame.rename(columns={"timestamp": "date"}).copy()
332
+ out["date"] = pd.to_datetime(out["date"], errors="coerce")
333
+ if daily:
334
+ out["date"] = out["date"].dt.normalize()
335
+ for column in ("open", "high", "low", "close", "volume"):
336
+ out[column] = pd.to_numeric(out[column], errors="coerce")
337
+ return (
338
+ out[["date", "open", "high", "low", "close", "volume"]]
339
+ .dropna(subset=["date", "open", "high", "low", "close"])
340
+ .drop_duplicates("date", keep="last")
341
+ .sort_values("date")
342
+ .reset_index(drop=True)
343
+ )
344
+
345
+
346
+ def fetch_yahoo_minutes(period: str = "5d") -> pd.DataFrame:
347
+ end = datetime.now(IST).replace(tzinfo=None) + timedelta(minutes=5)
348
+ start = period_start(period, end=end)
349
+ raw = yahoo_history_client().fetch_history(
350
+ YAHOO_NIFTY_SYMBOL,
351
+ interval="1m",
352
+ start=start,
353
+ end=end,
354
+ include_prepost=False,
355
+ )
356
+ return yahoo_history_to_ohlcv(raw, daily=False)
357
+
358
+
359
+ def fetch_yahoo_daily(period: str = "1mo") -> pd.DataFrame:
360
+ end = datetime.now(IST).replace(tzinfo=None) + timedelta(days=1)
361
+ start = period_start(period, end=end)
362
+ raw = yahoo_history_client().fetch_history(
363
+ YAHOO_NIFTY_SYMBOL,
364
+ interval="1d",
365
+ start=start,
366
+ end=end,
367
+ include_prepost=False,
368
+ )
369
+ return yahoo_history_to_ohlcv(raw, daily=True)
370
 
371
 
372
  def append_parquet_rows(path: Path, new_rows: pd.DataFrame, subset: list[str]) -> pd.DataFrame:
 
741
  return adjusted
742
 
743
 
744
+ def refresh_tplus1_prediction(session_date: date | None = None) -> dict[str, Any]:
745
+ if not TPLUS1_MODEL_PATH.exists():
746
+ raise FileNotFoundError(f"Missing T+1 model artifact: {TPLUS1_MODEL_PATH}")
747
+ payload = joblib.load(TPLUS1_MODEL_PATH)
748
+ features = payload["features"]
749
+ threshold = float(payload["threshold"])
750
+ frame = _add_tplus1_target_features(_build_tplus1_session_features(_minute_frame_for_tplus1()))
751
+ if session_date is not None:
752
+ row = frame[pd.to_datetime(frame["date"], errors="coerce").dt.date == session_date].tail(1)
753
+ else:
754
+ row = frame.tail(1)
755
+ if row.empty:
756
+ minutes = fetch_yahoo_minutes(period="7d")
757
+ append_parquet_rows(NIFTY_1M_PATH, minutes, ["date"])
758
+ frame = _add_tplus1_target_features(_build_tplus1_session_features(_minute_frame_for_tplus1()))
759
+ if session_date is not None:
760
+ row = frame[pd.to_datetime(frame["date"], errors="coerce").dt.date == session_date].tail(1)
761
+ else:
762
+ row = frame.tail(1)
763
+ if row.empty:
764
+ raise RuntimeError("No complete 14:00-14:20 window is available for T+1 prediction.")
765
  missing = [col for col in features if col not in row.columns]
766
  if missing:
767
  raise RuntimeError(f"T+1 feature row is missing model features: {missing[:5]}")
 
1177
  raise
1178
 
1179
 
1180
+ def close_refresh_due(now: datetime | None = None) -> bool:
1181
  now = now or datetime.now(IST)
1182
  if not is_trading_day(now.date()) or now.time() < CLOSE_REFRESH_READY:
1183
  return False
 
1192
  tomorrow_input = date.fromisoformat(str(tomorrow_latest.get("input_date"))[:10])
1193
  except Exception:
1194
  tomorrow_input = None
1195
+ return any(
1196
+ latest != now.date()
1197
+ for latest in (latest_daily, latest_minutes, latest_opening, latest_opening_outcome, tomorrow_input)
1198
+ )
1199
+
1200
+
1201
+ def latest_prediction_input_date(path: Path) -> date | None:
1202
+ if not path.exists():
1203
+ return None
1204
+ try:
1205
+ frame = pd.read_csv(path, usecols=["input_date"])
1206
+ except Exception:
1207
+ return None
1208
+ if frame.empty:
1209
+ return None
1210
+ value = pd.to_datetime(frame["input_date"], errors="coerce").max()
1211
+ return None if pd.isna(value) else value.date()
1212
+
1213
+
1214
+ def expected_completed_daily_date(now: datetime | None = None) -> date:
1215
+ now = now or datetime.now(IST)
1216
+ if is_trading_day(now.date()) and now.time() < CLOSE_REFRESH_READY:
1217
+ return previous_trading_day(now.date() - timedelta(days=1))
1218
+ return previous_trading_day(now.date())
1219
+
1220
+
1221
+ def expected_minute_date(now: datetime | None = None) -> date:
1222
+ now = now or datetime.now(IST)
1223
+ if is_trading_day(now.date()) and now.time() >= FIRST5_READY:
1224
+ return now.date()
1225
+ return previous_trading_day(now.date() - timedelta(days=1))
1226
+
1227
+
1228
+ def expected_tplus1_date(now: datetime | None = None) -> date:
1229
+ now = now or datetime.now(IST)
1230
+ if is_trading_day(now.date()) and now.time() >= TPLUS1_READY:
1231
+ return now.date()
1232
+ return previous_trading_day(now.date() - timedelta(days=1))
1233
+
1234
+
1235
+ def is_stale(latest: date | None, expected: date) -> bool:
1236
+ return latest is None or latest < expected
1237
+
1238
+
1239
+ def stale_data_status(now: datetime | None = None) -> dict[str, Any]:
1240
+ now = now or datetime.now(IST)
1241
+ expected_daily = expected_completed_daily_date(now)
1242
+ expected_minutes = expected_minute_date(now)
1243
+ expected_tplus1 = expected_tplus1_date(now)
1244
+ latest_daily = latest_parquet_date(NIFTY_1D_PATH)
1245
+ latest_minutes = latest_parquet_date(NIFTY_1M_PATH)
1246
+ latest_t5 = latest_prediction_input_date(LATEST_PATH)
1247
+ latest_tplus1 = latest_prediction_input_date(TPLUS1_LATEST_PATH)
1248
+ return {
1249
+ "server_time_ist": now.isoformat(),
1250
+ "expected_daily_date": expected_daily.isoformat(),
1251
+ "expected_minute_date": expected_minutes.isoformat(),
1252
+ "expected_tplus1_date": expected_tplus1.isoformat(),
1253
+ "latest_daily_date": latest_daily.isoformat() if latest_daily else None,
1254
+ "latest_minute_date": latest_minutes.isoformat() if latest_minutes else None,
1255
+ "latest_t5_date": latest_t5.isoformat() if latest_t5 else None,
1256
+ "latest_tplus1_date": latest_tplus1.isoformat() if latest_tplus1 else None,
1257
+ "daily_stale": is_stale(latest_daily, expected_daily),
1258
+ "minutes_stale": is_stale(latest_minutes, expected_minutes),
1259
+ "t5_stale": is_stale(latest_t5, expected_minutes),
1260
+ "tplus1_stale": is_stale(latest_tplus1, expected_tplus1),
1261
+ }
1262
+
1263
+
1264
+ def refresh_stale_data_once(now: datetime | None = None) -> dict[str, Any]:
1265
+ now = now or datetime.now(IST)
1266
+ status = stale_data_status(now)
1267
+ if not any(status[key] for key in ("daily_stale", "minutes_stale", "t5_stale", "tplus1_stale")):
1268
+ return {"status": "fresh", **status, "actions": []}
1269
+ if not _stale_refresh_lock.acquire(blocking=False):
1270
+ return {"status": "skipped", "reason": "stale refresh already running", **status, "actions": []}
1271
+
1272
+ actions: list[dict[str, Any]] = []
1273
+ try:
1274
+ if status["minutes_stale"]:
1275
+ minutes = fetch_yahoo_minutes(period="7d")
1276
+ combined = append_parquet_rows(NIFTY_1M_PATH, minutes, ["date"])
1277
+ actions.append(
1278
+ {
1279
+ "name": "minutes",
1280
+ "rows": int(len(combined)),
1281
+ "latest_date": pd.to_datetime(combined["date"], errors="coerce").max().date().isoformat(),
1282
+ }
1283
+ )
1284
+
1285
+ if status["daily_stale"]:
1286
+ daily_info = refresh_daily_data()
1287
+ outcomes = update_opening_outcomes_from_daily()
1288
+ actions.append({"name": "daily", **daily_info})
1289
+ actions.append({"name": "opening_outcomes", **outcomes})
1290
+ try:
1291
+ tomorrow = refresh_tomorrow_prediction(session_date=date.fromisoformat(status["expected_daily_date"]))
1292
+ actions.append({"name": "tomorrow_prediction", "input_date": tomorrow.get("input_date")})
1293
+ except Exception as exc:
1294
+ actions.append({"name": "tomorrow_prediction", "error": str(exc)})
1295
+
1296
+ if status["t5_stale"] and is_trading_day(now.date()) and now.time() >= FIRST5_READY:
1297
+ prediction = refresh_first5_prediction(session_date=now.date())
1298
+ actions.append({"name": "t5_prediction", "input_date": prediction.input_date})
1299
+
1300
+ if status["tplus1_stale"] and is_trading_day(now.date()) and now.time() >= TPLUS1_READY:
1301
+ prediction = refresh_tplus1_prediction(session_date=now.date())
1302
+ actions.append({"name": "tplus1_prediction", "input_date": prediction.get("input_date")})
1303
+
1304
+ clear_dashboard_payload_cache()
1305
+ refreshed_status = stale_data_status(datetime.now(IST))
1306
+ return {"status": "refreshed", **refreshed_status, "actions": actions}
1307
+ finally:
1308
+ _stale_refresh_lock.release()
1309
+
1310
+
1311
+ def next_ist_run_at(run_time: time = time(9, 20), now: datetime | None = None) -> datetime:
1312
  now = now or datetime.now(IST)
1313
  target_day = now.date()
1314
  if now >= datetime.combine(target_day, run_time, tzinfo=IST):
nifty_backend/yahoo_history_client.py ADDED
@@ -0,0 +1,445 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import gzip
4
+ import hashlib
5
+ import json
6
+ import sqlite3
7
+ import threading
8
+ import time
9
+ from dataclasses import dataclass
10
+ from datetime import datetime, timedelta
11
+ from itertools import cycle
12
+ from pathlib import Path
13
+ from typing import Any
14
+ from zoneinfo import ZoneInfo
15
+
16
+ import pandas as pd
17
+ import requests
18
+ from requests.adapters import HTTPAdapter
19
+ from urllib3.util.retry import Retry
20
+
21
+
22
+ YAHOO_CHART_HOSTS = (
23
+ "https://query1.finance.yahoo.com",
24
+ "https://query2.finance.yahoo.com",
25
+ )
26
+ DEFAULT_HEADERS = {
27
+ "User-Agent": (
28
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
29
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
30
+ "Chrome/136.0 Safari/537.36"
31
+ ),
32
+ "Accept": "application/json,text/plain,*/*",
33
+ "Accept-Language": "en-US,en;q=0.9",
34
+ "Connection": "keep-alive",
35
+ "Origin": "https://finance.yahoo.com",
36
+ "Referer": "https://finance.yahoo.com/",
37
+ }
38
+ BAR_COLUMNS = ["timestamp", "open", "high", "low", "close", "adj_close", "volume", "dividend", "split_ratio"]
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class IntervalPolicy:
43
+ interval: str
44
+ chunk_days: int
45
+ min_chunk_days: int
46
+ retention_days: int | None
47
+
48
+
49
+ INTERVAL_POLICIES: dict[str, IntervalPolicy] = {
50
+ "1m": IntervalPolicy("1m", chunk_days=7, min_chunk_days=1, retention_days=30),
51
+ "2m": IntervalPolicy("2m", chunk_days=60, min_chunk_days=2, retention_days=60),
52
+ "5m": IntervalPolicy("5m", chunk_days=60, min_chunk_days=5, retention_days=60),
53
+ "15m": IntervalPolicy("15m", chunk_days=60, min_chunk_days=5, retention_days=60),
54
+ "30m": IntervalPolicy("30m", chunk_days=60, min_chunk_days=5, retention_days=60),
55
+ "60m": IntervalPolicy("60m", chunk_days=60, min_chunk_days=5, retention_days=60),
56
+ "90m": IntervalPolicy("90m", chunk_days=60, min_chunk_days=5, retention_days=60),
57
+ "1h": IntervalPolicy("1h", chunk_days=60, min_chunk_days=5, retention_days=60),
58
+ "1d": IntervalPolicy("1d", chunk_days=3650, min_chunk_days=30, retention_days=None),
59
+ "5d": IntervalPolicy("5d", chunk_days=3650, min_chunk_days=30, retention_days=None),
60
+ "1wk": IntervalPolicy("1wk", chunk_days=3650, min_chunk_days=30, retention_days=None),
61
+ "1mo": IntervalPolicy("1mo", chunk_days=3650, min_chunk_days=30, retention_days=None),
62
+ "3mo": IntervalPolicy("3mo", chunk_days=3650, min_chunk_days=30, retention_days=None),
63
+ }
64
+
65
+
66
+ class YahooHistoryError(RuntimeError):
67
+ pass
68
+
69
+
70
+ class YahooSymbolError(YahooHistoryError):
71
+ pass
72
+
73
+
74
+ class YahooIntervalLimitError(YahooHistoryError):
75
+ pass
76
+
77
+
78
+ class YahooRateLimitError(YahooHistoryError):
79
+ pass
80
+
81
+
82
+ class SqliteResponseCache:
83
+ def __init__(self, path: Path) -> None:
84
+ self.path = path
85
+ self.path.parent.mkdir(parents=True, exist_ok=True)
86
+ self._lock = threading.Lock()
87
+ with self._connect() as connection:
88
+ connection.execute(
89
+ """
90
+ CREATE TABLE IF NOT EXISTS response_cache (
91
+ cache_key TEXT PRIMARY KEY,
92
+ fetched_at INTEGER NOT NULL,
93
+ payload_gzip BLOB NOT NULL
94
+ )
95
+ """
96
+ )
97
+
98
+ def _connect(self) -> sqlite3.Connection:
99
+ connection = sqlite3.connect(self.path)
100
+ connection.execute("PRAGMA journal_mode=WAL")
101
+ connection.execute("PRAGMA synchronous=NORMAL")
102
+ return connection
103
+
104
+ def get(self, cache_key: str, ttl_seconds: int) -> dict[str, Any] | None:
105
+ with self._lock, self._connect() as connection:
106
+ row = connection.execute(
107
+ "SELECT fetched_at, payload_gzip FROM response_cache WHERE cache_key = ?",
108
+ (cache_key,),
109
+ ).fetchone()
110
+ if row is None:
111
+ return None
112
+ fetched_at, payload_gzip = row
113
+ if int(time.time()) - int(fetched_at) > ttl_seconds:
114
+ return None
115
+ return json.loads(gzip.decompress(payload_gzip).decode("utf-8"))
116
+
117
+ def set(self, cache_key: str, payload: dict[str, Any]) -> None:
118
+ packed = gzip.compress(json.dumps(payload, separators=(",", ":"), ensure_ascii=True).encode("utf-8"))
119
+ with self._lock, self._connect() as connection:
120
+ connection.execute(
121
+ """
122
+ INSERT INTO response_cache (cache_key, fetched_at, payload_gzip)
123
+ VALUES (?, ?, ?)
124
+ ON CONFLICT(cache_key) DO UPDATE SET
125
+ fetched_at = excluded.fetched_at,
126
+ payload_gzip = excluded.payload_gzip
127
+ """,
128
+ (cache_key, int(time.time()), packed),
129
+ )
130
+
131
+
132
+ class RateLimiter:
133
+ def __init__(self, min_gap_seconds: float) -> None:
134
+ self.min_gap_seconds = max(0.0, float(min_gap_seconds))
135
+ self._lock = threading.Lock()
136
+ self._next_allowed = 0.0
137
+
138
+ def wait(self) -> None:
139
+ with self._lock:
140
+ delay = self._next_allowed - time.monotonic()
141
+ if delay > 0:
142
+ time.sleep(delay)
143
+ self._next_allowed = time.monotonic() + self.min_gap_seconds
144
+
145
+
146
+ class YahooHistoryClient:
147
+ def __init__(
148
+ self,
149
+ *,
150
+ cache_path: Path,
151
+ timeout_seconds: float = 25.0,
152
+ min_request_gap_seconds: float = 0.35,
153
+ max_retries: int = 5,
154
+ ) -> None:
155
+ self.cache = SqliteResponseCache(cache_path)
156
+ self.timeout_seconds = timeout_seconds
157
+ self.rate_limiter = RateLimiter(min_request_gap_seconds)
158
+ self.host_cycle = cycle(YAHOO_CHART_HOSTS)
159
+ self.session = self._build_session(max_retries=max_retries)
160
+
161
+ def _build_session(self, *, max_retries: int) -> requests.Session:
162
+ retry = Retry(
163
+ total=max_retries,
164
+ connect=max_retries,
165
+ read=max_retries,
166
+ backoff_factor=0.8,
167
+ status_forcelist=(429, 500, 502, 503, 504),
168
+ allowed_methods=("GET",),
169
+ respect_retry_after_header=True,
170
+ raise_on_status=False,
171
+ )
172
+ adapter = HTTPAdapter(max_retries=retry, pool_connections=16, pool_maxsize=16)
173
+ session = requests.Session()
174
+ session.headers.update(DEFAULT_HEADERS)
175
+ session.mount("https://", adapter)
176
+ session.mount("http://", adapter)
177
+ return session
178
+
179
+ def fetch_history(
180
+ self,
181
+ symbol: str,
182
+ *,
183
+ interval: str,
184
+ start: str | datetime,
185
+ end: str | datetime,
186
+ include_prepost: bool = False,
187
+ adjust_ohlc: bool = False,
188
+ ) -> pd.DataFrame:
189
+ policy = self._interval_policy(interval)
190
+ start_dt = self._coerce_datetime(start, end_of_day=False)
191
+ end_dt = self._coerce_datetime(end, end_of_day=True)
192
+ if end_dt <= start_dt:
193
+ raise ValueError("end must be later than start")
194
+ self._validate_retention_window(policy=policy, start_dt=start_dt, end_dt=end_dt)
195
+
196
+ frames: list[pd.DataFrame] = []
197
+ for chunk_start, chunk_end in self._iter_chunks(start_dt=start_dt, end_dt=end_dt, chunk_days=policy.chunk_days):
198
+ chunk = self._fetch_chunk_adaptive(
199
+ symbol=symbol,
200
+ interval=policy.interval,
201
+ chunk_start=chunk_start,
202
+ chunk_end=chunk_end,
203
+ min_chunk_days=policy.min_chunk_days,
204
+ include_prepost=include_prepost,
205
+ adjust_ohlc=adjust_ohlc,
206
+ )
207
+ if not chunk.empty:
208
+ frames.append(chunk)
209
+ if not frames:
210
+ return pd.DataFrame(columns=BAR_COLUMNS)
211
+ history = pd.concat(frames, ignore_index=True)
212
+ history = history.drop_duplicates(subset=["timestamp"], keep="last").sort_values("timestamp").reset_index(drop=True)
213
+ return history[(history["timestamp"] >= start_dt) & (history["timestamp"] <= end_dt)].reset_index(drop=True)
214
+
215
+ def _fetch_chunk_adaptive(
216
+ self,
217
+ *,
218
+ symbol: str,
219
+ interval: str,
220
+ chunk_start: datetime,
221
+ chunk_end: datetime,
222
+ min_chunk_days: int,
223
+ include_prepost: bool,
224
+ adjust_ohlc: bool,
225
+ ) -> pd.DataFrame:
226
+ try:
227
+ payload = self._request_chart(
228
+ symbol=symbol,
229
+ interval=interval,
230
+ start_dt=chunk_start,
231
+ end_dt=chunk_end,
232
+ include_prepost=include_prepost,
233
+ )
234
+ return self._payload_to_frame(payload=payload, adjust_ohlc=adjust_ohlc)
235
+ except YahooIntervalLimitError:
236
+ if max((chunk_end - chunk_start).days, 1) <= min_chunk_days:
237
+ raise
238
+ midpoint = chunk_start + (chunk_end - chunk_start) / 2
239
+ left = self._fetch_chunk_adaptive(
240
+ symbol=symbol,
241
+ interval=interval,
242
+ chunk_start=chunk_start,
243
+ chunk_end=midpoint,
244
+ min_chunk_days=min_chunk_days,
245
+ include_prepost=include_prepost,
246
+ adjust_ohlc=adjust_ohlc,
247
+ )
248
+ right = self._fetch_chunk_adaptive(
249
+ symbol=symbol,
250
+ interval=interval,
251
+ chunk_start=midpoint,
252
+ chunk_end=chunk_end,
253
+ min_chunk_days=min_chunk_days,
254
+ include_prepost=include_prepost,
255
+ adjust_ohlc=adjust_ohlc,
256
+ )
257
+ return pd.concat([left, right], ignore_index=True)
258
+
259
+ def _request_chart(
260
+ self,
261
+ *,
262
+ symbol: str,
263
+ interval: str,
264
+ start_dt: datetime,
265
+ end_dt: datetime,
266
+ include_prepost: bool,
267
+ ) -> dict[str, Any]:
268
+ params = {
269
+ "period1": str(int(start_dt.timestamp())),
270
+ "period2": str(int(end_dt.timestamp())),
271
+ "interval": interval,
272
+ "includePrePost": "true" if include_prepost else "false",
273
+ "events": "div,splits,capitalGains",
274
+ "includeAdjustedClose": "true",
275
+ }
276
+ last_error: Exception | None = None
277
+ for _ in range(len(YAHOO_CHART_HOSTS)):
278
+ base_url = next(self.host_cycle)
279
+ url = f"{base_url}/v8/finance/chart/{requests.utils.quote(symbol, safe='')}"
280
+ cache_key = self._cache_key(url=url, params=params)
281
+ cached = self.cache.get(cache_key, ttl_seconds=self._cache_ttl_seconds(end_dt=end_dt))
282
+ if cached is not None:
283
+ return cached
284
+
285
+ self.rate_limiter.wait()
286
+ response = self.session.get(url, params=params, timeout=self.timeout_seconds)
287
+ if response.status_code == 429:
288
+ last_error = YahooRateLimitError(f"Yahoo rate-limited {symbol} at interval {interval}.")
289
+ time.sleep(1.5)
290
+ continue
291
+ if response.status_code == 404:
292
+ raise YahooSymbolError(f"Yahoo did not recognize ticker {symbol}.")
293
+ if response.status_code == 422:
294
+ raise YahooIntervalLimitError(
295
+ f"Yahoo rejected {symbol} {interval} from {start_dt.isoformat()} to {end_dt.isoformat()}."
296
+ )
297
+ try:
298
+ response.raise_for_status()
299
+ except requests.HTTPError as exc:
300
+ last_error = exc
301
+ continue
302
+
303
+ payload = response.json()
304
+ error = payload.get("chart", {}).get("error")
305
+ if error:
306
+ description = error.get("description") or error.get("code") or str(error)
307
+ lowered = description.lower()
308
+ if "not found" in lowered or "no data found" in lowered or "symbol" in lowered:
309
+ raise YahooSymbolError(description)
310
+ if "range" in lowered or "interval" in lowered or "last" in lowered:
311
+ raise YahooIntervalLimitError(description)
312
+ if "rate limit" in lowered or "too many requests" in lowered:
313
+ raise YahooRateLimitError(description)
314
+ raise YahooHistoryError(description)
315
+ self.cache.set(cache_key, payload)
316
+ return payload
317
+
318
+ if last_error is not None:
319
+ raise YahooHistoryError(str(last_error)) from last_error
320
+ raise YahooHistoryError(f"Yahoo request failed for {symbol} {interval}.")
321
+
322
+ def _payload_to_frame(self, *, payload: dict[str, Any], adjust_ohlc: bool) -> pd.DataFrame:
323
+ result = payload.get("chart", {}).get("result") or []
324
+ if not result:
325
+ return pd.DataFrame(columns=BAR_COLUMNS)
326
+ result0 = result[0]
327
+ meta = result0.get("meta") or {}
328
+ timestamps = result0.get("timestamp") or []
329
+ quote_sets = result0.get("indicators", {}).get("quote") or []
330
+ if not timestamps or not quote_sets:
331
+ return pd.DataFrame(columns=BAR_COLUMNS)
332
+
333
+ try:
334
+ timezone = ZoneInfo(meta.get("exchangeTimezoneName") or "UTC")
335
+ except Exception:
336
+ timezone = ZoneInfo("UTC")
337
+ quote = quote_sets[0]
338
+ adjclose_sets = result0.get("indicators", {}).get("adjclose") or [{}]
339
+ adj_close = adjclose_sets[0].get("adjclose", []) if adjclose_sets else []
340
+ events = result0.get("events") or {}
341
+ dividends = self._event_series(events.get("dividends") or {}, value_key="amount")
342
+ splits = self._event_series(events.get("splits") or {}, value_key="splitRatio")
343
+ row_count = len(timestamps)
344
+
345
+ frame = pd.DataFrame(
346
+ {
347
+ "timestamp": pd.to_datetime(timestamps, unit="s", utc=True).tz_convert(timezone).tz_localize(None),
348
+ "open": self._normalize_values(quote.get("open", []), row_count),
349
+ "high": self._normalize_values(quote.get("high", []), row_count),
350
+ "low": self._normalize_values(quote.get("low", []), row_count),
351
+ "close": self._normalize_values(quote.get("close", []), row_count),
352
+ "adj_close": self._normalize_values(adj_close, row_count),
353
+ "volume": self._normalize_values(quote.get("volume", []), row_count),
354
+ }
355
+ )
356
+ for column in ("open", "high", "low", "close", "adj_close", "volume"):
357
+ frame[column] = pd.to_numeric(frame[column], errors="coerce")
358
+ frame = frame.dropna(subset=["timestamp", "close"]).reset_index(drop=True)
359
+ frame["volume"] = frame["volume"].fillna(0.0)
360
+
361
+ frame["epoch"] = (frame["timestamp"].astype("int64") // 1_000_000_000).astype("int64")
362
+ frame["dividend"] = frame["epoch"].map(dividends).fillna(0.0)
363
+ frame["split_ratio"] = frame["epoch"].map(splits).fillna(1.0)
364
+ frame = frame.drop(columns=["epoch"])
365
+
366
+ if adjust_ohlc:
367
+ ratio = frame["adj_close"].where(frame["close"] != 0, frame["close"]) / frame["close"].replace(0, pd.NA)
368
+ ratio = ratio.fillna(1.0)
369
+ for column in ("open", "high", "low", "close"):
370
+ frame[column] = frame[column] * ratio
371
+ return frame.drop_duplicates(subset=["timestamp"], keep="last").sort_values("timestamp").reset_index(drop=True)[BAR_COLUMNS]
372
+
373
+ @staticmethod
374
+ def _event_series(events: dict[str, Any], *, value_key: str) -> dict[int, float]:
375
+ output: dict[int, float] = {}
376
+ for event in events.values():
377
+ timestamp = event.get("date")
378
+ value = event.get(value_key)
379
+ if timestamp is None or value is None:
380
+ continue
381
+ try:
382
+ output[int(timestamp)] = float(value)
383
+ except (TypeError, ValueError):
384
+ continue
385
+ return output
386
+
387
+ @staticmethod
388
+ def _normalize_values(values: list[Any] | tuple[Any, ...], size: int) -> list[Any]:
389
+ normalized = list(values[:size])
390
+ if len(normalized) < size:
391
+ normalized.extend([None] * (size - len(normalized)))
392
+ return normalized
393
+
394
+ @staticmethod
395
+ def _cache_key(*, url: str, params: dict[str, str]) -> str:
396
+ material = json.dumps({"url": url, "params": params}, sort_keys=True, separators=(",", ":"))
397
+ return hashlib.sha256(material.encode("utf-8")).hexdigest()
398
+
399
+ @staticmethod
400
+ def _cache_ttl_seconds(*, end_dt: datetime) -> int:
401
+ now_utc = datetime.utcnow()
402
+ if end_dt < now_utc - timedelta(days=2):
403
+ return 7 * 24 * 60 * 60
404
+ if end_dt < now_utc - timedelta(hours=12):
405
+ return 60 * 60
406
+ return 90
407
+
408
+ @staticmethod
409
+ def _coerce_datetime(value: str | datetime, *, end_of_day: bool) -> datetime:
410
+ timestamp = pd.Timestamp(value)
411
+ if timestamp.tzinfo is not None:
412
+ timestamp = timestamp.tz_convert("UTC").tz_localize(None)
413
+ dt = timestamp.to_pydatetime()
414
+ if end_of_day and dt.hour == 0 and dt.minute == 0 and dt.second == 0 and dt.microsecond == 0:
415
+ return dt + timedelta(days=1)
416
+ return dt
417
+
418
+ @staticmethod
419
+ def _iter_chunks(*, start_dt: datetime, end_dt: datetime, chunk_days: int) -> list[tuple[datetime, datetime]]:
420
+ chunks: list[tuple[datetime, datetime]] = []
421
+ cursor = start_dt
422
+ while cursor < end_dt:
423
+ next_edge = min(cursor + timedelta(days=chunk_days), end_dt)
424
+ chunks.append((cursor, next_edge))
425
+ cursor = next_edge
426
+ return chunks
427
+
428
+ @staticmethod
429
+ def _interval_policy(interval: str) -> IntervalPolicy:
430
+ normalized = interval.strip()
431
+ if normalized not in INTERVAL_POLICIES:
432
+ allowed = ", ".join(sorted(INTERVAL_POLICIES))
433
+ raise ValueError(f"Unsupported interval {interval!r}. Allowed values: {allowed}")
434
+ return INTERVAL_POLICIES[normalized]
435
+
436
+ @staticmethod
437
+ def _validate_retention_window(*, policy: IntervalPolicy, start_dt: datetime, end_dt: datetime) -> None:
438
+ if policy.retention_days is None:
439
+ return
440
+ earliest = datetime.utcnow() - timedelta(days=policy.retention_days)
441
+ if start_dt < earliest or end_dt < earliest:
442
+ cutoff = earliest.strftime("%Y-%m-%d")
443
+ raise YahooIntervalLimitError(
444
+ f"Yahoo only serves {policy.interval} history back to about {cutoff}. Use 1d or coarser for deeper history."
445
+ )
requirements.txt CHANGED
@@ -1,7 +1,7 @@
1
  pandas
2
  pandas_market_calendars
3
  pyarrow
4
- yfinance
5
  fastapi
6
  uvicorn
7
  joblib
 
1
  pandas
2
  pandas_market_calendars
3
  pyarrow
4
+ requests
5
  fastapi
6
  uvicorn
7
  joblib
scripts/__pycache__/run_ist_scheduler.cpython-311.pyc CHANGED
Binary files a/scripts/__pycache__/run_ist_scheduler.cpython-311.pyc and b/scripts/__pycache__/run_ist_scheduler.cpython-311.pyc differ
 
scripts/run_ist_scheduler.py CHANGED
@@ -9,12 +9,14 @@ from zoneinfo import ZoneInfo
9
  sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
10
  from nifty_backend.runtime import (
11
  CLOSE_REFRESH_READY,
 
12
  is_trading_day,
13
  latest_saved_prediction,
14
  close_refresh_due,
15
  refresh_market_close_data,
16
  refresh_daily_data,
17
  refresh_first5_prediction,
 
18
  seconds_until_next_ist_run,
19
  )
20
 
@@ -50,6 +52,12 @@ def refresh_close_data_if_due() -> None:
50
  print(f"[scheduler] close data refreshed: {info}")
51
 
52
 
 
 
 
 
 
 
53
  def main() -> None:
54
  print("[scheduler] NIFTY first-five-minute scheduler started.")
55
  print("[scheduler] Runs the opening prediction after 09:20 IST so the 09:15-09:19 candles are complete.")
@@ -57,12 +65,14 @@ def main() -> None:
57
  try:
58
  refresh_if_current_session_is_ready()
59
  refresh_close_data_if_due()
 
60
  except Exception as exc:
61
  print(f"[scheduler] current-session refresh failed: {exc}")
62
- sleep_for = seconds_until_next_ist_run()
63
- target = datetime.now(IST).timestamp() + sleep_for
64
- print(f"[scheduler] sleeping {sleep_for / 60:.1f} minutes; next wake timestamp={target:.0f}")
65
- time.sleep(sleep_for)
 
66
  try:
67
  prediction = refresh_first5_prediction()
68
  print(f"[scheduler] first5 prediction refreshed: {prediction.to_dict()}")
@@ -73,9 +83,11 @@ def main() -> None:
73
  print(f"[scheduler] daily data refreshed: {info}")
74
  except Exception as exc:
75
  print(f"[scheduler] daily refresh failed: {exc}")
76
- sleep_for = seconds_until_next_ist_run(CLOSE_REFRESH_READY)
77
- print(f"[scheduler] sleeping {sleep_for / 60:.1f} minutes until close refresh.")
78
- time.sleep(sleep_for)
 
 
79
  try:
80
  refresh_close_data_if_due()
81
  except Exception as exc:
 
9
  sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
10
  from nifty_backend.runtime import (
11
  CLOSE_REFRESH_READY,
12
+ STALE_CHECK_INTERVAL_SECONDS,
13
  is_trading_day,
14
  latest_saved_prediction,
15
  close_refresh_due,
16
  refresh_market_close_data,
17
  refresh_daily_data,
18
  refresh_first5_prediction,
19
+ refresh_stale_data_once,
20
  seconds_until_next_ist_run,
21
  )
22
 
 
52
  print(f"[scheduler] close data refreshed: {info}")
53
 
54
 
55
+ def refresh_stale_data_if_due() -> None:
56
+ info = refresh_stale_data_once()
57
+ if info.get("status") == "refreshed":
58
+ print(f"[scheduler] stale data refreshed: {info}")
59
+
60
+
61
  def main() -> None:
62
  print("[scheduler] NIFTY first-five-minute scheduler started.")
63
  print("[scheduler] Runs the opening prediction after 09:20 IST so the 09:15-09:19 candles are complete.")
 
65
  try:
66
  refresh_if_current_session_is_ready()
67
  refresh_close_data_if_due()
68
+ refresh_stale_data_if_due()
69
  except Exception as exc:
70
  print(f"[scheduler] current-session refresh failed: {exc}")
71
+ next_first5 = seconds_until_next_ist_run()
72
+ if next_first5 > STALE_CHECK_INTERVAL_SECONDS:
73
+ time.sleep(STALE_CHECK_INTERVAL_SECONDS)
74
+ continue
75
+ time.sleep(next_first5)
76
  try:
77
  prediction = refresh_first5_prediction()
78
  print(f"[scheduler] first5 prediction refreshed: {prediction.to_dict()}")
 
83
  print(f"[scheduler] daily data refreshed: {info}")
84
  except Exception as exc:
85
  print(f"[scheduler] daily refresh failed: {exc}")
86
+ next_close = seconds_until_next_ist_run(CLOSE_REFRESH_READY)
87
+ if next_close > STALE_CHECK_INTERVAL_SECONDS:
88
+ time.sleep(STALE_CHECK_INTERVAL_SECONDS)
89
+ continue
90
+ time.sleep(next_close)
91
  try:
92
  refresh_close_data_if_due()
93
  except Exception as exc: