Jitendra12421 commited on
Commit
2744cfd
·
verified ·
1 Parent(s): e3b9d7f

Delete backend

Browse files
backend/.dockerignore DELETED
@@ -1,5 +0,0 @@
1
- __pycache__/
2
- *.pyc
3
- .venv/
4
- venv/
5
- .env
 
 
 
 
 
 
backend/Dockerfile DELETED
@@ -1,15 +0,0 @@
1
- FROM python:3.11-slim
2
-
3
- ENV PYTHONDONTWRITEBYTECODE=1
4
- ENV PYTHONUNBUFFERED=1
5
-
6
- WORKDIR /app
7
-
8
- COPY requirements.txt .
9
- RUN pip install --no-cache-dir -r requirements.txt
10
-
11
- COPY . .
12
-
13
- EXPOSE 7860
14
-
15
- CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--ws", "none"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/README.md DELETED
@@ -1,36 +0,0 @@
1
- ---
2
- title: NIFTY 50 Forecaster Backend
3
- emoji: 📈
4
- colorFrom: green
5
- colorTo: blue
6
- sdk: docker
7
- app_port: 7860
8
- ---
9
-
10
- # NIFTY 50 Forecaster Backend
11
-
12
- FastAPI Hugging Face Docker Space for the NIFTY 50 first-five-minute direction forecaster.
13
-
14
- ## Endpoints
15
-
16
- - `GET /health`
17
- - `GET /dashboard`
18
- - `GET /prediction/latest`
19
- - `POST /prediction/refresh-first5`
20
- - `POST /data/refresh-daily`
21
- - `GET /cron/keepalive`
22
-
23
- ## Data
24
-
25
- Parquet files live in `data/`:
26
-
27
- - `nifty50_1m.parquet`
28
- - `nifty50_1d.parquet`
29
- - `opening_direction_training_dataset.parquet`
30
- - `test_predictions.parquet`
31
-
32
- ## Runtime
33
-
34
- The API starts a daily background refresh loop. It wakes after `09:20 Asia/Kolkata`, fetches Yahoo Finance `^NSEI` 1-minute candles for the `09:15-09:19` opening window, appends them to Parquet, and writes the latest prediction.
35
-
36
- Netlify also pings `/cron/keepalive` every 10 minutes through its scheduled function.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/__init__.py DELETED
@@ -1,2 +0,0 @@
1
- """NIFTY Project backend package."""
2
-
 
 
 
backend/app.py DELETED
@@ -1,79 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import asyncio
4
- from datetime import date
5
-
6
- import sys
7
- from pathlib import Path
8
-
9
- from fastapi import FastAPI, Query
10
- from fastapi.middleware.cors import CORSMiddleware
11
-
12
- sys.path.insert(0, str(Path(__file__).resolve().parent))
13
- from nifty_backend.runtime import dashboard_payload, latest_saved_prediction, refresh_daily_data, refresh_first5_prediction, seconds_until_next_ist_run
14
-
15
-
16
- app = FastAPI(title="NIFTY 50 Forecaster Backend")
17
- app.add_middleware(
18
- CORSMiddleware,
19
- allow_origins=["*"],
20
- allow_credentials=False,
21
- allow_methods=["*"],
22
- allow_headers=["*"],
23
- )
24
-
25
-
26
- async def daily_ist_refresh_loop() -> None:
27
- while True:
28
- await asyncio.sleep(seconds_until_next_ist_run())
29
- try:
30
- await asyncio.to_thread(refresh_first5_prediction)
31
- except Exception as exc:
32
- print(f"[scheduler] first5 refresh failed: {exc}", flush=True)
33
- try:
34
- await asyncio.to_thread(refresh_daily_data)
35
- except Exception as exc:
36
- print(f"[scheduler] daily refresh failed: {exc}", flush=True)
37
-
38
-
39
- @app.on_event("startup")
40
- async def start_scheduler() -> None:
41
- asyncio.create_task(daily_ist_refresh_loop())
42
-
43
-
44
- @app.get("/health")
45
- def health() -> dict[str, str]:
46
- return {"status": "ok"}
47
-
48
-
49
- @app.get("/")
50
- def root() -> dict[str, str]:
51
- return {"service": "NIFTY 50 Forecaster Backend", "status": "ok"}
52
-
53
-
54
- @app.get("/dashboard")
55
- def dashboard() -> dict:
56
- return dashboard_payload()
57
-
58
-
59
- @app.get("/cron/keepalive")
60
- def cron_keepalive() -> dict:
61
- return {"status": "awake", "latest": latest_saved_prediction()}
62
-
63
-
64
- @app.get("/prediction/latest")
65
- def prediction_latest() -> dict:
66
- return latest_saved_prediction()
67
-
68
-
69
- @app.post("/prediction/refresh-first5")
70
- def prediction_refresh_first5(
71
- session_date: date | None = Query(default=None, description="Optional YYYY-MM-DD session date in IST."),
72
- ) -> dict:
73
- prediction = refresh_first5_prediction(session_date=session_date)
74
- return prediction.to_dict()
75
-
76
-
77
- @app.post("/data/refresh-daily")
78
- def data_refresh_daily() -> dict:
79
- return refresh_daily_data()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/data/nifty50_1d.parquet DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:cae5a22e18a378933d815b7b22bb413eeb2e196cdb48504fe8136a4549f7b8a7
3
- size 78101
 
 
 
 
backend/data/nifty50_1m.parquet DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:eff0ea13a459412466def2b530b482e84e06346ee4f7203519689baf0adce32c
3
- size 18555782
 
 
 
 
backend/data/opening_direction_training_dataset.parquet DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:c21f3c5e7781b67e9849a6d07c9ec28d3f087b74be7e2472785124ceb35a34f4
3
- size 4462258
 
 
 
 
backend/data/test_predictions.parquet DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:f159a7499394b7882262ffaa6f9b48c0f6ab763d024f373ee19109b301af90c1
3
- size 14499
 
 
 
 
backend/models/candidate_results.csv DELETED
@@ -1,14 +0,0 @@
1
- model_name,threshold,validation_accuracy,test_accuracy,validation_auc,test_auc
2
- blend_extra_trees_tight_logit_overlay,0.425,0.654320987654321,0.6524064171122995,0.6623500611995106,0.6563861499656042
3
- blend_extra_trees_tight_logit,0.425,0.6444444444444445,0.6310160427807486,0.6623500611995106,0.6563861499656042
4
- extra_trees_opening,0.514,0.6345679012345679,0.6203208556149733,0.6448470012239902,0.6575326759917449
5
- extra_trees_opening_tight,0.514,0.6296296296296297,0.6149732620320856,0.6446511627906977,0.6608576014675532
6
- soft_vote_tree_pack,0.511,0.6296296296296297,0.6042780748663101,0.6448959608323135,0.6621187800963081
7
- random_forest_opening,0.516,0.6296296296296297,0.5935828877005348,0.6448959608323134,0.6563861499656042
8
- extra_trees_opening_deep,0.514,0.6271604938271605,0.6042780748663101,0.6432558139534884,0.6671634946113276
9
- soft_vote_opening,0.556,0.6246913580246913,0.6256684491978609,0.638359853121175,0.6513414354505846
10
- gradient_boost_opening,0.512,0.6246913580246913,0.5828877005347594,0.640734394124847,0.631506535198349
11
- soft_vote_all_pack,0.503,0.6172839506172839,0.5989304812834224,0.6431089351285189,0.6598257280440265
12
- catboost_opening,0.524,0.6098765432098765,0.5882352941176471,0.6180660954712363,0.6319651456088053
13
- hist_gradient_opening,0.517,0.5925925925925926,0.5133689839572193,0.6042839657282741,0.5806007796376977
14
- logit_opening,0.386,0.582716049382716,0.5454545454545454,0.6403427172582619,0.6141939922036231
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/models/latest_prediction.csv DELETED
@@ -1,2 +0,0 @@
1
- input_date,first5_start,first5_end,prediction,prob_up,confidence,threshold,model_name
2
- 2026-05-21,2026-05-21 09:15:00,2026-05-21 09:19:00,UP,0.4379885393062092,0.5129885393062092,0.425,blend_extra_trees_tight_logit_overlay
 
 
 
backend/models/nifty_opening_direction_model.joblib DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:faa463488804279181dc6ee63ca62871d0d5817b0cbb23d34e7374d7628ced98
3
- size 16234249
 
 
 
 
backend/models/summary.json DELETED
@@ -1,29 +0,0 @@
1
- {
2
- "target": "same-day NIFTY 50 close > same-day NIFTY 50 open after first five 1-minute bars",
3
- "model_name": "blend_extra_trees_tight_logit_overlay",
4
- "threshold": 0.425,
5
- "train_rows": 2221,
6
- "valid_rows": 405,
7
- "test_rows": 187,
8
- "train_start": "2015-01-09",
9
- "train_end": "2023-12-29",
10
- "valid_start": "2024-01-01",
11
- "valid_end": "2025-08-14",
12
- "test_start": "2025-08-18",
13
- "test_end": "2026-05-21",
14
- "validation_accuracy": 0.654320987654321,
15
- "test_accuracy": 0.6524064171122995,
16
- "baseline_test_accuracy": 0.5240641711229946,
17
- "validation_auc": 0.6623500611995106,
18
- "test_auc": 0.6563861499656042,
19
- "validation_log_loss": 0.6563017134616456,
20
- "test_log_loss": 0.6607799782446233,
21
- "test_brier": 0.2339533732973711,
22
- "feature_count": 219,
23
- "latest_input_date": "2026-05-21",
24
- "latest_first5_start": "2026-05-21 09:15:00",
25
- "latest_first5_end": "2026-05-21 09:19:00",
26
- "latest_prob_up": 0.4379885393062093,
27
- "latest_prediction": "UP",
28
- "latest_confidence": 0.5129885393062092
29
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/nifty_backend/__init__.py DELETED
@@ -1,2 +0,0 @@
1
- """Backend runtime for the NIFTY 50 opening-direction forecaster."""
2
-
 
 
 
backend/nifty_backend/runtime.py DELETED
@@ -1,423 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import json
4
- import sys
5
- from dataclasses import dataclass
6
- from datetime import date, datetime, time, timedelta
7
- from pathlib import Path
8
- from typing import Any
9
- from zoneinfo import ZoneInfo
10
-
11
- import joblib
12
- import numpy as np
13
- import pandas as pd
14
- import yfinance as yf
15
-
16
-
17
- IST = ZoneInfo("Asia/Kolkata")
18
- YAHOO_NIFTY_SYMBOL = "^NSEI"
19
- BACKEND_ROOT = Path(__file__).resolve().parents[1]
20
- DATA_DIR = BACKEND_ROOT / "data"
21
- MODEL_DIR = BACKEND_ROOT / "models"
22
- OPENING_DATASET_PATH = DATA_DIR / "opening_direction_training_dataset.parquet"
23
- NIFTY_1M_PATH = DATA_DIR / "nifty50_1m.parquet"
24
- NIFTY_1D_PATH = DATA_DIR / "nifty50_1d.parquet"
25
- MODEL_PATH = MODEL_DIR / "nifty_opening_direction_model.joblib"
26
- LATEST_PATH = MODEL_DIR / "latest_prediction.csv"
27
- TEST_PREDICTIONS_PATH = DATA_DIR / "test_predictions.parquet"
28
-
29
- DECISION_OVERLAYS = [
30
- {
31
- "name": "fifth_minute_momentum_flip",
32
- "feature": "m5_ret_1m",
33
- "op": ">=",
34
- "value": 0.0005085411885759201,
35
- },
36
- {
37
- "name": "vix_stretch_flip",
38
- "feature": "india_vix_close_vs_sma_20",
39
- "op": ">=",
40
- "value": 0.24641908937959742,
41
- },
42
- ]
43
-
44
-
45
- class ProbabilityBlend:
46
- def __init__(self, models: list[Any], weights: np.ndarray):
47
- self.models = models
48
- self.weights = np.asarray(weights, dtype="float64")
49
- self.weights = self.weights / self.weights.sum()
50
-
51
- def predict_proba(self, x: pd.DataFrame) -> np.ndarray:
52
- probs = np.column_stack([predict_proba_up(model, x) for model in self.models])
53
- prob_up = probs @ self.weights
54
- return np.column_stack([1.0 - prob_up, prob_up])
55
-
56
-
57
- @dataclass(frozen=True)
58
- class Prediction:
59
- input_date: str
60
- first5_start: str
61
- first5_end: str
62
- prediction: str
63
- prob_up: float
64
- confidence: float
65
- threshold: float
66
- model_name: str
67
-
68
- def to_dict(self) -> dict[str, Any]:
69
- return {
70
- "input_date": self.input_date,
71
- "first5_start": self.first5_start,
72
- "first5_end": self.first5_end,
73
- "prediction": self.prediction,
74
- "prob_up": self.prob_up,
75
- "confidence": self.confidence,
76
- "threshold": self.threshold,
77
- "model_name": self.model_name,
78
- }
79
-
80
-
81
- def predict_proba_up(model: Any, x: pd.DataFrame) -> np.ndarray:
82
- return np.asarray(model.predict_proba(x)[:, 1], dtype="float64")
83
-
84
-
85
- def safe_div(numer: pd.Series | np.ndarray, denom: pd.Series | np.ndarray) -> pd.Series:
86
- n = pd.Series(numer, copy=False)
87
- d = pd.Series(denom, copy=False)
88
- out = pd.Series(np.nan, index=n.index, dtype="float64")
89
- mask = d.notna() & np.isfinite(d.to_numpy(dtype="float64")) & (d != 0)
90
- out.loc[mask] = n.loc[mask].to_numpy(dtype="float64") / d.loc[mask].to_numpy(dtype="float64")
91
- return out
92
-
93
-
94
- def load_model() -> dict[str, Any]:
95
- # Existing artifact was trained as a script, so its custom blend class
96
- # resolves through __main__ when unpickled.
97
- sys.modules["__main__"].ProbabilityBlend = ProbabilityBlend
98
- sys.modules["__main__"].predict_proba_up = predict_proba_up
99
- payload = joblib.load(MODEL_PATH)
100
- payload.setdefault("decision_overlays", DECISION_OVERLAYS)
101
- payload.setdefault("model_name", "nifty_opening_direction_model")
102
- return payload
103
-
104
-
105
- def overlay_mask(frame: pd.DataFrame, overlay: dict[str, object]) -> np.ndarray:
106
- feature = str(overlay["feature"])
107
- if feature not in frame.columns:
108
- return np.zeros(len(frame), dtype=bool)
109
- series = pd.to_numeric(frame[feature], errors="coerce")
110
- value = float(overlay["value"])
111
- if overlay["op"] == ">=":
112
- return (series >= value).fillna(False).to_numpy(dtype=bool)
113
- if overlay["op"] == "<=":
114
- return (series <= value).fillna(False).to_numpy(dtype=bool)
115
- raise ValueError(f"Unsupported overlay op: {overlay['op']}")
116
-
117
-
118
- def apply_decision_overlays(pred: np.ndarray, frame: pd.DataFrame, overlays: list[dict[str, object]]) -> np.ndarray:
119
- adjusted = np.asarray(pred, dtype="int64").copy()
120
- for overlay in overlays:
121
- mask = overlay_mask(frame, overlay)
122
- adjusted[mask] = 1 - adjusted[mask]
123
- return adjusted
124
-
125
-
126
- def directional_confidence(prob_up: np.ndarray, pred: np.ndarray, threshold: float) -> np.ndarray:
127
- prob_up = np.asarray(prob_up, dtype="float64")
128
- pred = np.asarray(pred, dtype="int64")
129
- base_side_prob = np.where(pred == 1, prob_up, 1.0 - prob_up)
130
- threshold_distance = np.abs(prob_up - float(threshold))
131
- return np.clip(0.50 + threshold_distance, base_side_prob, 0.99)
132
-
133
-
134
- def read_training_dataset() -> pd.DataFrame:
135
- df = pd.read_parquet(OPENING_DATASET_PATH)
136
- for col in ("date", "first5_start", "first5_end"):
137
- if col in df.columns:
138
- df[col] = pd.to_datetime(df[col], errors="coerce")
139
- return df.sort_values("date").reset_index(drop=True)
140
-
141
-
142
- def normalize_yahoo_frame(df: pd.DataFrame) -> pd.DataFrame:
143
- if df.empty:
144
- return pd.DataFrame(columns=["date", "open", "high", "low", "close", "volume"])
145
- if isinstance(df.columns, pd.MultiIndex):
146
- df.columns = [str(c[0]).lower() for c in df.columns]
147
- else:
148
- df.columns = [str(c).lower().replace(" ", "_") for c in df.columns]
149
- df = df.reset_index()
150
- date_col = next((c for c in df.columns if c.lower() in {"datetime", "date"}), df.columns[0])
151
- df["date"] = pd.to_datetime(df[date_col], errors="coerce")
152
- if df["date"].dt.tz is None:
153
- df["date"] = df["date"].dt.tz_localize("UTC").dt.tz_convert(IST)
154
- else:
155
- df["date"] = df["date"].dt.tz_convert(IST)
156
- rename = {
157
- "open": "open",
158
- "high": "high",
159
- "low": "low",
160
- "close": "close",
161
- "adj_close": "close",
162
- "volume": "volume",
163
- }
164
- out = pd.DataFrame({"date": df["date"].dt.tz_localize(None)})
165
- for src, dst in rename.items():
166
- if src in df.columns and dst not in out.columns:
167
- out[dst] = pd.to_numeric(df[src], errors="coerce")
168
- return out.dropna(subset=["date", "open", "high", "low", "close"]).sort_values("date")
169
-
170
-
171
- def fetch_yahoo_minutes(period: str = "5d") -> pd.DataFrame:
172
- raw = yf.download(YAHOO_NIFTY_SYMBOL, period=period, interval="1m", progress=False, prepost=False, auto_adjust=False)
173
- return normalize_yahoo_frame(raw)
174
-
175
-
176
- def fetch_yahoo_daily(period: str = "1mo") -> pd.DataFrame:
177
- raw = yf.download(YAHOO_NIFTY_SYMBOL, period=period, interval="1d", progress=False, prepost=False, auto_adjust=False)
178
- out = normalize_yahoo_frame(raw)
179
- out["date"] = pd.to_datetime(out["date"], errors="coerce").dt.normalize()
180
- return out.drop_duplicates("date", keep="last")
181
-
182
-
183
- def append_parquet_rows(path: Path, new_rows: pd.DataFrame, subset: list[str]) -> pd.DataFrame:
184
- if path.exists():
185
- existing = pd.read_parquet(path)
186
- combined = pd.concat([existing, new_rows], ignore_index=True)
187
- else:
188
- combined = new_rows.copy()
189
- combined = combined.drop_duplicates(subset=subset, keep="last").sort_values(subset).reset_index(drop=True)
190
- combined.to_parquet(path, index=False, compression="zstd")
191
- return combined
192
-
193
-
194
- def first5_features_from_minutes(minutes: pd.DataFrame, session_date: date | None = None) -> pd.DataFrame:
195
- if minutes.empty:
196
- raise RuntimeError("Yahoo returned no minute bars.")
197
- bars = minutes.copy()
198
- bars["dt"] = pd.to_datetime(bars["date"], errors="coerce")
199
- bars["session_date"] = bars["dt"].dt.normalize()
200
- if session_date is None:
201
- session_ts = bars["session_date"].max()
202
- else:
203
- session_ts = pd.Timestamp(session_date).normalize()
204
- day = bars[bars["session_date"] == session_ts].sort_values("dt").copy()
205
- start_dt = pd.Timestamp.combine(session_ts.date(), time(9, 15))
206
- end_dt = pd.Timestamp.combine(session_ts.date(), time(9, 19))
207
- first5 = day[(day["dt"] >= start_dt) & (day["dt"] <= end_dt)].head(5).copy()
208
- if len(first5) < 5:
209
- raise RuntimeError(f"Need 5 opening bars for {session_ts.date()}, got {len(first5)}.")
210
- first5["minute_index"] = np.arange(len(first5))
211
- first5["ret_1m"] = first5["close"].pct_change(fill_method=None)
212
- first5["range_pct_1m"] = safe_div(first5["high"] - first5["low"], first5["open"])
213
- first5["body_pct_1m"] = safe_div(first5["close"] - first5["open"], first5["open"])
214
- row = {
215
- "date": session_ts,
216
- "first5_start": first5["dt"].iloc[0],
217
- "first5_end": first5["dt"].iloc[-1],
218
- "first5_open": first5["open"].iloc[0],
219
- "first5_high": first5["high"].max(),
220
- "first5_low": first5["low"].min(),
221
- "first5_close": first5["close"].iloc[-1],
222
- "first5_volume": first5["volume"].sum() if "volume" in first5 else 0.0,
223
- "first5_bars": len(first5),
224
- "first5_last_1m_ret": first5["ret_1m"].iloc[-1],
225
- "first5_ret_std": first5["ret_1m"].std(),
226
- }
227
- row["first5_return"] = (row["first5_close"] - row["first5_open"]) / row["first5_open"]
228
- row["first5_range_pct"] = (row["first5_high"] - row["first5_low"]) / row["first5_open"]
229
- first5_range = row["first5_high"] - row["first5_low"]
230
- row["first5_body_to_range"] = (row["first5_close"] - row["first5_open"]) / first5_range if first5_range else np.nan
231
- row["first5_close_location"] = (row["first5_close"] - row["first5_low"]) / first5_range if first5_range else np.nan
232
- for idx, (_, candle) in enumerate(first5.iterrows(), start=1):
233
- for field in ("open", "high", "low", "close", "ret_1m", "range_pct_1m", "body_pct_1m"):
234
- row[f"m{idx}_{field}"] = candle[field]
235
- row[f"m{idx}_close_vs_first5_open"] = (candle["close"] - row["first5_open"]) / row["first5_open"]
236
- row[f"m{idx}_range_share"] = (candle["high"] - candle["low"]) / first5_range if first5_range else np.nan
237
- row["first5_return_accel"] = row["m5_ret_1m"] - row["m2_ret_1m"]
238
- row["first5_last2_return"] = (row["m5_close"] - row["m4_open"]) / row["m4_open"]
239
- row["first5_first2_return"] = (row["m2_close"] - row["m1_open"]) / row["m1_open"]
240
- row["first5_reversal"] = np.sign(row["first5_first2_return"]) * -np.sign(row["first5_last2_return"])
241
- row["dow"] = session_ts.dayofweek
242
- row["dom"] = session_ts.day
243
- row["month"] = session_ts.month
244
- return pd.DataFrame([row])
245
-
246
-
247
- def build_model_row(first5_row: pd.DataFrame) -> pd.DataFrame:
248
- dataset = read_training_dataset()
249
- latest_context = dataset.iloc[[-1]].copy()
250
- output = latest_context.copy()
251
- for col in first5_row.columns:
252
- output[col] = first5_row[col].iloc[0]
253
- if {"first5_open", "nifty_close"}.issubset(output.columns):
254
- output["first5_gap_from_prev_close"] = (output["first5_open"] - output["nifty_close"]) / output["nifty_close"]
255
- output["first5_close_vs_prev_close"] = (output["first5_close"] - output["nifty_close"]) / output["nifty_close"]
256
- if {"first5_range_pct", "nifty_range_pct"}.issubset(output.columns):
257
- output["first5_range_vs_prev_range"] = output["first5_range_pct"] / output["nifty_range_pct"]
258
- if {"first5_return", "nifty_ret_1"}.issubset(output.columns):
259
- output["first5_return_x_prev_ret"] = output["first5_return"] * output["nifty_ret_1"]
260
- output["gap_x_prev_ret"] = output["first5_gap_from_prev_close"] * output["nifty_ret_1"]
261
- if {"first5_return", "banknifty_ret_1"}.issubset(output.columns):
262
- output["first5_return_x_bank_ret_1"] = output["first5_return"] * output["banknifty_ret_1"]
263
- if {"first5_range_pct", "india_vix_ret_1"}.issubset(output.columns):
264
- output["first5_range_x_vix_ret_1"] = output["first5_range_pct"] * output["india_vix_ret_1"]
265
- output["target"] = np.nan
266
- output["day_return"] = np.nan
267
- return output
268
-
269
-
270
- def predict_row(row: pd.DataFrame) -> Prediction:
271
- payload = load_model()
272
- model = payload["model"]
273
- features = payload["features"]
274
- threshold = float(payload["threshold"])
275
- missing = [c for c in features if c not in row.columns]
276
- if missing:
277
- raise RuntimeError(f"Feature row is missing {len(missing)} features; first missing: {missing[:5]}")
278
- prob_up = predict_proba_up(model, row[features])
279
- raw_pred = (prob_up >= threshold).astype("int64")
280
- pred = apply_decision_overlays(raw_pred, row, payload.get("decision_overlays", DECISION_OVERLAYS))
281
- confidence = directional_confidence(prob_up, pred, threshold)
282
- prediction = Prediction(
283
- input_date=pd.to_datetime(row["date"].iloc[0]).date().isoformat(),
284
- first5_start=str(pd.to_datetime(row["first5_start"].iloc[0])),
285
- first5_end=str(pd.to_datetime(row["first5_end"].iloc[0])),
286
- prediction="UP" if int(pred[0]) == 1 else "DOWN",
287
- prob_up=float(prob_up[0]),
288
- confidence=float(confidence[0]),
289
- threshold=threshold,
290
- model_name=str(payload.get("model_name", "nifty_opening_direction_model")),
291
- )
292
- pd.DataFrame([prediction.to_dict()]).to_csv(LATEST_PATH, index=False)
293
- return prediction
294
-
295
-
296
- def latest_saved_prediction() -> dict[str, Any]:
297
- if LATEST_PATH.exists():
298
- return pd.read_csv(LATEST_PATH).iloc[-1].to_dict()
299
- summary_path = MODEL_DIR / "summary.json"
300
- if summary_path.exists():
301
- return json.loads(summary_path.read_text(encoding="utf-8"))
302
- raise FileNotFoundError("No latest prediction is available yet.")
303
-
304
-
305
- def _json_ready_frame(df: pd.DataFrame, limit: int | None = None) -> list[dict[str, Any]]:
306
- out = df.copy()
307
- if limit is not None:
308
- out = out.tail(limit)
309
- for col in out.columns:
310
- if pd.api.types.is_datetime64_any_dtype(out[col]):
311
- out[col] = out[col].dt.strftime("%Y-%m-%d %H:%M:%S")
312
- out = out.replace({np.nan: None})
313
- return out.to_dict(orient="records")
314
-
315
-
316
- def load_model_summary() -> dict[str, Any]:
317
- summary_path = MODEL_DIR / "summary.json"
318
- if not summary_path.exists():
319
- return {}
320
- return json.loads(summary_path.read_text(encoding="utf-8"))
321
-
322
-
323
- def load_candidate_results() -> list[dict[str, Any]]:
324
- path = MODEL_DIR / "candidate_results.csv"
325
- if not path.exists():
326
- return []
327
- return _json_ready_frame(pd.read_csv(path).head(12))
328
-
329
-
330
- def load_test_predictions() -> pd.DataFrame:
331
- if not TEST_PREDICTIONS_PATH.exists():
332
- return pd.DataFrame()
333
- df = pd.read_parquet(TEST_PREDICTIONS_PATH)
334
- df["date"] = pd.to_datetime(df["date"], errors="coerce")
335
- return df.sort_values("date").reset_index(drop=True)
336
-
337
-
338
- def dashboard_payload() -> dict[str, Any]:
339
- summary = load_model_summary()
340
- latest = latest_saved_prediction()
341
- test = load_test_predictions()
342
- daily = pd.read_parquet(NIFTY_1D_PATH)
343
- daily["date"] = pd.to_datetime(daily["date"], errors="coerce")
344
- daily = daily.sort_values("date").tail(180)
345
- dataset = read_training_dataset()
346
- opening = dataset[["date", "first5_return", "first5_range_pct", "first5_close_location"]].tail(120).copy()
347
-
348
- if not test.empty:
349
- recent_predictions = test.tail(40).copy()
350
- recent_accuracy = float(recent_predictions["correct"].mean())
351
- direction_mix = test.groupby("prediction")["correct"].agg(["count", "mean"]).reset_index()
352
- monthly = (
353
- test.assign(month=test["date"].dt.strftime("%Y-%m"))
354
- .groupby("month", as_index=False)["correct"]
355
- .mean()
356
- .rename(columns={"correct": "accuracy"})
357
- )
358
- else:
359
- recent_predictions = pd.DataFrame()
360
- recent_accuracy = None
361
- direction_mix = pd.DataFrame()
362
- monthly = pd.DataFrame()
363
-
364
- metrics = {
365
- "validation_accuracy": summary.get("validation_accuracy"),
366
- "test_accuracy": summary.get("test_accuracy"),
367
- "baseline_test_accuracy": summary.get("baseline_test_accuracy"),
368
- "validation_auc": summary.get("validation_auc"),
369
- "test_auc": summary.get("test_auc"),
370
- "test_brier": summary.get("test_brier"),
371
- "feature_count": summary.get("feature_count"),
372
- "recent_accuracy": recent_accuracy,
373
- }
374
- return {
375
- "latest": latest,
376
- "metrics": metrics,
377
- "summary": summary,
378
- "candidates": load_candidate_results(),
379
- "charts": {
380
- "daily_close": _json_ready_frame(daily[["date", "open", "high", "low", "close"]]),
381
- "opening_features": _json_ready_frame(opening),
382
- "monthly_accuracy": _json_ready_frame(monthly),
383
- "direction_mix": _json_ready_frame(direction_mix),
384
- "recent_predictions": _json_ready_frame(recent_predictions),
385
- },
386
- "data_status": {
387
- "nifty_1m_rows": int(len(pd.read_parquet(NIFTY_1M_PATH, columns=["date"]))),
388
- "nifty_1d_rows": int(len(pd.read_parquet(NIFTY_1D_PATH, columns=["date"]))),
389
- "training_rows": int(len(dataset)),
390
- "test_prediction_rows": int(len(test)),
391
- "latest_daily_date": pd.to_datetime(daily["date"]).max().date().isoformat(),
392
- },
393
- }
394
-
395
-
396
- def refresh_first5_prediction(session_date: date | None = None) -> Prediction:
397
- minutes = fetch_yahoo_minutes(period="5d")
398
- append_parquet_rows(NIFTY_1M_PATH, minutes, ["date"])
399
- first5 = first5_features_from_minutes(minutes, session_date=session_date)
400
- row = build_model_row(first5)
401
- dataset = read_training_dataset()
402
- merged = pd.concat([dataset, row], ignore_index=True)
403
- merged = merged.drop_duplicates(subset=["date"], keep="last").sort_values("date").reset_index(drop=True)
404
- merged.to_parquet(OPENING_DATASET_PATH, index=False, compression="zstd")
405
- return predict_row(row)
406
-
407
-
408
- def refresh_daily_data() -> dict[str, Any]:
409
- daily = fetch_yahoo_daily(period="1mo")
410
- combined = append_parquet_rows(NIFTY_1D_PATH, daily, ["date"])
411
- return {
412
- "rows": int(len(combined)),
413
- "latest_date": pd.to_datetime(combined["date"]).max().date().isoformat(),
414
- "path": str(NIFTY_1D_PATH),
415
- }
416
-
417
-
418
- def seconds_until_next_ist_run(run_time: time = time(9, 20)) -> float:
419
- now = datetime.now(IST)
420
- target = datetime.combine(now.date(), run_time, tzinfo=IST)
421
- if now >= target:
422
- target += timedelta(days=1)
423
- return max(1.0, (target - now).total_seconds())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/requirements.txt DELETED
@@ -1,9 +0,0 @@
1
- pandas
2
- pyarrow
3
- yfinance
4
- fastapi
5
- uvicorn
6
- joblib
7
- numpy
8
- scikit-learn
9
- catboost
 
 
 
 
 
 
 
 
 
 
backend/scripts/refresh_daily_data.py DELETED
@@ -1,15 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import sys
4
- from pathlib import Path
5
-
6
- sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
7
- from nifty_backend.runtime import refresh_daily_data
8
-
9
-
10
- def main() -> None:
11
- print(refresh_daily_data())
12
-
13
-
14
- if __name__ == "__main__":
15
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/scripts/refresh_first5_prediction.py DELETED
@@ -1,26 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import argparse
4
- import sys
5
- from datetime import date
6
- from pathlib import Path
7
-
8
- sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
9
- from nifty_backend.runtime import refresh_first5_prediction
10
-
11
-
12
- def parse_args() -> argparse.Namespace:
13
- parser = argparse.ArgumentParser(description="Fetch Yahoo Finance first five NIFTY minutes and refresh prediction.")
14
- parser.add_argument("--date", default=None, help="Optional IST session date, YYYY-MM-DD.")
15
- return parser.parse_args()
16
-
17
-
18
- def main() -> None:
19
- args = parse_args()
20
- session_date = date.fromisoformat(args.date) if args.date else None
21
- prediction = refresh_first5_prediction(session_date=session_date)
22
- print(prediction.to_dict())
23
-
24
-
25
- if __name__ == "__main__":
26
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/scripts/retrain_opening_model.py DELETED
@@ -1,182 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import argparse
4
- import json
5
- import sys
6
- from dataclasses import asdict, dataclass
7
- from pathlib import Path
8
-
9
- sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
10
-
11
- import joblib
12
- import numpy as np
13
- import pandas as pd
14
- from sklearn.ensemble import ExtraTreesClassifier
15
- from sklearn.impute import SimpleImputer
16
- from sklearn.linear_model import LogisticRegression
17
- from sklearn.metrics import accuracy_score, brier_score_loss, log_loss, roc_auc_score
18
- from sklearn.pipeline import make_pipeline
19
- from sklearn.preprocessing import StandardScaler
20
-
21
- from nifty_backend.runtime import (
22
- DECISION_OVERLAYS,
23
- MODEL_DIR,
24
- MODEL_PATH,
25
- OPENING_DATASET_PATH,
26
- ProbabilityBlend,
27
- apply_decision_overlays,
28
- directional_confidence,
29
- predict_proba_up,
30
- )
31
-
32
-
33
- DEFAULT_TRAIN_END = pd.Timestamp("2023-12-31")
34
- DEFAULT_VALID_END = pd.Timestamp("2025-08-17")
35
- RANDOM_SEED = 42
36
-
37
-
38
- @dataclass
39
- class RetrainSummary:
40
- model_name: str
41
- threshold: float
42
- train_rows: int
43
- valid_rows: int
44
- test_rows: int
45
- validation_accuracy: float
46
- test_accuracy: float
47
- validation_auc: float
48
- test_auc: float
49
- test_brier: float
50
- latest_prediction: str
51
- latest_prob_up: float
52
- latest_confidence: float
53
- feature_count: int
54
-
55
-
56
- def feature_columns(frame: pd.DataFrame) -> list[str]:
57
- excluded = {
58
- "date",
59
- "first5_start",
60
- "first5_end",
61
- "target",
62
- "day_open",
63
- "day_high",
64
- "day_low",
65
- "day_close",
66
- "day_volume",
67
- "day_return",
68
- }
69
- cols = []
70
- for col in frame.columns:
71
- if col in excluded:
72
- continue
73
- if pd.api.types.is_numeric_dtype(frame[col]) and frame[col].notna().mean() >= 0.40:
74
- if frame[col].nunique(dropna=True) > 1:
75
- cols.append(col)
76
- return cols
77
-
78
-
79
- def best_threshold(y_true: np.ndarray, prob_up: np.ndarray) -> tuple[float, float]:
80
- thresholds = np.linspace(0.35, 0.65, 301)
81
- scores = ((prob_up[:, None] >= thresholds[None, :]) == y_true[:, None]).mean(axis=0)
82
- idx = int(np.argmax(scores))
83
- return float(thresholds[idx]), float(scores[idx])
84
-
85
-
86
- def score_auc(y_true: np.ndarray, prob_up: np.ndarray) -> float:
87
- if len(np.unique(y_true)) < 2:
88
- return float("nan")
89
- return float(roc_auc_score(y_true, prob_up))
90
-
91
-
92
- def parse_args() -> argparse.Namespace:
93
- parser = argparse.ArgumentParser(description="Retrain the compact NIFTY opening-direction model from Parquet data.")
94
- parser.add_argument("--train-end", default=DEFAULT_TRAIN_END.date().isoformat())
95
- parser.add_argument("--valid-end", default=DEFAULT_VALID_END.date().isoformat())
96
- return parser.parse_args()
97
-
98
-
99
- def main() -> None:
100
- args = parse_args()
101
- train_end = pd.Timestamp(args.train_end)
102
- valid_end = pd.Timestamp(args.valid_end)
103
- frame = pd.read_parquet(OPENING_DATASET_PATH)
104
- frame["date"] = pd.to_datetime(frame["date"], errors="coerce")
105
- model_frame = frame.dropna(subset=["target"]).sort_values("date").reset_index(drop=True)
106
- features = feature_columns(model_frame)
107
- train_df = model_frame[model_frame["date"] <= train_end]
108
- valid_df = model_frame[(model_frame["date"] > train_end) & (model_frame["date"] <= valid_end)]
109
- test_df = model_frame[model_frame["date"] > valid_end]
110
- if train_df.empty or valid_df.empty or test_df.empty:
111
- raise RuntimeError("Training, validation, and test windows must all contain rows.")
112
-
113
- x_train = train_df[features]
114
- y_train = train_df["target"].to_numpy(dtype="int64")
115
- x_valid = valid_df[features]
116
- y_valid = valid_df["target"].to_numpy(dtype="int64")
117
- x_test = test_df[features]
118
- y_test = test_df["target"].to_numpy(dtype="int64")
119
-
120
- extra_trees = make_pipeline(
121
- SimpleImputer(strategy="median"),
122
- ExtraTreesClassifier(
123
- n_estimators=800,
124
- max_depth=4,
125
- min_samples_leaf=28,
126
- max_features=0.60,
127
- class_weight="balanced_subsample",
128
- random_state=RANDOM_SEED + 13,
129
- n_jobs=-1,
130
- ),
131
- )
132
- logit = make_pipeline(
133
- SimpleImputer(strategy="median"),
134
- StandardScaler(),
135
- LogisticRegression(C=0.25, class_weight="balanced", max_iter=2000, random_state=RANDOM_SEED),
136
- )
137
- extra_trees.fit(x_train, y_train)
138
- logit.fit(x_train, y_train)
139
- model = ProbabilityBlend([extra_trees, logit], np.array([0.75, 0.25]))
140
- valid_prob = predict_proba_up(model, x_valid)
141
- test_prob = predict_proba_up(model, x_test)
142
- threshold, _ = best_threshold(y_valid, valid_prob)
143
- valid_pred = apply_decision_overlays((valid_prob >= threshold).astype("int64"), valid_df, DECISION_OVERLAYS)
144
- test_pred = apply_decision_overlays((test_prob >= threshold).astype("int64"), test_df, DECISION_OVERLAYS)
145
- latest = frame.iloc[[-1]].copy()
146
- latest_prob = predict_proba_up(model, latest[features])
147
- latest_pred = apply_decision_overlays((latest_prob >= threshold).astype("int64"), latest, DECISION_OVERLAYS)
148
- latest_conf = directional_confidence(latest_prob, latest_pred, threshold)
149
-
150
- payload = {
151
- "model": model,
152
- "features": features,
153
- "threshold": threshold,
154
- "target": "same-day NIFTY 50 close > same-day NIFTY 50 open after first five 1-minute bars",
155
- "model_name": "compact_extra_trees_logit_overlay",
156
- "decision_overlays": DECISION_OVERLAYS,
157
- }
158
- joblib.dump(payload, MODEL_PATH)
159
-
160
- summary = RetrainSummary(
161
- model_name=payload["model_name"],
162
- threshold=float(threshold),
163
- train_rows=int(len(train_df)),
164
- valid_rows=int(len(valid_df)),
165
- test_rows=int(len(test_df)),
166
- validation_accuracy=float(accuracy_score(y_valid, valid_pred)),
167
- test_accuracy=float(accuracy_score(y_test, test_pred)),
168
- validation_auc=score_auc(y_valid, valid_prob),
169
- test_auc=score_auc(y_test, test_prob),
170
- test_brier=float(brier_score_loss(y_test, np.clip(test_prob, 1e-6, 1 - 1e-6))),
171
- latest_prediction="UP" if int(latest_pred[0]) == 1 else "DOWN",
172
- latest_prob_up=float(latest_prob[0]),
173
- latest_confidence=float(latest_conf[0]),
174
- feature_count=int(len(features)),
175
- )
176
- (MODEL_DIR / "summary.json").write_text(json.dumps(asdict(summary), indent=2), encoding="utf-8")
177
- pd.DataFrame([asdict(summary)]).to_csv(MODEL_DIR / "retrain_summary.csv", index=False)
178
- print(asdict(summary))
179
-
180
-
181
- if __name__ == "__main__":
182
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/scripts/run_ist_scheduler.py DELETED
@@ -1,37 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import time
4
- import sys
5
- from datetime import datetime
6
- from pathlib import Path
7
- from zoneinfo import ZoneInfo
8
-
9
- sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
10
- from nifty_backend.runtime import refresh_daily_data, refresh_first5_prediction, seconds_until_next_ist_run
11
-
12
-
13
- IST = ZoneInfo("Asia/Kolkata")
14
-
15
-
16
- def main() -> None:
17
- print("[scheduler] NIFTY first-five-minute scheduler started.")
18
- print("[scheduler] Runs the opening prediction after 09:20 IST so the 09:15-09:19 candles are complete.")
19
- while True:
20
- sleep_for = seconds_until_next_ist_run()
21
- target = datetime.now(IST).timestamp() + sleep_for
22
- print(f"[scheduler] sleeping {sleep_for / 60:.1f} minutes; next wake timestamp={target:.0f}")
23
- time.sleep(sleep_for)
24
- try:
25
- prediction = refresh_first5_prediction()
26
- print(f"[scheduler] first5 prediction refreshed: {prediction.to_dict()}")
27
- except Exception as exc:
28
- print(f"[scheduler] first5 refresh failed: {exc}")
29
- try:
30
- info = refresh_daily_data()
31
- print(f"[scheduler] daily data refreshed: {info}")
32
- except Exception as exc:
33
- print(f"[scheduler] daily refresh failed: {exc}")
34
-
35
-
36
- if __name__ == "__main__":
37
- main()