Jitendra12421 commited on
Commit
c17ec23
·
verified ·
1 Parent(s): 35558aa

Upload 12 files

Browse files
app.py CHANGED
@@ -8,6 +8,7 @@ from data_updater import update_daily_data, is_trading_day
8
  from forecaster_engine import generate_predictions
9
  from signal_generator import generate_signals
10
  from t5_engine import run_t5_pipeline
 
11
 
12
  IST = ZoneInfo("Asia/Kolkata")
13
  MARKET_CLOSE_BUFFER = time(15, 45) # Update runs after 3:45 PM
@@ -112,6 +113,37 @@ def t5_update_trigger(background_tasks: BackgroundTasks):
112
 
113
  return {"status": "triggered", "message": "T5 update pipeline started in the background."}
114
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  # ── NEW: Signal Generator Endpoints ──────────────────────────────────────────
116
 
117
  @app.get("/signals")
 
8
  from forecaster_engine import generate_predictions
9
  from signal_generator import generate_signals
10
  from t5_engine import run_t5_pipeline
11
+ from forecaster_cli import run_daemon
12
 
13
  IST = ZoneInfo("Asia/Kolkata")
14
  MARKET_CLOSE_BUFFER = time(15, 45) # Update runs after 3:45 PM
 
113
 
114
  return {"status": "triggered", "message": "T5 update pipeline started in the background."}
115
 
116
+ # ── NEW: NIFTY 50 Multi-Tier Forecaster Endpoints ─────────────────────────────
117
+
118
+ @app.get("/nifty50")
119
+ def get_nifty50_predictions():
120
+ """Get the latest high-conviction BUY predictions for NIFTY 50."""
121
+ nifty_file = os.path.join(os.path.dirname(__file__), "nifty50_predictions.json")
122
+ if not os.path.exists(nifty_file):
123
+ raise HTTPException(status_code=404, detail="NIFTY 50 predictions not yet generated")
124
+
125
+ with open(nifty_file, "r") as f:
126
+ data = json.load(f)
127
+
128
+ # Filter for high conviction trades (BUY)
129
+ high_conviction = [p for p in data.get("predictions", []) if p.get("Decision") == "BUY"]
130
+
131
+ return {
132
+ "last_updated": data.get("last_updated"),
133
+ "total_analyzed": len(data.get("predictions", [])),
134
+ "high_conviction_buys": len(high_conviction),
135
+ "predictions": high_conviction
136
+ }
137
+
138
+ @app.post("/cron/nifty50_update")
139
+ def nifty50_update_trigger(background_tasks: BackgroundTasks):
140
+ """
141
+ Trigger the multi-tier Random Forest NIFTY 50 forecasting daemon.
142
+ Should be called every two weeks.
143
+ """
144
+ background_tasks.add_task(run_daemon)
145
+ return {"status": "triggered", "message": "NIFTY 50 forecasting daemon started in the background."}
146
+
147
  # ── NEW: Signal Generator Endpoints ──────────────────────────────────────────
148
 
149
  @app.get("/signals")
feature_pipeline.py ADDED
@@ -0,0 +1,313 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import re
3
+ import threading
4
+ from datetime import datetime
5
+
6
+ import numpy as np
7
+ import pandas as pd
8
+ import requests
9
+ from bs4 import BeautifulSoup
10
+ from requests.adapters import HTTPAdapter
11
+ from urllib3.util.retry import Retry
12
+
13
+
14
+ class _ThreadLocalSessionFactory:
15
+ def __init__(self, headers=None, timeout=(3.5, 10.0), pool_maxsize=32):
16
+ self._local = threading.local()
17
+ self.headers = headers or {"User-Agent": "Mozilla/5.0"}
18
+ self.timeout = timeout
19
+ self.pool_maxsize = pool_maxsize
20
+
21
+ def get(self) -> requests.Session:
22
+ if not hasattr(self._local, "session"):
23
+ session = requests.Session()
24
+ session.headers.update(self.headers)
25
+ retry = Retry(
26
+ total=2,
27
+ connect=2,
28
+ read=2,
29
+ backoff_factor=0.15,
30
+ status_forcelist=(429, 500, 502, 503, 504),
31
+ allowed_methods=frozenset(["GET"]),
32
+ raise_on_status=False,
33
+ )
34
+ adapter = HTTPAdapter(
35
+ max_retries=retry,
36
+ pool_connections=self.pool_maxsize,
37
+ pool_maxsize=self.pool_maxsize,
38
+ )
39
+ session.mount("https://", adapter)
40
+ session.mount("http://", adapter)
41
+ self._local.session = session
42
+ return self._local.session
43
+
44
+
45
+ class ScreenerScraper:
46
+ def __init__(self):
47
+ self._session_factory = _ThreadLocalSessionFactory()
48
+ self.timeout = self._session_factory.timeout
49
+
50
+ def _session(self) -> requests.Session:
51
+ return self._session_factory.get()
52
+
53
+ @staticmethod
54
+ def _make_soup(html: str):
55
+ try:
56
+ return BeautifulSoup(html, "lxml")
57
+ except Exception:
58
+ return BeautifulSoup(html, "html.parser")
59
+
60
+ def _fetch_html(self, ticker: str, consolidated: bool = True) -> str:
61
+ t = ticker.upper().strip()
62
+ urls = [
63
+ f"https://www.screener.in/company/{t}/consolidated/",
64
+ f"https://www.screener.in/company/{t}/",
65
+ ] if consolidated else [
66
+ f"https://www.screener.in/company/{t}/",
67
+ f"https://www.screener.in/company/{t}/consolidated/",
68
+ ]
69
+
70
+ last_status = None
71
+ last_text = ""
72
+ session = self._session()
73
+
74
+ for url in urls:
75
+ try:
76
+ response = session.get(url, timeout=self.timeout)
77
+ last_status = response.status_code
78
+ last_text = response.text
79
+ if response.status_code == 200:
80
+ return response.text
81
+ except requests.RequestException:
82
+ continue
83
+
84
+ raise RuntimeError(f"Failed to fetch data for {t}. Last status: {last_status}")
85
+
86
+ @staticmethod
87
+ def _clean_text(value: str) -> str:
88
+ return " ".join(value.split()).replace("₹", "Rs.")
89
+
90
+ def get_stock_info(self, ticker):
91
+ html = self._fetch_html(ticker, consolidated=True)
92
+ soup = self._make_soup(html)
93
+
94
+ data = {
95
+ "ticker": ticker.upper(),
96
+ "key_metrics": {},
97
+ "history": {},
98
+ "documents": {},
99
+ }
100
+
101
+ top_ratios = soup.find("ul", id="top-ratios")
102
+ if top_ratios:
103
+ for li in top_ratios.find_all("li"):
104
+ n_span = li.find("span", class_="name")
105
+ v_span = li.find("span", class_="value")
106
+ if n_span and v_span:
107
+ name = n_span.get_text(strip=True).replace("₹", "Rs.")
108
+ val = self._clean_text(v_span.get_text(" ", strip=True))
109
+ data["key_metrics"][name] = val
110
+
111
+ sections = {
112
+ "quarters": "Quarterly Results",
113
+ "profit-loss": "Profit & Loss",
114
+ "balance-sheet": "Balance Sheet",
115
+ "cash-flow": "Cash Flows",
116
+ "ratios": "Financial Ratios",
117
+ }
118
+
119
+ for sec_id, sec_name in sections.items():
120
+ sec = soup.find("section", id=sec_id)
121
+ if not sec:
122
+ continue
123
+ tbl = sec.find("table")
124
+ if not tbl:
125
+ continue
126
+
127
+ thead = tbl.find("thead")
128
+ headers_list = [th.get_text(" ", strip=True) for th in thead.find_all("th")] if thead else []
129
+ tbody = tbl.find("tbody")
130
+ if not tbody:
131
+ continue
132
+
133
+ rows = []
134
+ for tr in tbody.find_all("tr"):
135
+ tds = tr.find_all("td")
136
+ if not tds:
137
+ continue
138
+ cols = [td.get_text(" ", strip=True) for td in tds]
139
+ rname = tr.find("td", class_="text")
140
+ if rname and cols:
141
+ cols[0] = rname.get_text(" ", strip=True).replace("+", "").strip()
142
+ rows.append(cols)
143
+
144
+ if not rows:
145
+ continue
146
+
147
+ if headers_list:
148
+ if len(headers_list) == len(rows[0]) - 1:
149
+ headers_list = ["Metric"] + headers_list
150
+ elif len(headers_list) == len(rows[0]):
151
+ headers_list[0] = "Metric"
152
+ else:
153
+ headers_list = ["Metric"] + headers_list[1:]
154
+
155
+ data["history"][sec_id] = {
156
+ "title": sec_name,
157
+ "headers": headers_list,
158
+ "rows": rows,
159
+ }
160
+
161
+ docs = soup.find_all("div", class_="documents")
162
+ for doc in docs:
163
+ h3 = doc.find("h3")
164
+ if not h3:
165
+ continue
166
+ sec_name = h3.get_text(" ", strip=True)
167
+ data["documents"][sec_name] = []
168
+ ul = doc.find("ul", class_="list-links")
169
+ if not ul:
170
+ continue
171
+ for li in ul.find_all("li"):
172
+ text_div = li.find("div")
173
+ a = li.find("a")
174
+ date_str = " ".join(text_div.get_text(" ", strip=True).replace("\n", " ").split()) if text_div else ""
175
+ link_str = a.get_text(" ", strip=True) if a else ""
176
+ if link_str and date_str.endswith(link_str):
177
+ date_str = date_str[:-len(link_str)].strip()
178
+ data["documents"][sec_name].append({"date": date_str, "title": link_str})
179
+
180
+ return data
181
+
182
+
183
+ class FeatureEngineer:
184
+ @staticmethod
185
+ def clean_numeric(val):
186
+ if pd.isna(val):
187
+ return np.nan
188
+ if isinstance(val, (int, float, np.number)):
189
+ return float(val)
190
+ if not isinstance(val, str):
191
+ return val
192
+ val = val.replace(",", "").replace("%", "").replace("₹", "").replace("Rs.", "").strip()
193
+ if val in ["-", ""]:
194
+ return np.nan
195
+ try:
196
+ return float(val)
197
+ except ValueError:
198
+ return val
199
+
200
+ @staticmethod
201
+ def parse_date(date_str):
202
+ try:
203
+ return pd.to_datetime(date_str, format="%b %Y")
204
+ except Exception:
205
+ return pd.to_datetime(date_str, errors="coerce")
206
+
207
+ @staticmethod
208
+ def _section_to_frame(sec_id, sec_data):
209
+ headers = sec_data.get("headers") or []
210
+ rows = sec_data.get("rows") or []
211
+ if len(headers) < 2 or not rows:
212
+ return None
213
+
214
+ df = pd.DataFrame(rows, columns=headers[: len(rows[0])])
215
+ if "Metric" not in df.columns:
216
+ df.columns = ["Metric"] + list(df.columns[1:])
217
+
218
+ df = df.set_index("Metric").transpose().reset_index().rename(columns={"index": "Date_Str"})
219
+ metric_cols = [c for c in df.columns if c != "Date_Str"]
220
+
221
+ if metric_cols:
222
+ cleaned = df[metric_cols].replace(
223
+ {",": "", "%": "", "₹": "", "Rs.": ""},
224
+ regex=True,
225
+ )
226
+ df[metric_cols] = cleaned.apply(pd.to_numeric, errors="coerce")
227
+
228
+ df["Date"] = df["Date_Str"].map(FeatureEngineer.parse_date)
229
+ df = df.dropna(subset=["Date"]).sort_values("Date").set_index("Date")
230
+ df = df.drop(columns=["Date_Str"])
231
+
232
+ prefix = sec_id.split("-")[0].upper() + "_"
233
+ df.columns = [f"{prefix}{col}" for col in df.columns]
234
+ return df
235
+
236
+ def build_features(self, data):
237
+ ticker = data["ticker"]
238
+
239
+ df_dict = {}
240
+ for sec_id, sec_data in data["history"].items():
241
+ df = self._section_to_frame(sec_id, sec_data)
242
+ if df is not None and not df.empty:
243
+ df_dict[sec_id] = df
244
+
245
+ if "quarters" not in df_dict or df_dict["quarters"].empty:
246
+ print("No quarterly data found.")
247
+ return None
248
+
249
+ main_df = df_dict["quarters"].copy()
250
+
251
+ for sec_id in ["profit-loss", "balance-sheet", "cash-flow", "ratios"]:
252
+ if sec_id in df_dict and not df_dict[sec_id].empty:
253
+ annual_df = df_dict[sec_id].sort_index()
254
+ main_df = pd.merge_asof(
255
+ main_df.sort_index(),
256
+ annual_df,
257
+ left_index=True,
258
+ right_index=True,
259
+ direction="backward",
260
+ )
261
+
262
+ announcements = data.get("documents", {}).get("Announcements", [])
263
+ if announcements:
264
+ event_dates = []
265
+ for ann in announcements:
266
+ ds = ann.get("date", "")
267
+ if "20" in ds:
268
+ parsed = pd.to_datetime(ds, errors="coerce")
269
+ if pd.notnull(parsed):
270
+ event_dates.append(parsed)
271
+ if event_dates:
272
+ events_series = pd.Series(1, index=pd.DatetimeIndex(event_dates))
273
+ quarterly_counts = events_series.resample("Q").sum()
274
+ main_df["ANN_COUNT"] = 0
275
+ for q_date, count in quarterly_counts.items():
276
+ valid_idx = main_df.index[main_df.index <= q_date]
277
+ if len(valid_idx) > 0:
278
+ main_df.loc[valid_idx[-1], "ANN_COUNT"] += int(count)
279
+
280
+ for k, v in data.get("key_metrics", {}).items():
281
+ main_df[f"STATIC_{k.replace(' ', '_')}"] = self.clean_numeric(v)
282
+
283
+ main_df["Ticker"] = ticker
284
+
285
+ if "QUARTERS_Sales" in main_df.columns:
286
+ main_df["QUARTERS_Sales_YoY_Growth"] = main_df["QUARTERS_Sales"].pct_change(periods=4) * 100
287
+
288
+ return main_df
289
+
290
+
291
+ def run_pipeline(ticker):
292
+ print(f"[{ticker}] Scraping data...")
293
+ data = ScreenerScraper().get_stock_info(ticker)
294
+
295
+ print(f"[{ticker}] Engineering features...")
296
+ df = FeatureEngineer().build_features(data)
297
+
298
+ if df is not None:
299
+ filename = f"{ticker}_features.parquet"
300
+ print(f"[{ticker}] Exporting to {filename}...")
301
+ df.to_parquet(filename, engine="pyarrow", index=True)
302
+ print("Success!")
303
+ return filename
304
+
305
+ print("Failed to build features.")
306
+ return None
307
+
308
+
309
+ if __name__ == "__main__":
310
+ import sys
311
+
312
+ ticker = sys.argv[1] if len(sys.argv) > 1 else "TCS"
313
+ run_pipeline(ticker)
forecaster_cli.py ADDED
@@ -0,0 +1,282 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import pandas as pd
3
+ import numpy as np
4
+ import concurrent.futures
5
+ import time
6
+ import joblib
7
+ import datetime
8
+ import os
9
+ import warnings
10
+
11
+ warnings.filterwarnings('ignore')
12
+
13
+ from feature_pipeline import ScreenerScraper
14
+
15
+ TIERS = {
16
+ "Large": "https://archives.nseindia.com/content/indices/ind_nifty100list.csv",
17
+ "Mid": "https://archives.nseindia.com/content/indices/ind_niftymidcap150list.csv",
18
+ "Small": "https://archives.nseindia.com/content/indices/ind_niftysmallcap250list.csv",
19
+ "Nifty50": "https://archives.nseindia.com/content/indices/ind_nifty50list.csv"
20
+ }
21
+
22
+ def get_current_historic_val(table_data, row_name):
23
+ if not table_data: return np.nan
24
+ for row in table_data.get('rows', []):
25
+ if row and row[0].lower() == row_name.lower():
26
+ for val_raw in reversed(row[1:]):
27
+ val = str(val_raw).replace(',', '').replace('%', '').strip()
28
+ if val not in ['-', '']:
29
+ try: return float(val)
30
+ except: continue
31
+ return np.nan
32
+
33
+ def fetch_live_features(ticker):
34
+ try:
35
+ scraper = ScreenerScraper()
36
+ data = None
37
+ for _ in range(3):
38
+ try:
39
+ data = scraper.get_stock_info(ticker)
40
+ if "error" in data and "429" in data["error"]:
41
+ time.sleep(1.5)
42
+ continue
43
+ break
44
+ except Exception:
45
+ time.sleep(1)
46
+
47
+ if not data or "error" in data: return None
48
+
49
+ pl = data['history'].get('profit-loss', {})
50
+ bs = data['history'].get('balance-sheet', {})
51
+ ratios = data['history'].get('ratios', {})
52
+
53
+ sales_row = None
54
+ for row in pl.get('rows', []):
55
+ if row and row[0].lower() == 'sales':
56
+ sales_row = row
57
+ break
58
+
59
+ sales = np.nan
60
+ prev_sales = np.nan
61
+ if sales_row and len(sales_row) >= 3:
62
+ try:
63
+ sales = float(str(sales_row[-1]).replace(',', '').strip())
64
+ prev_sales = float(str(sales_row[-2]).replace(',', '').strip())
65
+ except:
66
+ pass
67
+
68
+ opm = get_current_historic_val(pl, 'OPM %')
69
+ net_profit = get_current_historic_val(pl, 'Net Profit')
70
+ equity = get_current_historic_val(bs, 'Equity Capital')
71
+ reserves = get_current_historic_val(bs, 'Reserves')
72
+ borrowings = get_current_historic_val(bs, 'Borrowings')
73
+ roce = get_current_historic_val(ratios, 'ROCE %')
74
+
75
+ sales_growth = ((sales - prev_sales) / prev_sales * 100) if pd.notnull(prev_sales) and prev_sales > 0 else np.nan
76
+ total_eq = equity + reserves if pd.notnull(equity) and pd.notnull(reserves) else np.nan
77
+ roe = (net_profit / total_eq * 100) if pd.notnull(total_eq) and total_eq > 0 else np.nan
78
+ debt_to_equity = (borrowings / total_eq) if pd.notnull(total_eq) and total_eq > 0 else np.nan
79
+
80
+ pe = np.nan
81
+ for metric in data.get('metrics', []):
82
+ if metric['name'] == 'Stock P/E':
83
+ pe = metric['value']
84
+ break
85
+
86
+ return {
87
+ 'Ticker': ticker,
88
+ 'Sales_Growth': sales_growth,
89
+ 'OPM': opm,
90
+ 'ROCE': roce,
91
+ 'ROE': roe,
92
+ 'Debt_to_Equity': debt_to_equity,
93
+ 'PE_Ratio': pe
94
+ }
95
+ except Exception:
96
+ return None
97
+
98
+ def get_market_cap_tier(ticker):
99
+ print(f"[{ticker}] Resolving market cap classification from NSE servers...")
100
+ try:
101
+ large = pd.read_csv(TIERS["Large"])['Symbol'].tolist()
102
+ if ticker in large: return "Large"
103
+
104
+ mid = pd.read_csv(TIERS["Mid"])['Symbol'].tolist()
105
+ if ticker in mid: return "Mid"
106
+
107
+ small = pd.read_csv(TIERS["Small"])['Symbol'].tolist()
108
+ if ticker in small: return "Small"
109
+
110
+ except Exception as e:
111
+ print(f"Warning: Could not fetch NSE lists ({e}). Defaulting to Small Cap.")
112
+
113
+ return "Small" # Default to small cap model for everything else
114
+
115
+ def generate_reasoning(features, tier, prob):
116
+ reasons = []
117
+
118
+ if tier == "Large":
119
+ if pd.notnull(features.get('Sales_Growth')):
120
+ if features['Sales_Growth'] < 5: reasons.append(f"Weak Sales Growth ({features['Sales_Growth']:.1f}%) drags down Large Cap momentum.")
121
+ elif features['Sales_Growth'] > 15: reasons.append(f"Strong Sales Growth ({features['Sales_Growth']:.1f}%) is an excellent indicator for Large Caps.")
122
+ if pd.notnull(features.get('ROE')) and features['ROE'] > 20:
123
+ reasons.append(f"High ROE ({features['ROE']:.1f}%) shows efficient capital use.")
124
+ elif tier == "Small":
125
+ if pd.notnull(features.get('Debt_to_Equity')):
126
+ if features['Debt_to_Equity'] > 1.5: reasons.append(f"Dangerously high Debt/Equity ({features['Debt_to_Equity']:.2f}) signals severe structural risk.")
127
+ elif features['Debt_to_Equity'] < 0.5: reasons.append(f"Low Debt/Equity ({features['Debt_to_Equity']:.2f}) provides strong survival padding.")
128
+ if pd.notnull(features.get('OPM')) and features['OPM'] < 10:
129
+ reasons.append(f"Low margins ({features['OPM']:.1f}%) leave little room for error.")
130
+ elif tier == "Mid":
131
+ reasons.append("Mid caps exhibit inverted return logic. Metrics are volatile and evaluated in aggregate.")
132
+
133
+ if not reasons:
134
+ reasons.append("Fundamentals are mixed or average, showing no extreme strengths or weaknesses.")
135
+
136
+ return " ".join(reasons)
137
+
138
+ def run_inference(ticker):
139
+ tier = get_market_cap_tier(ticker)
140
+ print(f"[{ticker}] Classified as: {tier} Cap")
141
+
142
+ print(f"[{ticker}] Extracting live fundamental data...")
143
+ features = fetch_live_features(ticker)
144
+
145
+ if not features:
146
+ print(f"[{ticker}] Error: Could not extract live data.")
147
+ return
148
+
149
+ df = pd.DataFrame([features])
150
+ model_features = ['Sales_Growth', 'OPM', 'ROCE', 'ROE', 'Debt_to_Equity', 'PE_Ratio']
151
+ X = df[model_features].copy()
152
+
153
+ try:
154
+ model = joblib.load(f'rf_model_{tier.lower()}.pkl')
155
+ imputer = joblib.load(f'imputer_{tier.lower()}.pkl')
156
+ scaler = joblib.load(f'scaler_{tier.lower()}.pkl')
157
+ except Exception as e:
158
+ print(f"Error loading models: {e}")
159
+ return
160
+
161
+ X_imputed = imputer.transform(X)
162
+ X_scaled = scaler.transform(X_imputed)
163
+
164
+ prob = model.predict_proba(X_scaled)[0][1]
165
+
166
+ if prob > 0.65:
167
+ decision = "BUY"
168
+ else:
169
+ decision = "PASS"
170
+
171
+ reasoning = generate_reasoning(features, tier, prob)
172
+
173
+ print("\n" + "="*50)
174
+ print(f" FORECAST FOR {ticker} ({tier} Cap Model)")
175
+ print("="*50)
176
+ print(f" Decision: {decision} (Confidence: {prob*100:.1f}%)")
177
+ print(f" Reasoning: {reasoning}")
178
+ print("-" * 50)
179
+ print(f" Sales Growth: {features['Sales_Growth']:.2f}%")
180
+ print(f" ROE: {features['ROE']:.2f}%")
181
+ print(f" ROCE: {features['ROCE']:.2f}%")
182
+ print(f" Debt/Equity: {features['Debt_to_Equity']:.2f}")
183
+ print(f" OPM: {features['OPM']:.2f}%")
184
+ print("="*50 + "\n")
185
+ return {"Ticker": ticker, "Tier": tier, "Decision": decision, **features}
186
+
187
+ def run_daemon():
188
+ print("Starting NIFTY 50 Forecasting Daemon...")
189
+
190
+ try:
191
+ df_nifty = pd.read_csv(TIERS["Nifty50"])
192
+ tickers = df_nifty['Symbol'].tolist()
193
+ except Exception as e:
194
+ print(f"Could not load NIFTY 50 tickers: {e}")
195
+ return
196
+
197
+ print(f"[{datetime.datetime.now()}] Waking up to process NIFTY 50...")
198
+
199
+ # We don't fetch market cap tiers because NIFTY 50 is all Large Cap
200
+
201
+ # Verify rate limit status first with a test call
202
+ print("Testing connection...")
203
+ test_feat = fetch_live_features("RELIANCE")
204
+ if not test_feat:
205
+ print(f"[{datetime.datetime.now()}] Error: IP Rate Limit Block detected on startup. Will not proceed.")
206
+ return
207
+
208
+ results = []
209
+ total = len(tickers)
210
+ processed = 0
211
+ with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
212
+ futures = {executor.submit(fetch_live_features, ticker): ticker for ticker in tickers}
213
+ for future in concurrent.futures.as_completed(futures):
214
+ res = future.result()
215
+ processed += 1
216
+ if res:
217
+ results.append(res)
218
+
219
+ pct = int((processed / total) * 100)
220
+ bar = '#' * (pct // 5) + '-' * (20 - (pct // 5))
221
+ print(f"\r[NIFTY 50] {bar} {pct}% ({processed}/{total})", end='', flush=True)
222
+ print("\n", flush=True)
223
+
224
+ if results:
225
+ df = pd.DataFrame(results)
226
+ features = ['Sales_Growth', 'OPM', 'ROCE', 'ROE', 'Debt_to_Equity', 'PE_Ratio']
227
+
228
+ try:
229
+ # NIFTY 50 is strictly Large Cap
230
+ model = joblib.load('rf_model_large.pkl')
231
+ imputer = joblib.load('imputer_large.pkl')
232
+ scaler = joblib.load('scaler_large.pkl')
233
+
234
+ X = df[features].copy()
235
+ X_imputed = imputer.transform(X)
236
+ X_scaled = scaler.transform(X_imputed)
237
+
238
+ probs = model.predict_proba(X_scaled)[:, 1]
239
+ df['Confidence'] = probs
240
+ df['Decision'] = df['Confidence'].apply(lambda x: "BUY" if x > 0.65 else "PASS")
241
+
242
+ # Generate reasoning for each
243
+ reasonings = []
244
+ for _, row in df.iterrows():
245
+ feat_dict = {
246
+ 'Sales_Growth': row['Sales_Growth'],
247
+ 'ROE': row['ROE'],
248
+ 'ROCE': row['ROCE'],
249
+ 'Debt_to_Equity': row['Debt_to_Equity'],
250
+ 'OPM': row['OPM']
251
+ }
252
+ r = generate_reasoning(feat_dict, "Large", row['Confidence'])
253
+ reasonings.append(r)
254
+ df['Reasoning'] = reasonings
255
+
256
+ # Format report and save to JSON
257
+ report = df[['Ticker', 'Decision', 'Confidence', 'Reasoning', 'Sales_Growth', 'ROE', 'Debt_to_Equity', 'OPM']]
258
+ report_dict = report.to_dict(orient='records')
259
+ import json
260
+ with open("nifty50_predictions.json", "w") as f:
261
+ json.dump({"last_updated": datetime.datetime.now().isoformat(), "predictions": report_dict}, f, indent=4)
262
+
263
+ print(f"[{datetime.datetime.now()}] Successfully generated nifty50_predictions.json with {len(df)} stocks.", flush=True)
264
+ except Exception as e:
265
+ print(f"Error during NIFTY 50 prediction: {e}", flush=True)
266
+ else:
267
+ print(f"[{datetime.datetime.now()}] Error: Failed to extract live data for all 50 stocks (Likely IP Rate Limit Block). Skipping report generation.", flush=True)
268
+ print("Daemon run completed.", flush=True)
269
+
270
+ if __name__ == "__main__":
271
+ parser = argparse.ArgumentParser(description="Multi-Cap Stock Forecasting Engine")
272
+ parser.add_argument("--ticker", type=str, help="Run an on-demand forecast for a specific ticker")
273
+ parser.add_argument("--daemon", action="store_true", help="Run the background 14-day loop for NIFTY 50")
274
+
275
+ args = parser.parse_args()
276
+
277
+ if args.daemon:
278
+ run_daemon()
279
+ elif args.ticker:
280
+ run_inference(args.ticker.upper())
281
+ else:
282
+ print("Please provide a --ticker or run with --daemon. Use -h for help.")
imputer_large.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2894a48dbcb70bc99e657795b4973954e4599b5c83e97be9623d8c15a029990a
3
+ size 855
imputer_mid.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:455258365334b9db6b2af0c00433d97bc2c8eb5e29702d9ed95d9737874d4251
3
+ size 855
imputer_small.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d65a78b810f54e372e91724d3bd8d481f2806b5c2f4ed76c3c7377115be5bd49
3
+ size 855
rf_model_large.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c0231dc1ab983560de50edcbff6599edc673f9c2f528e8cd2c479d6421e963a7
3
+ size 150537
rf_model_mid.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:07e84e6700854d7545cb81d1c434f7c70173d92a4b84a1e2ced40bfee5a84464
3
+ size 135177
rf_model_small.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:481d91e098ec92aecaf115f137ef18da64e715a6ceaf2c44b38890967ce15c64
3
+ size 155337
scaler_large.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bcc1ca9f607af2f9fed7c3da780c9006eabc622cc8e3f3afb3176c04de4f286a
3
+ size 727
scaler_mid.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ec9343fa00941cae7852d91a0775ac01ac6ad94b692cdbaee12a2116e774f182
3
+ size 727
scaler_small.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2b648ba253b033f0b37d397e27d9e7152b0019c77cf7d5d95f148d93e8eaa0d6
3
+ size 727