Spaces:
Sleeping
Sleeping
Upload 12 files
Browse files- app.py +32 -0
- feature_pipeline.py +313 -0
- forecaster_cli.py +282 -0
- imputer_large.pkl +3 -0
- imputer_mid.pkl +3 -0
- imputer_small.pkl +3 -0
- rf_model_large.pkl +3 -0
- rf_model_mid.pkl +3 -0
- rf_model_small.pkl +3 -0
- scaler_large.pkl +3 -0
- scaler_mid.pkl +3 -0
- scaler_small.pkl +3 -0
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
|