Jitendra12421 commited on
Commit
fed3916
·
verified ·
1 Parent(s): 60e4002

Upload 35 files

Browse files
Files changed (2) hide show
  1. app.py +347 -347
  2. nifty_backend/runtime.py +0 -0
app.py CHANGED
@@ -1,347 +1,347 @@
1
- from __future__ import annotations
2
-
3
- import asyncio
4
- import threading
5
- from datetime import date, datetime, time, timedelta
6
-
7
- import sys
8
- from pathlib import Path
9
-
10
- from fastapi import BackgroundTasks, Query
11
- 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")
34
- app.add_middleware(
35
- CORSMiddleware,
36
- allow_origins=["*"],
37
- allow_credentials=False,
38
- allow_methods=["*"],
39
- allow_headers=["*"],
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):
54
- return {"status": "skipped", "reason": "close refresh already running"}
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:
63
- try:
64
- latest = payload if payload is not None else latest_saved_prediction()
65
- raw = latest.get("input_date")
66
- return date.fromisoformat(str(raw)) if raw else None
67
- except Exception:
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()
75
- current_time = now.time()
76
- trading_day = is_trading_day(today)
77
- latest_date = latest_prediction_date()
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:
89
- status = "Market Closed"
90
- detail = f"Next trading session is {next_session.isoformat()}."
91
- elif current_time < time(9, 0):
92
- status = "Waiting for 9:00 AM"
93
- detail = "Market has not entered pre-open yet."
94
- elif current_time < MARKET_OPEN:
95
- status = "Market Pre-Open"
96
- detail = "Market opens at 9:15 AM IST."
97
- elif current_time < FIRST5_READY:
98
- status = "Market Officially Opened"
99
- detail = "Waiting for the first 5 one-minute bars."
100
- elif current_time <= MARKET_CLOSE:
101
- if market_status in {"Fetching T+5 Prediction Data...", "Prediction Failed"}:
102
- status = market_status
103
- detail = "The first-five-minute prediction job is still resolving."
104
- elif has_current_first5:
105
- status = "Prediction Ready"
106
- detail = "Today's first-five-minute prediction is available."
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(),
118
- "is_trading_day": trading_day,
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:
131
- state = current_market_state()
132
- payload.setdefault("data_status", {})
133
- payload["data_status"].update(state)
134
-
135
- t5_latest = payload.get("latest") or {}
136
- tomorrow_latest = payload.get("tomorrow_latest") or {}
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"
146
- tomorrow_reason = "Market close refresh is generating the next-session payload."
147
- else:
148
- tomorrow_status = "Ready" if tomorrow_available else "Pending"
149
- tomorrow_reason = None if tomorrow_available else "No saved next-session signal is available."
150
- payload["predictions"] = {
151
- "tomorrow": {
152
- "available": tomorrow_available,
153
- "status": tomorrow_status,
154
- "reason": tomorrow_reason,
155
- "target_date": tomorrow_latest.get("target_date") or state["next_session_date"],
156
- "input_date": tomorrow_latest.get("input_date"),
157
- "prediction": tomorrow_latest.get("prediction") if tomorrow_available else None,
158
- "prob_up": tomorrow_latest.get("prob_up") if tomorrow_available else None,
159
- "confidence": tomorrow_latest.get("confidence") if tomorrow_available else None,
160
- "threshold": tomorrow_latest.get("threshold") if tomorrow_available else None,
161
- "model_name": tomorrow_latest.get("model_name"),
162
- "source_model": tomorrow_latest.get("source_model"),
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,
175
- "model_name": t5_latest.get("model_name"),
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,
188
- "threshold": tplus1_latest.get("threshold") if tplus1_available else None,
189
- "model_name": tplus1_latest.get("model_name"),
190
- "validation_accuracy": (payload.get("tplus1_summary") or {}).get("validation_accuracy"),
191
- "test_accuracy": (payload.get("tplus1_summary") or {}).get("test_accuracy"),
192
- },
193
- }
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
201
- await asyncio.sleep(seconds_until_next_ist_run(time(9, 0)))
202
- if not is_trading_day(datetime.now(IST).date()):
203
- market_status = "Market Closed"
204
- continue
205
- market_status = "Market Pre-Open"
206
- print("[scheduler] 9:00 AM IST - Market Pre-Open", flush=True)
207
-
208
- # Wait until 9:15 AM IST
209
- await asyncio.sleep(seconds_until_next_ist_run(time(9, 15)))
210
- market_status = "Market Officially Opened"
211
- print("[scheduler] 9:15 AM IST - Market Officially Opened", flush=True)
212
-
213
- # Wait until 9:20 AM IST
214
- await asyncio.sleep(seconds_until_next_ist_run(time(9, 20)))
215
- market_status = "Fetching T+5 Prediction Data..."
216
- print("[scheduler] 9:20 AM IST - Fetching Data", flush=True)
217
-
218
- try:
219
- await asyncio.to_thread(refresh_first5_prediction)
220
- market_status = "Prediction Ready"
221
- except Exception as exc:
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:
236
- print(f"[scheduler] close refresh failed: {exc}", flush=True)
237
-
238
-
239
- async def refresh_current_session_once() -> None:
240
- global market_status
241
- now = datetime.now(IST)
242
- if not is_trading_day(now.date()) or now.time() < FIRST5_READY:
243
- return
244
- if latest_prediction_date() == now.date():
245
- return
246
- market_status = "Fetching T+5 Prediction Data..."
247
- print("[startup] Current session needs first-five refresh; fetching now.", flush=True)
248
- try:
249
- await asyncio.to_thread(refresh_first5_prediction)
250
- market_status = "Prediction Ready"
251
- except Exception as exc:
252
- print(f"[startup] first5 refresh failed: {exc}", flush=True)
253
- market_status = "Prediction Failed"
254
- try:
255
- await asyncio.to_thread(refresh_daily_data)
256
- except Exception as exc:
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()
281
- today = datetime.now(IST).date()
282
- if not is_trading_day(today):
283
- market_status = "Market Closed"
284
- elif now < time(9, 0):
285
- market_status = "Waiting for 9:00 AM"
286
- elif now < time(9, 15):
287
- market_status = "Market Pre-Open"
288
- elif now < time(9, 20):
289
- market_status = "Market Officially Opened"
290
- elif latest_prediction_date() == today:
291
- market_status = "Prediction Ready"
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")
302
- def health() -> dict[str, str]:
303
- return {"status": "ok"}
304
-
305
-
306
- @app.get("/")
307
- 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")
326
- def prediction_latest() -> dict:
327
- return latest_saved_prediction()
328
-
329
-
330
- @app.post("/prediction/refresh-first5")
331
- def prediction_refresh_first5(
332
- session_date: date | None = Query(default=None, description="Optional YYYY-MM-DD session date in IST."),
333
- ) -> dict:
334
- prediction = refresh_first5_prediction(session_date=session_date)
335
- return prediction.to_dict()
336
-
337
-
338
- @app.post("/data/refresh-daily")
339
- def data_refresh_daily() -> dict:
340
- return refresh_daily_data()
341
-
342
-
343
- @app.post("/data/refresh-market-close")
344
- def data_refresh_market_close(
345
- session_date: date | None = Query(default=None, description="Optional YYYY-MM-DD session date in IST."),
346
- ) -> dict:
347
- return refresh_market_close_data(session_date=session_date)
 
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import threading
5
+ from datetime import date, datetime, time, timedelta
6
+
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ from fastapi import BackgroundTasks, Query
11
+ 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")
34
+ app.add_middleware(
35
+ CORSMiddleware,
36
+ allow_origins=["*"],
37
+ allow_credentials=False,
38
+ allow_methods=["*"],
39
+ allow_headers=["*"],
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):
54
+ return {"status": "skipped", "reason": "close refresh already running"}
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:
63
+ try:
64
+ latest = payload if payload is not None else latest_saved_prediction()
65
+ raw = latest.get("input_date")
66
+ return date.fromisoformat(str(raw)) if raw else None
67
+ except Exception:
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()
75
+ current_time = now.time()
76
+ trading_day = is_trading_day(today)
77
+ latest_date = latest_prediction_date()
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:
89
+ status = "Market Closed"
90
+ detail = f"Next trading session is {next_session.isoformat()}."
91
+ elif current_time < time(9, 0):
92
+ status = "Waiting for 9:00 AM"
93
+ detail = "Market has not entered pre-open yet."
94
+ elif current_time < MARKET_OPEN:
95
+ status = "Market Pre-Open"
96
+ detail = "Market opens at 9:15 AM IST."
97
+ elif current_time < FIRST5_READY:
98
+ status = "Market Officially Opened"
99
+ detail = "Waiting for the first 5 one-minute bars."
100
+ elif current_time <= MARKET_CLOSE:
101
+ if market_status in {"Fetching T+5 Prediction Data...", "Prediction Failed"}:
102
+ status = market_status
103
+ detail = "The first-five-minute prediction job is still resolving."
104
+ elif has_current_first5:
105
+ status = "Prediction Ready"
106
+ detail = "Today's first-five-minute prediction is available."
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(),
118
+ "is_trading_day": trading_day,
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:
131
+ state = current_market_state()
132
+ payload.setdefault("data_status", {})
133
+ payload["data_status"].update(state)
134
+
135
+ t5_latest = payload.get("latest") or {}
136
+ tomorrow_latest = payload.get("tomorrow_latest") or {}
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"
146
+ tomorrow_reason = "Market close refresh is generating the next-session payload."
147
+ else:
148
+ tomorrow_status = "Ready" if tomorrow_available else "Pending"
149
+ tomorrow_reason = None if tomorrow_available else "No saved next-session signal is available."
150
+ payload["predictions"] = {
151
+ "tomorrow": {
152
+ "available": tomorrow_available,
153
+ "status": tomorrow_status,
154
+ "reason": tomorrow_reason,
155
+ "target_date": tomorrow_latest.get("target_date") or state["next_session_date"],
156
+ "input_date": tomorrow_latest.get("input_date"),
157
+ "prediction": tomorrow_latest.get("prediction") if tomorrow_available else None,
158
+ "prob_up": tomorrow_latest.get("prob_up") if tomorrow_available else None,
159
+ "confidence": tomorrow_latest.get("confidence") if tomorrow_available else None,
160
+ "threshold": tomorrow_latest.get("threshold") if tomorrow_available else None,
161
+ "model_name": tomorrow_latest.get("model_name"),
162
+ "source_model": tomorrow_latest.get("source_model"),
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,
175
+ "model_name": t5_latest.get("model_name"),
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,
188
+ "threshold": tplus1_latest.get("threshold") if tplus1_available else None,
189
+ "model_name": tplus1_latest.get("model_name"),
190
+ "validation_accuracy": (payload.get("tplus1_summary") or {}).get("validation_accuracy"),
191
+ "test_accuracy": (payload.get("tplus1_summary") or {}).get("test_accuracy"),
192
+ },
193
+ }
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
201
+ await asyncio.sleep(seconds_until_next_ist_run(time(9, 0)))
202
+ if not is_trading_day(datetime.now(IST).date()):
203
+ market_status = "Market Closed"
204
+ continue
205
+ market_status = "Market Pre-Open"
206
+ print("[scheduler] 9:00 AM IST - Market Pre-Open", flush=True)
207
+
208
+ # Wait until 9:15 AM IST
209
+ await asyncio.sleep(seconds_until_next_ist_run(time(9, 15)))
210
+ market_status = "Market Officially Opened"
211
+ print("[scheduler] 9:15 AM IST - Market Officially Opened", flush=True)
212
+
213
+ # Wait until 9:20 AM IST
214
+ await asyncio.sleep(seconds_until_next_ist_run(time(9, 20)))
215
+ market_status = "Fetching T+5 Prediction Data..."
216
+ print("[scheduler] 9:20 AM IST - Fetching Data", flush=True)
217
+
218
+ try:
219
+ await asyncio.to_thread(refresh_first5_prediction)
220
+ market_status = "Prediction Ready"
221
+ except Exception as exc:
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:
236
+ print(f"[scheduler] close refresh failed: {exc}", flush=True)
237
+
238
+
239
+ async def refresh_current_session_once() -> None:
240
+ global market_status
241
+ now = datetime.now(IST)
242
+ if not is_trading_day(now.date()) or now.time() < FIRST5_READY:
243
+ return
244
+ if latest_prediction_date() == now.date():
245
+ return
246
+ market_status = "Fetching T+5 Prediction Data..."
247
+ print("[startup] Current session needs first-five refresh; fetching now.", flush=True)
248
+ try:
249
+ await asyncio.to_thread(refresh_first5_prediction)
250
+ market_status = "Prediction Ready"
251
+ except Exception as exc:
252
+ print(f"[startup] first5 refresh failed: {exc}", flush=True)
253
+ market_status = "Prediction Failed"
254
+ try:
255
+ await asyncio.to_thread(refresh_daily_data)
256
+ except Exception as exc:
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()
281
+ today = datetime.now(IST).date()
282
+ if not is_trading_day(today):
283
+ market_status = "Market Closed"
284
+ elif now < time(9, 0):
285
+ market_status = "Waiting for 9:00 AM"
286
+ elif now < time(9, 15):
287
+ market_status = "Market Pre-Open"
288
+ elif now < time(9, 20):
289
+ market_status = "Market Officially Opened"
290
+ elif latest_prediction_date() == today:
291
+ market_status = "Prediction Ready"
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")
302
+ def health() -> dict[str, str]:
303
+ return {"status": "ok"}
304
+
305
+
306
+ @app.get("/")
307
+ 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")
326
+ def prediction_latest() -> dict:
327
+ return latest_saved_prediction()
328
+
329
+
330
+ @app.post("/prediction/refresh-first5")
331
+ def prediction_refresh_first5(
332
+ session_date: date | None = Query(default=None, description="Optional YYYY-MM-DD session date in IST."),
333
+ ) -> dict:
334
+ prediction = refresh_first5_prediction(session_date=session_date)
335
+ return prediction.to_dict()
336
+
337
+
338
+ @app.post("/data/refresh-daily")
339
+ def data_refresh_daily() -> dict:
340
+ return refresh_daily_data()
341
+
342
+
343
+ @app.post("/data/refresh-market-close")
344
+ def data_refresh_market_close(
345
+ session_date: date | None = Query(default=None, description="Optional YYYY-MM-DD session date in IST."),
346
+ ) -> dict:
347
+ return refresh_market_close_data(session_date=session_date)
nifty_backend/runtime.py CHANGED
The diff for this file is too large to render. See raw diff