Upload 39 files
Browse files- __pycache__/__init__.cpython-311.pyc +0 -0
- __pycache__/app.cpython-311.pyc +0 -0
- app.py +201 -91
- data/nifty50_1d.parquet +2 -2
- data/nifty50_1m.parquet +2 -2
- data/opening_direction_training_dataset.parquet +2 -2
- models/latest_prediction.csv +2 -2
- models/tomorrow_latest_prediction.csv +1 -1
- models/tomorrow_summary.json +5 -5
- models/tplus1_latest_prediction.csv +1 -1
- models/yahoo_history_cache.sqlite3 +0 -0
- nifty_backend/__pycache__/runtime.cpython-311.pyc +0 -0
- nifty_backend/__pycache__/yahoo_history_client.cpython-311.pyc +0 -0
- nifty_backend/runtime.py +231 -47
- nifty_backend/yahoo_history_client.py +445 -0
- requirements.txt +1 -1
- scripts/__pycache__/run_ist_scheduler.cpython-311.pyc +0 -0
- scripts/run_ist_scheduler.py +19 -7
__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 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 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 |
-
|
| 46 |
-
|
| 47 |
-
|
|
|
|
| 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 |
-
|
| 82 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
"
|
| 124 |
-
"
|
| 125 |
-
"
|
| 126 |
-
"
|
| 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 |
-
|
| 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
|
| 169 |
-
"reason": None if t5_available else state["
|
| 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
|
| 182 |
-
"reason": None if tplus1_available else state["
|
| 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(
|
| 231 |
-
print("[scheduler]
|
| 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
|
| 270 |
-
try:
|
| 271 |
-
await asyncio.to_thread(
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 297 |
-
asyncio.create_task(
|
| 298 |
-
asyncio.create_task(
|
|
|
|
|
|
|
| 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 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 3 |
-
size
|
|
|
|
| 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:
|
| 3 |
-
size
|
|
|
|
| 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:
|
| 3 |
-
size
|
|
|
|
| 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-
|
|
|
|
| 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-
|
|
|
|
| 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-
|
| 29 |
-
"latest_forecast_for": "next trading session 2026-05-
|
| 30 |
-
"latest_forecast_prob_up": 0.
|
| 31 |
-
"latest_forecast_signal": "
|
| 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-
|
| 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-
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 295 |
-
|
| 296 |
-
return
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
def
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
|
|
|
| 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 |
-
|
| 77 |
-
|
| 78 |
-
|
|
|
|
|
|
|
| 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:
|