Spaces:
Running
Running
| import argparse | |
| import pandas as pd | |
| import numpy as np | |
| import concurrent.futures | |
| import time | |
| import joblib | |
| import datetime | |
| import os | |
| import warnings | |
| warnings.filterwarnings('ignore') | |
| from feature_pipeline import ScreenerScraper | |
| TIERS = { | |
| "Large": "https://archives.nseindia.com/content/indices/ind_nifty100list.csv", | |
| "Mid": "https://archives.nseindia.com/content/indices/ind_niftymidcap150list.csv", | |
| "Small": "https://archives.nseindia.com/content/indices/ind_niftysmallcap250list.csv", | |
| "Nifty50": "https://archives.nseindia.com/content/indices/ind_nifty50list.csv" | |
| } | |
| def get_current_historic_val(table_data, row_name): | |
| if not table_data: return np.nan | |
| for row in table_data.get('rows', []): | |
| if row and row[0].lower() == row_name.lower(): | |
| for val_raw in reversed(row[1:]): | |
| val = str(val_raw).replace(',', '').replace('%', '').strip() | |
| if val not in ['-', '']: | |
| try: return float(val) | |
| except: continue | |
| return np.nan | |
| def fetch_live_features(ticker): | |
| import requests | |
| from bs4 import BeautifulSoup | |
| import re | |
| import numpy as np | |
| try: | |
| url = f"https://ticker.finology.in/company/{ticker}" | |
| html = requests.get(url, headers={'User-Agent': 'Mozilla/5.0'}).text | |
| soup = BeautifulSoup(html, 'html.parser') | |
| def clean(val): | |
| if not val: return np.nan | |
| try: return float(re.sub(r'[^\d.]', '', val)) | |
| except: return np.nan | |
| data = {} | |
| for div in soup.find_all('div', class_=re.compile(r'compess')): | |
| text = div.get_text(" ", strip=True) | |
| if "P/E" in text: data['PE_Ratio'] = clean(text.replace('P/E', '')) | |
| if "Sales Growth" in text: data['Sales_Growth'] = clean(text.replace('Sales Growth', '')) | |
| if "ROE" in text: data['ROE'] = clean(text.replace('ROE', '')) | |
| if "ROCE" in text: data['ROCE'] = clean(text.replace('ROCE', '')) | |
| if "CASH" in text: data['CASH'] = clean(text.replace('CASH', '')) | |
| if "DEBT " in text: data['DEBT'] = clean(text.replace('DEBT', '')) | |
| if "Book Value" in text: data['BV'] = clean(text.replace('Book Value', '')) | |
| if "No. of Shares" in text: data['Shares'] = clean(text.replace('No. of Shares', '')) | |
| debt_to_equity = np.nan | |
| if 'DEBT' in data and 'BV' in data and 'Shares' in data and data['BV'] > 0 and data['Shares'] > 0: | |
| total_equity = data['BV'] * data['Shares'] | |
| debt_to_equity = data['DEBT'] / total_equity if total_equity > 0 else 0 | |
| return { | |
| 'Ticker': ticker, | |
| 'Sales_Growth': data.get('Sales_Growth', np.nan), | |
| 'OPM': np.nan, # Imputer will naturally handle this gracefully | |
| 'ROCE': data.get('ROCE', np.nan), | |
| 'ROE': data.get('ROE', np.nan), | |
| 'Debt_to_Equity': debt_to_equity, | |
| 'PE_Ratio': data.get('PE_Ratio', np.nan) | |
| } | |
| except Exception: | |
| return None | |
| def get_market_cap_tier(ticker): | |
| print(f"[{ticker}] Resolving market cap classification via Finology...") | |
| try: | |
| import requests | |
| from bs4 import BeautifulSoup | |
| import re | |
| url = f"https://ticker.finology.in/company/{ticker}" | |
| html = requests.get(url, headers={'User-Agent': 'Mozilla/5.0'}).text | |
| soup = BeautifulSoup(html, 'html.parser') | |
| cap = 0 | |
| for div in soup.find_all('div', class_=re.compile(r'compess')): | |
| text = div.get_text(" ", strip=True) | |
| if "Market Cap" in text: | |
| try: cap = float(re.sub(r'[^\d.]', '', text.replace('Market Cap', ''))) | |
| except: pass | |
| if cap >= 20000: return "Large" | |
| if cap >= 5000: return "Mid" | |
| return "Small" | |
| except Exception as e: | |
| print(f"Warning: Could not fetch market cap ({e}). Defaulting to Small.") | |
| return "Small" # Default to small cap model for everything else | |
| def generate_reasoning(features, tier, prob): | |
| reasons = [] | |
| if tier == "Large": | |
| if pd.notnull(features.get('Sales_Growth')): | |
| if features['Sales_Growth'] < 5: reasons.append(f"Weak Sales Growth ({features['Sales_Growth']:.1f}%) drags down Large Cap momentum.") | |
| elif features['Sales_Growth'] > 15: reasons.append(f"Strong Sales Growth ({features['Sales_Growth']:.1f}%) is an excellent indicator for Large Caps.") | |
| if pd.notnull(features.get('ROE')) and features['ROE'] > 20: | |
| reasons.append(f"High ROE ({features['ROE']:.1f}%) shows efficient capital use.") | |
| elif tier == "Small": | |
| if pd.notnull(features.get('Debt_to_Equity')): | |
| if features['Debt_to_Equity'] > 1.5: reasons.append(f"Dangerously high Debt/Equity ({features['Debt_to_Equity']:.2f}) signals severe structural risk.") | |
| elif features['Debt_to_Equity'] < 0.5: reasons.append(f"Low Debt/Equity ({features['Debt_to_Equity']:.2f}) provides strong survival padding.") | |
| if pd.notnull(features.get('OPM')) and features['OPM'] < 10: | |
| reasons.append(f"Low margins ({features['OPM']:.1f}%) leave little room for error.") | |
| elif tier == "Mid": | |
| reasons.append("Mid caps exhibit inverted return logic. Metrics are volatile and evaluated in aggregate.") | |
| if not reasons: | |
| reasons.append("Fundamentals are mixed or average, showing no extreme strengths or weaknesses.") | |
| return " ".join(reasons) | |
| def get_prediction(ticker): | |
| tier = get_market_cap_tier(ticker) | |
| features = fetch_live_features(ticker) | |
| if not features: | |
| return {"error": "Could not extract live data."} | |
| df = pd.DataFrame([features]) | |
| model_features = ['Sales_Growth', 'OPM', 'ROCE', 'ROE', 'Debt_to_Equity', 'PE_Ratio'] | |
| X = df[model_features].copy() | |
| try: | |
| model = joblib.load(f'rf_model_{tier.lower()}.pkl') | |
| imputer = joblib.load(f'imputer_{tier.lower()}.pkl') | |
| scaler = joblib.load(f'scaler_{tier.lower()}.pkl') | |
| except Exception as e: | |
| return {"error": f"Error loading models: {e}"} | |
| X_imputed = imputer.transform(X) | |
| X_scaled = scaler.transform(X_imputed) | |
| prob = float(model.predict_proba(X_scaled)[0][1]) | |
| decision = "BUY" if prob > 0.65 else "PASS" | |
| reasoning = generate_reasoning(features, tier, prob) | |
| import math | |
| clean_features = {} | |
| for k, v in features.items(): | |
| if isinstance(v, float) and math.isnan(v): | |
| clean_features[k] = None | |
| else: | |
| clean_features[k] = v | |
| return { | |
| "Ticker": ticker, | |
| "Tier": tier, | |
| "Decision": decision, | |
| "Confidence": prob, | |
| "Reasoning": reasoning, | |
| "Features": clean_features | |
| } | |
| def run_inference(ticker): | |
| res = get_prediction(ticker) | |
| if "error" in res: | |
| print(f"[{ticker}] {res['error']}") | |
| return | |
| print("\n" + "="*50) | |
| print(f" FORECAST FOR {ticker} ({res['Tier']} Cap Model)") | |
| print("="*50) | |
| print(f" Decision: {res['Decision']} (Confidence: {res['Confidence']*100:.1f}%)") | |
| print(f" Reasoning: {res['Reasoning']}") | |
| print("-" * 50) | |
| for k, v in res['Features'].items(): | |
| print(f" {k}: {v}") | |
| print("="*50 + "\n") | |
| return {"Ticker": ticker, "Tier": tier, "Decision": decision, **features} | |
| def run_daemon(): | |
| print("Starting NIFTY 50 Forecasting Daemon...") | |
| try: | |
| df_nifty = pd.read_csv(TIERS["Nifty50"]) | |
| tickers = df_nifty['Symbol'].tolist() | |
| except Exception as e: | |
| print(f"Could not load NIFTY 50 tickers: {e}") | |
| return | |
| print(f"[{datetime.datetime.now()}] Waking up to process NIFTY 50...") | |
| # We don't fetch market cap tiers because NIFTY 50 is all Large Cap | |
| # Verify rate limit status first with a test call | |
| print("Testing connection...") | |
| test_feat = fetch_live_features("RELIANCE") | |
| if not test_feat: | |
| print(f"[{datetime.datetime.now()}] Error: IP Rate Limit Block detected on startup. Will not proceed.") | |
| return | |
| results = [] | |
| total = len(tickers) | |
| processed = 0 | |
| with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: | |
| futures = {executor.submit(fetch_live_features, ticker): ticker for ticker in tickers} | |
| for future in concurrent.futures.as_completed(futures): | |
| res = future.result() | |
| processed += 1 | |
| if res: | |
| results.append(res) | |
| pct = int((processed / total) * 100) | |
| bar = '#' * (pct // 5) + '-' * (20 - (pct // 5)) | |
| print(f"\r[NIFTY 50] {bar} {pct}% ({processed}/{total})", end='', flush=True) | |
| print("\n", flush=True) | |
| if results: | |
| df = pd.DataFrame(results) | |
| features = ['Sales_Growth', 'OPM', 'ROCE', 'ROE', 'Debt_to_Equity', 'PE_Ratio'] | |
| try: | |
| # NIFTY 50 is strictly Large Cap | |
| model = joblib.load('rf_model_large.pkl') | |
| imputer = joblib.load('imputer_large.pkl') | |
| scaler = joblib.load('scaler_large.pkl') | |
| X = df[features].copy() | |
| X_imputed = imputer.transform(X) | |
| X_scaled = scaler.transform(X_imputed) | |
| probs = model.predict_proba(X_scaled)[:, 1] | |
| df['Confidence'] = probs | |
| df['Decision'] = df['Confidence'].apply(lambda x: "BUY" if x > 0.65 else "PASS") | |
| # Generate reasoning for each | |
| reasonings = [] | |
| for _, row in df.iterrows(): | |
| feat_dict = { | |
| 'Sales_Growth': row['Sales_Growth'], | |
| 'ROE': row['ROE'], | |
| 'ROCE': row['ROCE'], | |
| 'Debt_to_Equity': row['Debt_to_Equity'], | |
| 'OPM': row['OPM'] | |
| } | |
| r = generate_reasoning(feat_dict, "Large", row['Confidence']) | |
| reasonings.append(r) | |
| df['Reasoning'] = reasonings | |
| # Format report and save to JSON | |
| report = df[['Ticker', 'Decision', 'Confidence', 'Reasoning', 'Sales_Growth', 'ROE', 'Debt_to_Equity', 'OPM']] | |
| report_dict = report.to_dict(orient='records') | |
| import math | |
| for item in report_dict: | |
| for k, v in item.items(): | |
| if isinstance(v, float) and math.isnan(v): | |
| item[k] = None | |
| import json | |
| with open("nifty50_predictions.json", "w") as f: | |
| json.dump({"last_updated": datetime.datetime.now().isoformat(), "predictions": report_dict}, f, indent=4) | |
| print(f"[{datetime.datetime.now()}] Successfully generated nifty50_predictions.json with {len(df)} stocks.", flush=True) | |
| except Exception as e: | |
| print(f"Error during NIFTY 50 prediction: {e}", flush=True) | |
| else: | |
| 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) | |
| print("Daemon run completed.", flush=True) | |
| if __name__ == "__main__": | |
| parser = argparse.ArgumentParser(description="Multi-Cap Stock Forecasting Engine") | |
| parser.add_argument("--ticker", type=str, help="Run an on-demand forecast for a specific ticker") | |
| parser.add_argument("--daemon", action="store_true", help="Run the background 14-day loop for NIFTY 50") | |
| args = parser.parse_args() | |
| if args.daemon: | |
| run_daemon() | |
| elif args.ticker: | |
| run_inference(args.ticker.upper()) | |
| else: | |
| print("Please provide a --ticker or run with --daemon. Use -h for help.") | |