Spaces:
Running
Running
Upload 52 files
Browse files- app.py +10 -0
- models/nifty_opening_mfe_regressor/outputs/latest_prediction.csv +1 -1
- models/nifty_opening_mfe_regressor/outputs/mfe_live_history.csv +2 -0
- models/tomorrow_latest_prediction.csv +1 -1
- models/tomorrow_prediction_history.parquet +2 -2
- models/yahoo_history_cache.sqlite3 +2 -2
- nifty_backend/__pycache__/runtime.cpython-311.pyc +2 -2
- nifty_backend/runtime.py +89 -0
- scraper.py +97 -0
app.py
CHANGED
|
@@ -38,6 +38,7 @@ from kotak_neo import (
|
|
| 38 |
KotakNeoSessionRequired,
|
| 39 |
kotak_neo_manager,
|
| 40 |
)
|
|
|
|
| 41 |
|
| 42 |
|
| 43 |
app = FastAPI(title="NIFTY 50 Forecaster Backend")
|
|
@@ -667,3 +668,12 @@ def data_refresh_market_close(
|
|
| 667 |
session_date: date | None = Query(default=None, description="Optional YYYY-MM-DD session date in IST."),
|
| 668 |
) -> dict:
|
| 669 |
return refresh_market_close_data(session_date=session_date)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
KotakNeoSessionRequired,
|
| 39 |
kotak_neo_manager,
|
| 40 |
)
|
| 41 |
+
from scraper import get_stock_info
|
| 42 |
|
| 43 |
|
| 44 |
app = FastAPI(title="NIFTY 50 Forecaster Backend")
|
|
|
|
| 668 |
session_date: date | None = Query(default=None, description="Optional YYYY-MM-DD session date in IST."),
|
| 669 |
) -> dict:
|
| 670 |
return refresh_market_close_data(session_date=session_date)
|
| 671 |
+
|
| 672 |
+
|
| 673 |
+
@app.get("/api/info/{ticker}")
|
| 674 |
+
def stock_info(ticker: str) -> dict:
|
| 675 |
+
data = get_stock_info(ticker)
|
| 676 |
+
if "error" in data:
|
| 677 |
+
raise HTTPException(status_code=404, detail=data["error"])
|
| 678 |
+
return data
|
| 679 |
+
|
models/nifty_opening_mfe_regressor/outputs/latest_prediction.csv
CHANGED
|
@@ -1,2 +1,2 @@
|
|
| 1 |
input_date,first5_start,first5_end,first5_close,predicted_up_points,predicted_down_points
|
| 2 |
-
2026-06-
|
|
|
|
| 1 |
input_date,first5_start,first5_end,first5_close,predicted_up_points,predicted_down_points
|
| 2 |
+
2026-06-11,2026-06-11 09:15:00,2026-06-11 09:19:00,23112.650390625,97.5434972440442,76.48054546871592
|
models/nifty_opening_mfe_regressor/outputs/mfe_live_history.csv
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
input_date,first5_start,first5_end,first5_close,predicted_up_points,predicted_down_points
|
| 2 |
+
2026-06-11,2026-06-11 09:15:00,2026-06-11 09:19:00,23112.650390625,97.5434972440442,76.48054546871592
|
models/tomorrow_latest_prediction.csv
CHANGED
|
@@ -1,2 +1,2 @@
|
|
| 1 |
input_date,target_date,prediction,prob_up,confidence,threshold,model_name,source_model,validation_accuracy,test_accuracy,source
|
| 2 |
-
2026-06-
|
|
|
|
| 1 |
input_date,target_date,prediction,prob_up,confidence,threshold,model_name,source_model,validation_accuracy,test_accuracy,source
|
| 2 |
+
2026-06-12,2026-06-15,UP,0.5568798000516237,0.5568798000516237,0.534,nifty_tomorrow_direction_model,locked_multiwindow_nifty50_ensemble_v2,0.5886524822695035,0.7070707070707071,live
|
models/tomorrow_prediction_history.parquet
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ba49d81e54af07af6cd1b7defee306558ab4377dc09888b1d8452446aae13f4a
|
| 3 |
+
size 8300
|
models/yahoo_history_cache.sqlite3
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8198956ffb6a3aca16eabd5f6fc4aa188ba816d324a7f9987caf631d1b0eea14
|
| 3 |
+
size 421888
|
nifty_backend/__pycache__/runtime.cpython-311.pyc
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:83ba07597a5f92396b6297f2fe401f732a2a617e82c6fd3311f2d3e5537475e5
|
| 3 |
+
size 143804
|
nifty_backend/runtime.py
CHANGED
|
@@ -60,6 +60,8 @@ MFE_OUTPUT_DIR = MODEL_DIR / "nifty_opening_mfe_regressor" / "outputs"
|
|
| 60 |
MFE_SUMMARY_PATH = MFE_OUTPUT_DIR / "summary.json"
|
| 61 |
MFE_LATEST_PATH = MFE_OUTPUT_DIR / "latest_prediction.csv"
|
| 62 |
MFE_TEST_PREDICTIONS_PATH = MFE_OUTPUT_DIR / "test_predictions.csv"
|
|
|
|
|
|
|
| 63 |
TPLUS1_MODEL_PATH = MODEL_DIR / "nifty_1420_tplus1_logistic_model.joblib"
|
| 64 |
TPLUS1_LATEST_PATH = MODEL_DIR / "tplus1_latest_prediction.csv"
|
| 65 |
TPLUS1_SUMMARY_PATH = MODEL_DIR / "tplus1_summary.json"
|
|
@@ -945,6 +947,80 @@ def latest_tplus1_prediction() -> dict[str, Any]:
|
|
| 945 |
}
|
| 946 |
|
| 947 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 948 |
def _minute_frame_for_tplus1() -> pd.DataFrame:
|
| 949 |
minute = pd.read_parquet(NIFTY_1M_PATH)
|
| 950 |
minute = minute.copy()
|
|
@@ -1267,6 +1343,9 @@ def dashboard_payload() -> dict[str, Any]:
|
|
| 1267 |
_file_cache_key(NIFTY_1M_PATH),
|
| 1268 |
_file_cache_key(LIVE_ACCURACY_PATH),
|
| 1269 |
_file_cache_key(TOMORROW_PREDICTION_HISTORY_PATH),
|
|
|
|
|
|
|
|
|
|
| 1270 |
)
|
| 1271 |
with _dashboard_payload_lock:
|
| 1272 |
return copy.deepcopy(_dashboard_payload_cached(key))
|
|
@@ -1442,6 +1521,8 @@ def _dashboard_payload_cached(key: tuple[tuple[str, int | None, int | None], ...
|
|
| 1442 |
tomorrow_test = load_tomorrow_test_predictions()
|
| 1443 |
tomorrow_history = _load_prediction_history(TOMORROW_PREDICTION_HISTORY_PATH)
|
| 1444 |
tplus1_test = load_tplus1_test_predictions()
|
|
|
|
|
|
|
| 1445 |
daily = pd.read_parquet(NIFTY_1D_PATH)
|
| 1446 |
daily["date"] = pd.to_datetime(daily["date"], errors="coerce")
|
| 1447 |
daily = daily.sort_values("date").tail(180)
|
|
@@ -1521,6 +1602,10 @@ def _dashboard_payload_cached(key: tuple[tuple[str, int | None, int | None], ...
|
|
| 1521 |
"latest": tplus1_latest,
|
| 1522 |
"summary": tplus1_summary,
|
| 1523 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1524 |
},
|
| 1525 |
"metrics": metrics,
|
| 1526 |
"models": model_metrics,
|
|
@@ -1551,6 +1636,10 @@ def refresh_first5_prediction(session_date: date | None = None, minutes: pd.Data
|
|
| 1551 |
merged = merged.drop_duplicates(subset=["date"], keep="last").sort_values("date").reset_index(drop=True)
|
| 1552 |
merged.to_parquet(OPENING_DATASET_PATH, index=False, compression="zstd")
|
| 1553 |
prediction = predict_row(row)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1554 |
return prediction
|
| 1555 |
|
| 1556 |
|
|
|
|
| 60 |
MFE_SUMMARY_PATH = MFE_OUTPUT_DIR / "summary.json"
|
| 61 |
MFE_LATEST_PATH = MFE_OUTPUT_DIR / "latest_prediction.csv"
|
| 62 |
MFE_TEST_PREDICTIONS_PATH = MFE_OUTPUT_DIR / "test_predictions.csv"
|
| 63 |
+
MFE_MODEL_PATH = MFE_OUTPUT_DIR / "nifty_opening_mfe_regressor.joblib"
|
| 64 |
+
MFE_LIVE_HISTORY_PATH = MFE_OUTPUT_DIR / "mfe_live_history.csv"
|
| 65 |
TPLUS1_MODEL_PATH = MODEL_DIR / "nifty_1420_tplus1_logistic_model.joblib"
|
| 66 |
TPLUS1_LATEST_PATH = MODEL_DIR / "tplus1_latest_prediction.csv"
|
| 67 |
TPLUS1_SUMMARY_PATH = MODEL_DIR / "tplus1_summary.json"
|
|
|
|
| 947 |
}
|
| 948 |
|
| 949 |
|
| 950 |
+
def load_mfe_summary() -> dict[str, Any]:
|
| 951 |
+
if MFE_SUMMARY_PATH.exists():
|
| 952 |
+
return json.loads(MFE_SUMMARY_PATH.read_text(encoding="utf-8"))
|
| 953 |
+
return {}
|
| 954 |
+
|
| 955 |
+
|
| 956 |
+
def latest_mfe_prediction() -> dict[str, Any]:
|
| 957 |
+
if MFE_LATEST_PATH.exists():
|
| 958 |
+
row = pd.read_csv(MFE_LATEST_PATH).iloc[-1].to_dict()
|
| 959 |
+
return {k: (None if pd.isna(v) else v) for k, v in row.items()}
|
| 960 |
+
summary = load_mfe_summary()
|
| 961 |
+
return {
|
| 962 |
+
"input_date": summary.get("latest_input_date"),
|
| 963 |
+
"first5_start": summary.get("latest_first5_start"),
|
| 964 |
+
"first5_end": summary.get("latest_first5_end"),
|
| 965 |
+
"predicted_up_points": summary.get("latest_predicted_up_points"),
|
| 966 |
+
"predicted_down_points": summary.get("latest_predicted_down_points"),
|
| 967 |
+
}
|
| 968 |
+
|
| 969 |
+
|
| 970 |
+
def refresh_mfe_prediction(session_date: date | None = None) -> dict[str, Any]:
|
| 971 |
+
if not MFE_MODEL_PATH.exists():
|
| 972 |
+
return {}
|
| 973 |
+
payload = joblib.load(MFE_MODEL_PATH)
|
| 974 |
+
up_model = payload["up_model"]
|
| 975 |
+
down_model = payload["down_model"]
|
| 976 |
+
up_features = payload["up_features"]
|
| 977 |
+
down_features = payload["down_features"]
|
| 978 |
+
up_calib = payload["up_calibration"]
|
| 979 |
+
down_calib = payload["down_calibration"]
|
| 980 |
+
|
| 981 |
+
dataset = pd.read_parquet(OPENING_DATASET_PATH)
|
| 982 |
+
dataset["_session_date"] = pd.to_datetime(dataset["date"], errors="coerce").dt.normalize()
|
| 983 |
+
if session_date is not None:
|
| 984 |
+
latest_df = dataset[dataset["_session_date"].dt.date == session_date].tail(1)
|
| 985 |
+
else:
|
| 986 |
+
latest_df = dataset.tail(1)
|
| 987 |
+
|
| 988 |
+
if latest_df.empty:
|
| 989 |
+
return {}
|
| 990 |
+
|
| 991 |
+
raw_up = float(np.clip(up_model.predict(latest_df[up_features])[0], 0.0, None))
|
| 992 |
+
pred_up = float(np.clip((raw_up * up_calib["scale"]) + up_calib["offset"], 0.0, None))
|
| 993 |
+
|
| 994 |
+
raw_down = float(np.clip(down_model.predict(latest_df[down_features])[0], 0.0, None))
|
| 995 |
+
pred_down = float(np.clip((raw_down * down_calib["scale"]) + down_calib["offset"], 0.0, None))
|
| 996 |
+
|
| 997 |
+
out = {
|
| 998 |
+
"input_date": latest_df["_session_date"].dt.date.iloc[0].isoformat(),
|
| 999 |
+
"first5_start": str(latest_df["first5_start"].iloc[0]),
|
| 1000 |
+
"first5_end": str(latest_df["first5_end"].iloc[0]),
|
| 1001 |
+
"first5_close": float(latest_df["first5_close"].iloc[0]),
|
| 1002 |
+
"predicted_up_points": pred_up,
|
| 1003 |
+
"predicted_down_points": pred_down,
|
| 1004 |
+
}
|
| 1005 |
+
pd.DataFrame([out]).to_csv(MFE_LATEST_PATH, index=False)
|
| 1006 |
+
|
| 1007 |
+
# Append to live history
|
| 1008 |
+
live_df = pd.DataFrame([out])
|
| 1009 |
+
if MFE_LIVE_HISTORY_PATH.exists():
|
| 1010 |
+
try:
|
| 1011 |
+
existing = pd.read_csv(MFE_LIVE_HISTORY_PATH)
|
| 1012 |
+
# Avoid duplicates if refreshed multiple times in the same session
|
| 1013 |
+
existing = existing[existing["input_date"] != out["input_date"]]
|
| 1014 |
+
pd.concat([existing, live_df], ignore_index=True).to_csv(MFE_LIVE_HISTORY_PATH, index=False)
|
| 1015 |
+
except Exception:
|
| 1016 |
+
live_df.to_csv(MFE_LIVE_HISTORY_PATH, index=False)
|
| 1017 |
+
else:
|
| 1018 |
+
live_df.to_csv(MFE_LIVE_HISTORY_PATH, index=False)
|
| 1019 |
+
|
| 1020 |
+
clear_dashboard_payload_cache()
|
| 1021 |
+
return out
|
| 1022 |
+
|
| 1023 |
+
|
| 1024 |
def _minute_frame_for_tplus1() -> pd.DataFrame:
|
| 1025 |
minute = pd.read_parquet(NIFTY_1M_PATH)
|
| 1026 |
minute = minute.copy()
|
|
|
|
| 1343 |
_file_cache_key(NIFTY_1M_PATH),
|
| 1344 |
_file_cache_key(LIVE_ACCURACY_PATH),
|
| 1345 |
_file_cache_key(TOMORROW_PREDICTION_HISTORY_PATH),
|
| 1346 |
+
_file_cache_key(MFE_SUMMARY_PATH),
|
| 1347 |
+
_file_cache_key(MFE_LATEST_PATH),
|
| 1348 |
+
_file_cache_key(MFE_MODEL_PATH),
|
| 1349 |
)
|
| 1350 |
with _dashboard_payload_lock:
|
| 1351 |
return copy.deepcopy(_dashboard_payload_cached(key))
|
|
|
|
| 1521 |
tomorrow_test = load_tomorrow_test_predictions()
|
| 1522 |
tomorrow_history = _load_prediction_history(TOMORROW_PREDICTION_HISTORY_PATH)
|
| 1523 |
tplus1_test = load_tplus1_test_predictions()
|
| 1524 |
+
mfe_summary = load_mfe_summary()
|
| 1525 |
+
mfe_latest = latest_mfe_prediction()
|
| 1526 |
daily = pd.read_parquet(NIFTY_1D_PATH)
|
| 1527 |
daily["date"] = pd.to_datetime(daily["date"], errors="coerce")
|
| 1528 |
daily = daily.sort_values("date").tail(180)
|
|
|
|
| 1602 |
"latest": tplus1_latest,
|
| 1603 |
"summary": tplus1_summary,
|
| 1604 |
},
|
| 1605 |
+
"mfe": {
|
| 1606 |
+
"latest": mfe_latest,
|
| 1607 |
+
"summary": mfe_summary,
|
| 1608 |
+
},
|
| 1609 |
},
|
| 1610 |
"metrics": metrics,
|
| 1611 |
"models": model_metrics,
|
|
|
|
| 1636 |
merged = merged.drop_duplicates(subset=["date"], keep="last").sort_values("date").reset_index(drop=True)
|
| 1637 |
merged.to_parquet(OPENING_DATASET_PATH, index=False, compression="zstd")
|
| 1638 |
prediction = predict_row(row)
|
| 1639 |
+
try:
|
| 1640 |
+
refresh_mfe_prediction(session_date=session_date)
|
| 1641 |
+
except Exception as exc:
|
| 1642 |
+
print(f"MFE refresh failed: {exc}", flush=True)
|
| 1643 |
return prediction
|
| 1644 |
|
| 1645 |
|
scraper.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
from bs4 import BeautifulSoup
|
| 3 |
+
|
| 4 |
+
def get_stock_info(ticker):
|
| 5 |
+
url = f"https://www.screener.in/company/{ticker.upper()}/consolidated/"
|
| 6 |
+
headers = {
|
| 7 |
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36"
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
response = requests.get(url, headers=headers)
|
| 11 |
+
if response.status_code == 404:
|
| 12 |
+
url = f"https://www.screener.in/company/{ticker.upper()}/"
|
| 13 |
+
response = requests.get(url, headers=headers)
|
| 14 |
+
|
| 15 |
+
if response.status_code != 200:
|
| 16 |
+
return {"error": f"Failed to fetch data for {ticker.upper()}. Status {response.status_code}"}
|
| 17 |
+
|
| 18 |
+
soup = BeautifulSoup(response.text, 'html.parser')
|
| 19 |
+
|
| 20 |
+
data = {
|
| 21 |
+
"ticker": ticker.upper(),
|
| 22 |
+
"key_metrics": {},
|
| 23 |
+
"pros": [],
|
| 24 |
+
"cons": [],
|
| 25 |
+
"growth": [],
|
| 26 |
+
"history": {}
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
# 1. Key Metrics
|
| 30 |
+
top_ratios = soup.find('ul', id='top-ratios')
|
| 31 |
+
if top_ratios:
|
| 32 |
+
for li in top_ratios.find_all('li'):
|
| 33 |
+
n_span = li.find('span', class_='name')
|
| 34 |
+
v_span = li.find('span', class_='value')
|
| 35 |
+
if n_span and v_span:
|
| 36 |
+
name = n_span.text.strip().replace('₹', 'Rs.')
|
| 37 |
+
val = ' '.join(v_span.text.split()).replace('₹', 'Rs.')
|
| 38 |
+
data["key_metrics"][name] = val
|
| 39 |
+
|
| 40 |
+
# 2. Pros/Cons
|
| 41 |
+
analysis = soup.find('section', id='analysis')
|
| 42 |
+
if analysis:
|
| 43 |
+
pros = analysis.find('div', class_='pros')
|
| 44 |
+
if pros:
|
| 45 |
+
data["pros"] = [li.text.strip() for li in pros.find_all('li')]
|
| 46 |
+
cons = analysis.find('div', class_='cons')
|
| 47 |
+
if cons:
|
| 48 |
+
data["cons"] = [li.text.strip() for li in cons.find_all('li')]
|
| 49 |
+
|
| 50 |
+
# 3. Growth Metrics
|
| 51 |
+
ranges = soup.find_all('table', class_='ranges-table')
|
| 52 |
+
for table in ranges:
|
| 53 |
+
th = table.find('th')
|
| 54 |
+
if not th: continue
|
| 55 |
+
metric = th.text.strip()
|
| 56 |
+
for tr in table.find_all('tr')[1:]:
|
| 57 |
+
tds = tr.find_all('td')
|
| 58 |
+
if len(tds) == 2:
|
| 59 |
+
data["growth"].append({
|
| 60 |
+
"Metric": metric,
|
| 61 |
+
"Period": tds[0].text.strip(),
|
| 62 |
+
"Value": tds[1].text.strip()
|
| 63 |
+
})
|
| 64 |
+
|
| 65 |
+
# 4. Tables
|
| 66 |
+
sections = {
|
| 67 |
+
'quarters': 'Quarterly Results',
|
| 68 |
+
'profit-loss': 'Profit & Loss',
|
| 69 |
+
'balance-sheet': 'Balance Sheet',
|
| 70 |
+
'cash-flow': 'Cash Flows',
|
| 71 |
+
'ratios': 'Financial Ratios',
|
| 72 |
+
'shareholding': 'Shareholding Pattern'
|
| 73 |
+
}
|
| 74 |
+
for sec_id, sec_name in sections.items():
|
| 75 |
+
sec = soup.find('section', id=sec_id)
|
| 76 |
+
if not sec: continue
|
| 77 |
+
tbl = sec.find('table')
|
| 78 |
+
if not tbl: continue
|
| 79 |
+
thead = tbl.find('thead')
|
| 80 |
+
headers = [th.text.strip().replace('₹', 'Rs.') for th in thead.find_all('th')] if thead else []
|
| 81 |
+
rows = []
|
| 82 |
+
for tr in tbl.find('tbody').find_all('tr'):
|
| 83 |
+
cols = [td.text.strip().replace('₹', 'Rs.') for td in tr.find_all('td')]
|
| 84 |
+
rname = tr.find('td', class_='text')
|
| 85 |
+
if rname:
|
| 86 |
+
cols[0] = rname.text.replace('+', '').strip()
|
| 87 |
+
if cols:
|
| 88 |
+
rows.append(cols)
|
| 89 |
+
if headers and len(headers) == len(rows[0]) - 1:
|
| 90 |
+
headers.insert(0, 'Metric')
|
| 91 |
+
data["history"][sec_id] = {
|
| 92 |
+
"title": sec_name,
|
| 93 |
+
"headers": headers,
|
| 94 |
+
"rows": rows
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
return data
|