Spaces:
Runtime error
Runtime error
| # ----------------------- | |
| # streamlit_trading_dashboard.py | |
| # Added: NSE market calendar check (pandas_market_calendars when available; yf fallback) | |
| # ----------------------- | |
| import warnings | |
| import logging | |
| import urllib3 | |
| import streamlit as st # type: ignore | |
| import pandas as pd | |
| import numpy as np | |
| import yfinance as yf | |
| import time | |
| import threading | |
| from datetime import datetime, timedelta, date | |
| import plotly.graph_objects as go | |
| from sklearn.linear_model import LinearRegression, ElasticNet | |
| from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor, ExtraTreesRegressor | |
| from sklearn.svm import SVR | |
| from sklearn.neighbors import KNeighborsRegressor | |
| from sklearn.preprocessing import MinMaxScaler | |
| from sklearn.metrics import mean_absolute_error, mean_squared_error | |
| from statsmodels.tsa.arima.model import ARIMA | |
| from statsmodels.tsa.statespace.sarimax import SARIMAX | |
| from statsmodels.tsa.holtwinters import ExponentialSmoothing | |
| # optional tensorflow / transformers | |
| TENSORFLOW_AVAILABLE = False | |
| try: | |
| import tensorflow as tf | |
| from tensorflow.keras.models import Sequential # type: ignore | |
| from tensorflow.keras.layers import LSTM, Dense, Input # type: ignore | |
| TENSORFLOW_AVAILABLE = True | |
| except Exception: | |
| TENSORFLOW_AVAILABLE = False | |
| TRANSFORMERS_AVAILABLE = False | |
| try: | |
| from transformers import pipeline | |
| TRANSFORMERS_AVAILABLE = True | |
| except Exception: | |
| TRANSFORMERS_AVAILABLE = False | |
| # silence some noisy output | |
| warnings.filterwarnings("ignore", category=DeprecationWarning) | |
| urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) | |
| logging.getLogger("yfinance").setLevel(logging.ERROR) | |
| logging.getLogger("urllib3").setLevel(logging.ERROR) | |
| st.set_page_config(page_title="Trading Dashboard", layout="wide") | |
| st.markdown( | |
| """ | |
| <style> | |
| .topbar { padding: 10px 16px; border-radius: 8px; background: linear-gradient(90deg,#0f172a,#0b1220); color: white; margin-bottom: 8px;} | |
| .stButton>button {width:100% !important; height:40px; margin-bottom:6px;} | |
| table {border-collapse: collapse;} | |
| th, td {padding:6px 8px;} | |
| .arrow-up { color: green; font-weight:700 } | |
| .arrow-down { color: red; font-weight:700 } | |
| </style> | |
| """, | |
| unsafe_allow_html=True, | |
| ) | |
| st.markdown('<div class="topbar"><h2>Trading Dashboard </h2></div>', unsafe_allow_html=True) | |
| # simple global live snapshot + thread control | |
| LIVE_PRICE = None | |
| LIVE_LOCK = threading.Lock() | |
| LIVE_STOP_EVENT = None | |
| LIVE_THREAD = None | |
| LIVE_WS = None # <<-- hold reference to websocket object so we can close it | |
| # ----------------------- | |
| # cached helpers | |
| # ----------------------- | |
| def safe_history(ticker, period="10y", interval="1d"): | |
| t = yf.Ticker(ticker) | |
| try: | |
| hist = t.history(period=period, interval=interval, auto_adjust=False) | |
| except Exception: | |
| hist = pd.DataFrame() | |
| return hist | |
| def safe_info(ticker): | |
| t = yf.Ticker(ticker) | |
| try: | |
| return t.info | |
| except Exception: | |
| return {} | |
| def safe_news(ticker, count=200): | |
| t = yf.Ticker(ticker) | |
| try: | |
| n = getattr(t, "get_news", None) | |
| if callable(n): | |
| return n(count=count) | |
| return getattr(t, "news", []) or [] | |
| except Exception: | |
| return [] | |
| def safe_financials(ticker): | |
| # avoid fundamentals calls for index tickers to prevent 404 noise | |
| if isinstance(ticker, str) and ticker.startswith("^"): | |
| return None, None, None | |
| t = yf.Ticker(ticker) | |
| def sg(attr): | |
| try: | |
| a = getattr(t, attr) | |
| return a() if callable(a) else a | |
| except Exception: | |
| return None | |
| balance = sg("get_balance_sheet") | |
| if balance is None or (hasattr(balance, "empty") and balance.empty): | |
| balance = sg("balance_sheet") | |
| income = sg("get_income_stmt") | |
| if income is None or (hasattr(income, "empty") and income.empty): | |
| income = sg("income_stmt") | |
| cashflow = sg("get_cashflow") | |
| if cashflow is None or (hasattr(cashflow, "empty") and cashflow.empty): | |
| cashflow = sg("cashflow") | |
| return balance, income, cashflow | |
| def load_finbert_pipeline(): | |
| if not TRANSFORMERS_AVAILABLE: | |
| return None | |
| try: | |
| return pipeline("sentiment-analysis", model="ProsusAI/finbert", tokenizer="ProsusAI/finbert", device=-1) | |
| except Exception: | |
| try: | |
| return pipeline("sentiment-analysis", device=-1) | |
| except Exception: | |
| return None | |
| def finbert_score_from_label(item): | |
| label = item.get("label", "").lower() | |
| score = float(item.get("score", 0.0)) | |
| if "neg" in label: | |
| return -score | |
| if "pos" in label: | |
| return score | |
| return 0.0 | |
| def compute_news_sentiment(news_list, backend="finbert"): | |
| if not news_list: | |
| return pd.Series(dtype=float) | |
| items = [] | |
| for it in news_list: | |
| c = it.get("content", {}) if isinstance(it, dict) else {} | |
| title = (c.get("title") or it.get("title") or "") | |
| summary = (c.get("summary") or it.get("summary") or "") | |
| text = (title + " " + summary).strip() | |
| pdstr = c.get("pubDate") or c.get("displayTime") or it.get("pubDate") or it.get("displayTime") | |
| try: | |
| ts = pd.to_datetime(pdstr) if pdstr else pd.Timestamp.now() | |
| except Exception: | |
| ts = pd.Timestamp.now() | |
| items.append({"dt": ts.normalize(), "text": text[:2048]}) | |
| dfn = pd.DataFrame(items).set_index("dt") | |
| if dfn.empty: | |
| return pd.Series(dtype=float) | |
| if backend == "finbert": | |
| pipe = load_finbert_pipeline() | |
| if pipe is None: | |
| return pd.Series(dtype=float) | |
| scores = [] | |
| for txt in dfn["text"].tolist(): | |
| try: | |
| out = pipe(txt[:1024]) | |
| out0 = out[0] if isinstance(out, list) else out | |
| scores.append(finbert_score_from_label(out0)) | |
| except Exception: | |
| scores.append(np.nan) | |
| dfn["score"] = scores | |
| dfn_mean = dfn.groupby(dfn.index.date)["score"].mean() | |
| dfn_mean.index = pd.to_datetime(dfn_mean.index) | |
| return dfn_mean.sort_index() | |
| return pd.Series(dtype=float) | |
| # ----------------------- | |
| # Market open check for Indian market (NSE) | |
| # ----------------------- | |
| import yfinance as yf | |
| import pandas as pd | |
| import requests | |
| from datetime import date, datetime, timedelta | |
| HOLIDAY_CACHE = None | |
| def load_nse_holidays(): | |
| global HOLIDAY_CACHE | |
| if HOLIDAY_CACHE is not None: | |
| return HOLIDAY_CACHE | |
| url = "https://www.nseindia.com/api/holiday-master?type=trading" | |
| headers = {"User-Agent": "Mozilla/5.0", "Accept": "application/json"} | |
| r = requests.get(url, headers=headers, timeout=10) | |
| r.raise_for_status() | |
| data = r.json() | |
| holidays = set() | |
| for item in data.get("CBM", []) + data.get("TRADING", []) + data.get("CLEARING", []): | |
| try: | |
| d = datetime.strptime(item["tradingDate"], "%d-%b-%Y").date() | |
| holidays.add(d) | |
| except: | |
| pass | |
| HOLIDAY_CACHE = holidays | |
| return holidays | |
| def fallback(value, last_close): | |
| try: | |
| v = float(value) | |
| if pd.isna(v): | |
| return float(last_close) | |
| return v | |
| except: | |
| return float(last_close) | |
| def to_returns(series): | |
| return series.pct_change().dropna() | |
| def price_from_returns(last_price, future_returns): | |
| price = last_price | |
| for r in future_returns: | |
| price *= (1 + r) | |
| return float(price) | |
| def is_nse_open_on(check_date: date): | |
| """ | |
| Returns: (is_open, reason) | |
| """ | |
| # 1οΈβ£ weekend | |
| if check_date.weekday() >= 5: | |
| return False, "weekend" | |
| # 2οΈβ£ official NSE holidays | |
| try: | |
| if check_date in load_nse_holidays(): | |
| return False, "nse_official_holiday" | |
| except: | |
| pass | |
| # 3οΈβ£ market calendar (authoritative for trading sessions) | |
| calendar_says_open = None | |
| try: | |
| import pandas_market_calendars as mcal | |
| try: | |
| cal = mcal.get_calendar("NSE") | |
| except: | |
| cal = mcal.get_calendar("XNSE") | |
| start = (check_date - timedelta(days=2)).strftime("%Y-%m-%d") | |
| end = (check_date + timedelta(days=2)).strftime("%Y-%m-%d") | |
| sched = cal.schedule(start_date=start, end_date=end) | |
| if sched is not None and not sched.empty: | |
| session_days = {pd.to_datetime(x).date() for x in sched.index} | |
| if check_date in session_days: | |
| calendar_says_open = True | |
| else: | |
| return False, "nse_calendar:holiday" | |
| except: | |
| calendar_says_open = None | |
| # 4οΈβ£ Yahoo (used ONLY as a sanity check β never to block predictions) | |
| try: | |
| t = yf.Ticker("^NSEI") | |
| win_start = (pd.Timestamp(check_date) - pd.Timedelta(days=10)).strftime("%Y-%m-%d") | |
| win_end = (pd.Timestamp(check_date) + pd.Timedelta(days=2)).strftime("%Y-%m-%d") | |
| hist = t.history(start=win_start, end=win_end, interval="1d") | |
| if not hist.empty: | |
| traded_days = {pd.to_datetime(x).date() for x in hist.index} | |
| if check_date in traded_days: | |
| return True, "history" | |
| # βοΈ IMPORTANT CHANGE: | |
| # If calendar says open but Yahoo has no data (future days) β STILL OPEN | |
| if calendar_says_open: | |
| return True, "calendar_open_no_yahoo_data" | |
| # otherwise treat as holiday | |
| return False, "no_data_yahoo" | |
| except Exception: | |
| # if Yahoo fails entirely but calendar says open β STILL OPEN | |
| if calendar_says_open: | |
| return True, "calendar_open_yahoo_error" | |
| return False, "yahoo_error" | |
| # small utilities | |
| def human_readable(n): | |
| try: | |
| n = float(n) | |
| except Exception: | |
| return n | |
| units = [(1e12, "T"), (1e9, "B"), (1e6, "M"), (1e3, "K")] | |
| for div, suf in units: | |
| if abs(n) >= div: | |
| return f"{n/div:,.2f}{suf}" | |
| return f"{n:,.2f}" | |
| def create_supervised_features(series: pd.Series, lags: int = 5, exog: pd.DataFrame = None): | |
| df = pd.DataFrame({"y": series}) | |
| for lag in range(1, lags + 1): | |
| df[f"lag_{lag}"] = df["y"].shift(lag) | |
| if exog is not None: | |
| exog = exog.reindex(df.index).ffill().fillna(0.0) | |
| for c in exog.columns: | |
| df[c] = exog[c].values | |
| df = df.dropna() | |
| return df | |
| def evaluate_preds(y_true, y_pred): | |
| y_true = np.array(y_true).astype(float) | |
| y_pred = np.array(y_pred).astype(float) | |
| mae = float(np.mean(np.abs(y_true - y_pred))) | |
| rmse = float(np.sqrt(np.mean((y_true - y_pred) ** 2))) | |
| denom = np.where(np.abs(y_true) < 1e-8, 1e-8, np.abs(y_true)) | |
| mape = float(np.mean(np.abs((y_true - y_pred) / denom))) * 100.0 | |
| return {"MAE": mae, "RMSE": rmse, "MAPE": mape} | |
| # ----------------------- | |
| # session defaults & top UI | |
| # ----------------------- | |
| if "loaded_ticker" not in st.session_state: | |
| st.session_state.loaded_ticker = None | |
| if "hist" not in st.session_state: | |
| st.session_state.hist = pd.DataFrame() | |
| if "info" not in st.session_state: | |
| st.session_state.info = {} | |
| if "news" not in st.session_state: | |
| st.session_state.news = [] | |
| if "balance" not in st.session_state: | |
| st.session_state.balance = None | |
| if "income" not in st.session_state: | |
| st.session_state.income = None | |
| if "cashflow" not in st.session_state: | |
| st.session_state.cashflow = None | |
| if "selected_chart" not in st.session_state: | |
| st.session_state.selected_chart = "Price" | |
| if "live_running" not in st.session_state: | |
| st.session_state.live_running = False | |
| if "news_sentiment" not in st.session_state: | |
| st.session_state.news_sentiment = pd.Series(dtype=float) | |
| # Top controls layout (uses st.toggle for live) | |
| col1, col2, col3 = st.columns([3, 2, 2]) | |
| with col1: | |
| ticker = st.text_input("Ticker (e.g., ICICIBANK.NS or ^NSEI)", value=st.session_state.loaded_ticker or "^NSEI", key="ticker_input") | |
| load_btn = st.button("Load / Refresh") | |
| with col2: | |
| period = st.selectbox("History period", ["6mo", "1y", "2y", "5y", "10y"], index=4, key="period_input") | |
| interval = st.selectbox("Interval", ["1d", "1wk", "1mo"], index=0, key="interval_input") | |
| with col3: | |
| # Use st.toggle (user requested) β it's a boolean control | |
| try: | |
| live_toggle = st.toggle("Live stream (toggle ON/OFF)", value=st.session_state.get("live_running", False), key="live_toggle") | |
| except Exception: | |
| # fallback if st.toggle is not available: use checkbox (keeps functionality) | |
| live_toggle = st.checkbox("Live stream (toggle ON/OFF)", value=st.session_state.get("live_running", False), key="live_toggle_fallback") | |
| poll_seconds = st.number_input("Poll interval (s)", min_value=5, max_value=60, value=10, step=1, key="poll_interval") | |
| refresh_now = st.button("Refresh Live Now") | |
| def load_ticker(ticker_symbol, period, interval): | |
| hist = safe_history(ticker_symbol, period=period, interval=interval) | |
| info = safe_info(ticker_symbol) | |
| news = safe_news(ticker_symbol, count=200) | |
| bal, inc, cf = safe_financials(ticker_symbol) | |
| if not hist.empty and not isinstance(hist.index, pd.DatetimeIndex): | |
| hist.index = pd.to_datetime(hist.index) | |
| st.session_state.loaded_ticker = ticker_symbol | |
| st.session_state.hist = hist | |
| st.session_state.info = info | |
| st.session_state.news = news | |
| st.session_state.balance = bal | |
| st.session_state.income = inc | |
| st.session_state.cashflow = cf | |
| st.session_state.news_sentiment = pd.Series(dtype=float) | |
| global LIVE_PRICE | |
| with LIVE_LOCK: | |
| LIVE_PRICE = LIVE_PRICE | |
| if load_btn: | |
| load_ticker(ticker, period, interval) | |
| if st.session_state.loaded_ticker is None: | |
| load_ticker(ticker, period, interval) | |
| # --- Live stream (fixed stop) --- | |
| def ws_listener(ticker_sym, stop_event): | |
| WS = getattr(yf, "WebSocket", None) | |
| if WS is None: | |
| return False | |
| try: | |
| with WS() as ws: | |
| ws.subscribe([ticker_sym]) | |
| for message in ws.listen(): | |
| if stop_event.is_set(): | |
| try: | |
| ws.unsubscribe([ticker_sym]) | |
| ws.close() | |
| except Exception: | |
| pass | |
| break | |
| try: | |
| if isinstance(message, dict): | |
| price = message.get("price") or message.get("close") | |
| ts = message.get("time") or datetime.now().isoformat() | |
| try: | |
| if isinstance(ts, str) and ts.isdigit() and len(ts) >= 10: | |
| ts = datetime.fromtimestamp( | |
| int(ts) / (1000.0 if len(ts) >= 13 else 1) | |
| ).isoformat() | |
| except Exception: | |
| pass | |
| if price is not None: | |
| with LIVE_LOCK: | |
| global LIVE_PRICE | |
| LIVE_PRICE = {"price": float(price), "time": str(ts)} | |
| except Exception: | |
| continue | |
| return True | |
| except Exception: | |
| return False | |
| def poller(ticker_sym, interval_s, stop_event): | |
| while not stop_event.is_set(): | |
| try: | |
| t = yf.Ticker(ticker_sym) | |
| recent = t.history(period="1d", interval="1m", auto_adjust=False) | |
| if not recent.empty: | |
| price = float(recent["Close"].iloc[-1]) | |
| ts = str(recent.index[-1]) | |
| with LIVE_LOCK: | |
| global LIVE_PRICE | |
| LIVE_PRICE = {"price": price, "time": ts} | |
| else: | |
| h = st.session_state.hist | |
| if not h.empty: | |
| with LIVE_LOCK: | |
| LIVE_PRICE = {"price": float(h["Close"].iloc[-1]), "time": str(h.index[-1])} | |
| except Exception: | |
| with LIVE_LOCK: | |
| LIVE_PRICE = {"error": "poll error"} | |
| # wait so we can exit quickly when stop_event is set | |
| stop_event.wait(timeout=interval_s) | |
| def start_live_thread(ticker_sym, interval_s): | |
| global LIVE_STOP_EVENT, LIVE_THREAD, LIVE_WS | |
| # If there's an existing run, stop it first (defensive) | |
| if LIVE_STOP_EVENT is not None and not LIVE_STOP_EVENT.is_set(): | |
| stop_live_thread() | |
| LIVE_STOP_EVENT = threading.Event() | |
| LIVE_WS = None | |
| def runner(ev, tck, interval_local): | |
| used = False | |
| try: | |
| used = ws_listener(tck, ev) | |
| except Exception: | |
| used = False | |
| if not used and not ev.is_set(): | |
| poller(tck, interval_local, ev) | |
| # thread exit cleanup | |
| try: | |
| # ensure WS closed | |
| if LIVE_WS is not None: | |
| try: | |
| if hasattr(LIVE_WS, "close"): | |
| LIVE_WS.close() | |
| except Exception: | |
| pass | |
| finally: | |
| try: | |
| # try to close underlying socket if accessible | |
| sock = getattr(LIVE_WS, "_sock", None) or getattr(LIVE_WS, "sock", None) | |
| if sock is not None: | |
| try: | |
| sock.close() | |
| except Exception: | |
| pass | |
| except Exception: | |
| pass | |
| try: | |
| # ensure cleared | |
| globals()['LIVE_WS'] = None | |
| except Exception: | |
| pass | |
| except Exception: | |
| pass | |
| finally: | |
| try: | |
| st.session_state.live_running = False | |
| except Exception: | |
| pass | |
| LIVE_THREAD = threading.Thread(target=runner, args=(LIVE_STOP_EVENT, ticker_sym, interval_s), daemon=True) | |
| LIVE_THREAD.start() | |
| st.session_state.live_running = True | |
| def stop_live_thread(): | |
| global LIVE_STOP_EVENT, LIVE_THREAD, LIVE_WS | |
| # set the stop event | |
| if LIVE_STOP_EVENT is not None: | |
| try: | |
| LIVE_STOP_EVENT.set() | |
| except Exception: | |
| pass | |
| # Best-effort: close the websocket object to unblock ws.listen() | |
| if LIVE_WS is not None: | |
| try: | |
| if hasattr(LIVE_WS, "close"): | |
| try: | |
| LIVE_WS.close() | |
| except Exception: | |
| pass | |
| # try underlying socket objects too | |
| sock = getattr(LIVE_WS, "_sock", None) or getattr(LIVE_WS, "sock", None) | |
| if sock is not None: | |
| try: | |
| sock.close() | |
| except Exception: | |
| pass | |
| except Exception: | |
| pass | |
| finally: | |
| try: | |
| globals()['LIVE_WS'] = None | |
| except Exception: | |
| pass | |
| # try to join thread briefly | |
| try: | |
| if LIVE_THREAD is not None: | |
| LIVE_THREAD.join(timeout=1.0) | |
| except Exception: | |
| pass | |
| LIVE_THREAD = None | |
| st.session_state.live_running = False | |
| # react to toggle state change | |
| # If toggle is True and thread not running -> start it | |
| # If toggle is False and thread running -> stop it | |
| if live_toggle and not st.session_state.get("live_running", False): | |
| start_live_thread(st.session_state.loaded_ticker or ticker, int(poll_seconds)) | |
| if (not live_toggle) and st.session_state.get("live_running", False): | |
| stop_live_thread() | |
| # Refresh now | |
| if refresh_now: | |
| try: | |
| t_temp = yf.Ticker(st.session_state.loaded_ticker or ticker) | |
| recent = t_temp.history(period="1d", interval="1m", auto_adjust=False) | |
| if not recent.empty: | |
| price = float(recent["Close"].iloc[-1]) | |
| ts = str(recent.index[-1]) | |
| with LIVE_LOCK: | |
| LIVE_PRICE = {"price": price, "time": ts} | |
| else: | |
| h = st.session_state.hist | |
| if not h.empty: | |
| with LIVE_LOCK: | |
| LIVE_PRICE = {"price": float(h["Close"].iloc[-1]), "time": str(h.index[-1])} | |
| except Exception: | |
| with LIVE_LOCK: | |
| LIVE_PRICE = {"error": "refresh failed"} | |
| with LIVE_LOCK: | |
| live_snapshot = dict(LIVE_PRICE) if LIVE_PRICE is not None else None | |
| tabs = st.tabs(["Overview", "Financials", "News", "Charts", "Forecast"]) | |
| # Overview | |
| with tabs[0]: | |
| info = st.session_state.info or {} | |
| hist = st.session_state.hist.copy() | |
| st.subheader(info.get("shortName") or info.get("longName") or st.session_state.loaded_ticker) | |
| if hist.empty: | |
| st.warning("No historical data available for this ticker.") | |
| else: | |
| last_close = float(hist["Close"].iloc[-1]) | |
| prev_close = float(hist["Close"].iloc[-2]) if len(hist) > 1 else last_close | |
| change = last_close - prev_close | |
| pct = (change / prev_close * 100) if prev_close != 0 else 0.0 | |
| live_price_val = live_snapshot.get("price") if live_snapshot else None | |
| live_time_val = live_snapshot.get("time") if live_snapshot else None | |
| chart_col, metrics_col = st.columns([8, 6]) | |
| with chart_col: | |
| fig = go.Figure() | |
| fig.add_trace(go.Scatter(x=hist.index, y=hist["Close"], name="Close", line=dict(width=2))) | |
| fig.update_layout(height=560, margin=dict(l=0, r=0, t=20, b=0)) | |
| st.plotly_chart(fig, use_container_width=True) | |
| with metrics_col: | |
| r1c1, r1c2 = st.columns([1, 1]) | |
| with r1c1: | |
| if live_price_val is not None: | |
| st.metric("Live Price", f"{live_price_val:.2f}", delta=f"{(live_price_val - last_close):.2f}") | |
| else: | |
| st.metric("Live Price", "β") | |
| if live_time_val: | |
| st.caption(f"Live update: {live_time_val}") | |
| with r1c2: | |
| st.metric("Last Close", f"{last_close:.2f}", delta=f"{change:.2f}") | |
| st.markdown(f"**Pct change:** {pct:.2f}%") | |
| st.markdown("---") | |
| r2c1, r2c2 = st.columns([1, 1]) | |
| with r2c1: | |
| try: | |
| st.metric("Open", f"{float(hist['Open'].iloc[-1]):.2f}") | |
| except Exception: | |
| st.metric("Open", "β") | |
| with r2c2: | |
| st.metric("Previous Close", f"{prev_close:.2f}") | |
| st.markdown("---") | |
| r3c1, r3c2, r3c3 = st.columns([1, 1, 1]) | |
| with r3c1: | |
| st.metric("Market Cap", human_readable(info.get("marketCap", "β"))) | |
| with r3c2: | |
| st.metric("PE (TTM)", f"{info.get('trailingPE', 'β')}") | |
| with r3c3: | |
| st.metric("Beta", f"{info.get('beta', 'β')}") | |
| st.markdown("---") | |
| r4c1, r4c2 = st.columns([1, 1]) | |
| with r4c1: | |
| st.metric("52W High", f"{info.get('fiftyTwoWeekHigh', 'β')}") | |
| with r4c2: | |
| st.metric("52W Low", f"{info.get('fiftyTwoWeekLow', 'β')}") | |
| # Financials | |
| with tabs[1]: | |
| st.header("Financial Statements & Key Data") | |
| bal, inc, cf = st.session_state.balance, st.session_state.income, st.session_state.cashflow | |
| if bal is None and inc is None and cf is None: | |
| st.info("No financial statements available (likely an index ticker or missing data).") | |
| else: | |
| with st.expander("Balance Sheet"): | |
| st.dataframe(bal if bal is not None else pd.DataFrame()) | |
| with st.expander("Income Statement"): | |
| st.dataframe(inc if inc is not None else pd.DataFrame()) | |
| with st.expander("Cash Flow"): | |
| st.dataframe(cf if cf is not None else pd.DataFrame()) | |
| if not (isinstance(st.session_state.loaded_ticker, str) and st.session_state.loaded_ticker.startswith("^")): | |
| t = yf.Ticker(st.session_state.loaded_ticker) | |
| try: | |
| rec = t.get_recommendations() if getattr(t, "get_recommendations", None) else getattr(t, "recommendations", None) | |
| if rec is not None and len(rec) > 0: | |
| st.subheader("Analyst Recommendations") | |
| st.dataframe(rec.head(20)) | |
| except Exception: | |
| pass | |
| try: | |
| earnings = t.get_earnings() if getattr(t, "get_earnings", None) else getattr(t, "earnings", None) | |
| if earnings is not None and len(earnings) > 0: | |
| st.subheader("Earnings History") | |
| st.dataframe(earnings.tail(10)) | |
| except Exception: | |
| pass | |
| try: | |
| capg = t.get_capital_gains() if getattr(t, "get_capital_gains", None) else getattr(t, "capital_gains", None) | |
| if capg is not None: | |
| st.subheader("Capital Gains") | |
| st.dataframe(capg) | |
| except Exception: | |
| pass | |
| else: | |
| st.info("Skipping recommendations/earnings/capital gains for index tickers (e.g., ^NSEI).") | |
| # News & FinBERT | |
| with tabs[2]: | |
| st.header("News & Sentiment") | |
| news_list = st.session_state.news or [] | |
| if not news_list: | |
| st.info("No news available for this ticker.") | |
| else: | |
| use_finbert = st.checkbox("Enable FinBERT sentiment (optional)", value=False) | |
| N = st.slider("Number of news items to show", 1, min(100, len(news_list)), min(10, len(news_list))) | |
| items = news_list[:N] | |
| pipe = None | |
| if use_finbert: | |
| with st.spinner("Loading FinBERT..."): | |
| pipe = load_finbert_pipeline() | |
| if pipe is None: | |
| st.warning("FinBERT not available (install transformers & torch).") | |
| for it in items: | |
| c = it.get("content", {}) if isinstance(it, dict) else {} | |
| title = c.get("title") or it.get("title") or "No title" | |
| summary = c.get("summary") or it.get("summary") or "" | |
| provider = (c.get("provider") or {}).get("displayName") or "" | |
| published = c.get("pubDate") or c.get("displayTime") or "" | |
| url = (c.get("canonicalUrl") or {}).get("url") or (c.get("clickThroughUrl") or {}).get("url") or "" | |
| st.markdown(f"### [{title}]({url})") | |
| st.caption(f"{provider} β’ {published}") | |
| if summary: | |
| st.write(summary) | |
| if pipe is not None: | |
| try: | |
| out = pipe((title + " " + summary)[:1024]) | |
| out0 = out[0] if isinstance(out, list) else out | |
| score = finbert_score_from_label(out0) | |
| badge = "Bullish πΊ" if score > 0.05 else ("Bearish π»" if score < -0.05 else "Neutral") | |
| st.markdown(f"**FinBERT:** {badge} ({score:.3f})") | |
| except Exception as e: | |
| st.write("FinBERT error:", e) | |
| st.divider() | |
| # Charts | |
| with tabs[3]: | |
| st.header("Charts") | |
| left_col, right_col = st.columns([1, 4]) | |
| charts = ["Price", "Candlestick", "EMA", "SMA", "RSI", "MACD", "Volume"] | |
| with left_col: | |
| for ch in charts: | |
| if st.button(ch, key=f"chart_btn_{ch}"): | |
| st.session_state.selected_chart = ch | |
| with right_col: | |
| sel = st.session_state.selected_chart or "Price" | |
| st.markdown(f"### {sel}") | |
| hist = st.session_state.hist | |
| if hist.empty: | |
| st.info("No history to plot.") | |
| else: | |
| if sel == "Price": | |
| fig = go.Figure(); fig.add_trace(go.Scatter(x=hist.index, y=hist["Close"], name="Close")); st.plotly_chart(fig, use_container_width=True) | |
| elif sel == "Candlestick": | |
| fig = go.Figure(); fig.add_trace(go.Candlestick(x=hist.index, open=hist["Open"], high=hist["High"], low=hist["Low"], close=hist["Close"])); st.plotly_chart(fig, use_container_width=True) | |
| elif sel == "EMA": | |
| hist["EMA20"] = hist["Close"].ewm(span=20).mean(); hist["EMA50"] = hist["Close"].ewm(span=50).mean() | |
| fig = go.Figure(); fig.add_trace(go.Scatter(x=hist.index, y=hist["EMA20"], name="EMA20")); fig.add_trace(go.Scatter(x=hist.index, y=hist["EMA50"], name="EMA50")); st.plotly_chart(fig, use_container_width=True) | |
| elif sel == "SMA": | |
| hist["SMA50"] = hist["Close"].rolling(50).mean(); fig = go.Figure(); fig.add_trace(go.Scatter(x=hist.index, y=hist["SMA50"], name="SMA50")); st.plotly_chart(fig, use_container_width=True) | |
| elif sel == "RSI": | |
| delta = hist["Close"].diff() | |
| up = delta.clip(lower=0).rolling(14).mean() | |
| down = -delta.clip(upper=0).rolling(14).mean() | |
| rsi = 100 - (100 / (1 + (up / down).replace([np.inf, -np.inf], np.nan))) | |
| fig = go.Figure(); fig.add_trace(go.Scatter(x=hist.index, y=rsi, name="RSI")); st.plotly_chart(fig, use_container_width=True) | |
| elif sel == "MACD": | |
| ema12 = hist["Close"].ewm(span=12).mean(); ema26 = hist["Close"].ewm(span=26).mean() | |
| macd = ema12 - ema26; signal = macd.ewm(span=9).mean() | |
| fig = go.Figure(); fig.add_trace(go.Scatter(x=hist.index, y=macd, name="MACD")); fig.add_trace(go.Scatter(x=hist.index, y=signal, name="Signal")); st.plotly_chart(fig, use_container_width=True) | |
| elif sel == "Volume": | |
| fig = go.Figure(); fig.add_trace(go.Bar(x=hist.index, y=hist["Volume"], name="Volume")); st.plotly_chart(fig, use_container_width=True) | |
| # helper: naive exog forecast | |
| def forecast_exog_from_price(last_close_series, days_ahead, ema_spans=(10, 50), last_volume=None, last_sentiment=0.0): | |
| if last_close_series.empty or days_ahead <= 0: | |
| return pd.DataFrame(columns=['EMA10', 'EMA50', 'Volume_fill', 'sentiment']) | |
| y = last_close_series.values | |
| n = len(y) | |
| if n >= 3: | |
| t = np.arange(n).reshape(-1, 1) | |
| lr = LinearRegression().fit(t, y) | |
| future_t = np.arange(n, n + days_ahead).reshape(-1, 1) | |
| price_fore = lr.predict(future_t) | |
| else: | |
| k = min(5, n) | |
| mv = np.mean(y[-k:]) if n > 0 else 0 | |
| price_fore = np.array([mv] * days_ahead) | |
| ema_vals = {} | |
| for span in ema_spans: | |
| alpha = 2.0 / (span + 1.0) | |
| last_ema = last_close_series.ewm(span=span).mean().iloc[-1] | |
| arr = [] | |
| prev = last_ema | |
| for p in price_fore: | |
| prev = alpha * p + (1 - alpha) * prev | |
| arr.append(prev) | |
| ema_vals[f"EMA{span}"] = arr | |
| volumes = [float(last_volume) if last_volume is not None else 0.0] * days_ahead | |
| sentiments = [float(last_sentiment)] * days_ahead | |
| future_index = [ (last_close_series.index[-1].date() + timedelta(days=i+1)) for i in range(days_ahead) ] | |
| df = pd.DataFrame({ | |
| "EMA10": ema_vals.get("EMA10", [np.nan]*days_ahead), | |
| "EMA50": ema_vals.get("EMA50", [np.nan]*days_ahead), | |
| "Volume_fill": volumes, | |
| "sentiment": sentiments | |
| }, index=pd.to_datetime(future_index)) | |
| return df | |
| # ----------------------- | |
| # Forecast & Backtest (models with NaN models) - REPLACEMENT BLOCK | |
| # Paste this entire block in place of your current "with tabs[4]:" section | |
| # ----------------------- | |
| # Forecast & Backtest (with trimming NaN models) | |
| with tabs[4]: | |
| st.header("Forecast & Backtest (models with NaN removed)") | |
| hist = st.session_state.hist.copy() | |
| if hist.empty: | |
| st.info("No historical data to forecast.") | |
| else: | |
| col_a, col_b = st.columns([3, 1]) | |
| with col_a: | |
| target_date = st.date_input("Forecast target date", value=date.today() + timedelta(days=1)) | |
| available_models = [ | |
| 'Naive', 'SeasonalNaive', 'MovingAverage', 'LinearReg', 'RandomForest', | |
| 'ARIMA', 'ARIMAX', 'LSTM', 'GradientBoosting', 'ExtraTrees', | |
| 'ElasticNet', 'KNN', 'SVR', 'HoltWinters', 'EnsembleMean' | |
| ] | |
| models_sel = st.multiselect("Models", available_models, default=['LSTM','ARIMA','LinearReg','RandomForest','HoltWinters']) | |
| use_finbert_for_forecast = st.checkbox("Use FinBERT sentiment as exogenous feature (optional)", value=False) | |
| run = st.button("Run Forecasts") | |
| st.markdown("### Backtesting (walk-forward 1-step)") | |
| backtest_run = st.button("Run Backtest") | |
| n_obs = len(hist) | |
| default_train = min(252, max(30, n_obs - 10)) if n_obs > 40 else max(10, n_obs - 2) | |
| bt_window = st.number_input("Backtest train window (days)", min_value=10, max_value=max(10, n_obs-1), value=int(default_train)) | |
| default_steps = max(1, min(100, max(1, n_obs - int(bt_window) - 1))) | |
| bt_steps = st.number_input("Backtest steps (max test points)", min_value=1, max_value=max(1, n_obs-1), value=int(default_steps)) | |
| lags = st.number_input("Lag features (for ML models)", min_value=1, max_value=30, value=5) | |
| with col_b: | |
| st.metric("Last Close", f"{float(hist['Close'].iloc[-1]):.2f}") | |
| # compute sentiment if needed | |
| if use_finbert_for_forecast: | |
| with st.spinner("Computing FinBERT sentiment series..."): | |
| s_series = compute_news_sentiment(st.session_state.news, backend="finbert") | |
| if not s_series.empty: | |
| sentiment_map = {pd.Timestamp(d).date(): v for d, v in s_series.items()} | |
| hist['sentiment'] = [sentiment_map.get(d.date(), np.nan) for d in hist.index] | |
| hist['sentiment'] = pd.Series(hist['sentiment']).ffill().fillna(0.0).values | |
| else: | |
| hist['sentiment'] = 0.0 | |
| else: | |
| hist['sentiment'] = 0.0 | |
| hist['EMA10'] = hist['Close'].ewm(span=10).mean() | |
| hist['EMA50'] = hist['Close'].ewm(span=50).mean() | |
| hist['Volume_fill'] = hist['Volume'].ffill().fillna(0.0) | |
| last_dt = hist.index[-1] | |
| days_ahead = (target_date - last_dt.date()).days | |
| if days_ahead < 0: | |
| days_ahead = 0 | |
| future_exog = forecast_exog_from_price(hist['Close'], max(days_ahead,1), ema_spans=(10,50), | |
| last_volume=float(hist['Volume'].iloc[-1]) if not hist['Volume'].empty else 0.0, | |
| last_sentiment=float(hist['sentiment'].iloc[-1]) if 'sentiment' in hist.columns else 0.0) | |
| # run forecasts | |
| if run: | |
| if days_ahead == 0: | |
| st.warning("Target date must be after last available historical date for forecasting.") | |
| else: | |
| # check NSE market calendar before making predictions | |
| is_open, reason = is_nse_open_on(target_date) | |
| if not is_open: | |
| st.warning(f"Forecast skipped β market appears closed on {target_date} (reason: {reason}).") | |
| else: | |
| close = hist['Close'] | |
| results = {} | |
| def try_float(x): | |
| try: | |
| return float(x) | |
| except Exception: | |
| return np.nan | |
| # Naive | |
| if 'Naive' in models_sel: | |
| results['Naive'] = fallback(close.iloc[-1], close.iloc[-1]) | |
| # SeasonalNaive | |
| if 'SeasonalNaive' in models_sel: | |
| try: | |
| period = 5 if len(close) >= 5 else 1 | |
| idx = -period if len(close) >= period else -1 | |
| val = close.iloc[idx] | |
| except: | |
| val = close.iloc[-1] | |
| results['SeasonalNaive'] = fallback(val, close.iloc[-1]) | |
| # MovingAverage | |
| if 'MovingAverage' in models_sel: | |
| mv = close.rolling(20).mean().iloc[-1] if len(close) >= 20 else close.mean() | |
| results['MovingAverage'] = fallback(mv, close.iloc[-1]) | |
| # LinearReg | |
| if 'LinearReg' in models_sel: | |
| try: | |
| df_lr = close.reset_index(drop=True).reset_index() | |
| df_lr.columns = ['t','y'] | |
| lr = LinearRegression().fit(df_lr[['t']], df_lr['y']) | |
| t_future = len(df_lr) + days_ahead | |
| pred = lr.predict([[t_future]])[0] | |
| except: | |
| pred = close.iloc[-1] | |
| results['LinearReg'] = fallback(pred, close.iloc[-1]) | |
| # helper for tree-like forecasts with lags + exog | |
| def forecast_via_tree_return(model_factory): | |
| try: | |
| # build returns target | |
| ret = to_returns(close) | |
| df = pd.DataFrame({'r': ret}) | |
| ex = hist[['EMA10','EMA50','Volume_fill','sentiment']].iloc[-len(ret):].reset_index(drop=True) | |
| df = df.join(ex) | |
| for lag in range(1, lags+1): | |
| df[f'lag_{lag}'] = df['r'].shift(lag) | |
| df = df.dropna().reset_index(drop=True) | |
| if df.shape[0] < max(20, lags+2): | |
| return float(close.iloc[-1]) | |
| X = df.drop(columns=['r']) | |
| y = df['r'] | |
| model = model_factory() | |
| model.fit(X, y) | |
| cur = X.iloc[[-1]].copy() | |
| cols = X.columns | |
| preds = [] | |
| for step in range(days_ahead): | |
| cur = cur[cols] | |
| rhat = float(model.predict(cur)[0]) | |
| preds.append(rhat) | |
| for i in range(lags, 1, -1): | |
| cur[f'lag_{i}'] = cur[f'lag_{i-1}'] | |
| cur['lag_1'] = rhat | |
| if step < len(future_exog): | |
| fe = future_exog.iloc[step] | |
| for c in ['EMA10','EMA50','Volume_fill','sentiment']: | |
| cur[c] = float(fe[c]) | |
| return price_from_returns(float(close.iloc[-1]), preds) | |
| except: | |
| return float(close.iloc[-1]) | |
| if 'RandomForest' in models_sel: | |
| val = forecast_via_tree_return( | |
| lambda: RandomForestRegressor( | |
| n_estimators=200, | |
| max_depth=6, | |
| min_samples_leaf=10, | |
| random_state=0 | |
| ) | |
| ) | |
| results['RandomForest'] = try_float(val) | |
| if 'GradientBoosting' in models_sel: | |
| val = forecast_via_tree_return(lambda: GradientBoostingRegressor()) | |
| results['GradientBoosting'] = try_float(val) | |
| if 'ExtraTrees' in models_sel: | |
| val = forecast_via_tree(lambda: ExtraTreesRegressor(n_estimators=100, random_state=0)) | |
| results['ExtraTrees'] = try_float(val) | |
| if 'ElasticNet' in models_sel: | |
| # ElasticNet needs special instantiation | |
| try: | |
| df_rf = pd.DataFrame({'y': close}) | |
| ex = hist[['EMA10','EMA50','Volume_fill','sentiment']].reset_index(drop=True) | |
| df_rf = df_rf.join(ex) | |
| for lag in range(1, lags+1): | |
| df_rf[f'lag_{lag}'] = df_rf['y'].shift(lag) | |
| df_rf = df_rf.dropna().reset_index(drop=True) | |
| if df_rf.shape[0] < max(10, lags+2): | |
| results['ElasticNet'] = np.nan | |
| else: | |
| X = df_rf.drop(columns=['y']) | |
| y = df_rf['y'] | |
| en = ElasticNet(random_state=0).fit(X, y) | |
| cur = X.iloc[[-1]].copy() | |
| preds = [] | |
| for step in range(days_ahead): | |
| cur = cur[X.columns] | |
| p = float(en.predict(cur)[0]) | |
| preds.append(p) | |
| for i in range(lags, 1, -1): | |
| if f'lag_{i}' in cur.columns and f'lag_{i-1}' in cur.columns: | |
| cur[f'lag_{i}'] = cur[f'lag_{i-1}'] | |
| if 'lag_1' in cur.columns: | |
| cur['lag_1'] = p | |
| if step < len(future_exog): | |
| fe = future_exog.iloc[step] | |
| for col in ['EMA10','EMA50','Volume_fill','sentiment']: | |
| if col in cur.columns: | |
| cur[col] = float(fe[col]) | |
| results['ElasticNet'] = try_float(preds[-1]) | |
| except Exception: | |
| results['ElasticNet'] = np.nan | |
| if 'KNN' in models_sel: | |
| val = forecast_via_tree(lambda: KNeighborsRegressor(n_neighbors=5)) | |
| results['KNN'] = try_float(val) | |
| if 'SVR' in models_sel: | |
| val = forecast_via_tree(lambda: SVR()) | |
| results['SVR'] = try_float(val) | |
| if 'ARIMA' in models_sel: | |
| try: | |
| m = ARIMA(close, order=(5,1,0)).fit() | |
| pred = m.forecast(steps=days_ahead).iloc[-1] | |
| except: | |
| pred = close.iloc[-1] | |
| results['ARIMA'] = fallback(pred, close.iloc[-1]) | |
| if 'ARIMAX' in models_sel: | |
| try: | |
| exog = hist[['EMA10','EMA50','Volume_fill','sentiment']] | |
| m = SARIMAX(close, exog=exog, order=(1,1,1)).fit(disp=False) | |
| future_ex = future_exog[['EMA10','EMA50','Volume_fill','sentiment']] | |
| pred = m.predict(start=len(close), end=len(close)+days_ahead-1, exog=future_ex).iloc[-1] | |
| except: | |
| pred = close.iloc[-1] | |
| results['ARIMAX'] = fallback(pred, close.iloc[-1]) | |
| if 'HoltWinters' in models_sel: | |
| try: | |
| hw = ExponentialSmoothing(close, trend="add", seasonal=None).fit() | |
| hw_pred = hw.forecast(days_ahead) | |
| results['HoltWinters'] = try_float(hw_pred.iloc[-1]) | |
| except Exception: | |
| results['HoltWinters'] = np.nan | |
| if 'LSTM' in models_sel: | |
| if TENSORFLOW_AVAILABLE: | |
| try: | |
| seq = close.values.reshape(-1,1) | |
| scaler = MinMaxScaler() | |
| seq_s = scaler.fit_transform(seq) | |
| # use returns | |
| rets = np.diff(seq_s.squeeze(), prepend=seq_s[0]).reshape(-1,1) | |
| window = min(30, len(seq_s)-1) | |
| Xs, ys = [], [] | |
| for i in range(len(rets)-window): | |
| Xs.append(seq_s[i:i+window]) | |
| ys.append(rets[i+window][0]) | |
| if len(Xs) < 5: | |
| pred = close.iloc[-1] | |
| else: | |
| Xs = np.array(Xs).reshape(-1,window,1) | |
| ys = np.array(ys) | |
| model = Sequential([Input(shape=(window,1)), LSTM(64), Dense(1)]) | |
| model.compile(optimizer='adam', loss='mse') | |
| model.fit(Xs, ys, epochs=5, verbose=0) | |
| cur = seq_s[-window:].reshape(1,window,1) | |
| preds = [] | |
| for _ in range(days_ahead): | |
| p = model.predict(cur, verbose=0)[0,0] | |
| preds.append(p) | |
| cur = np.roll(cur, -1, axis=1) | |
| cur[0,-1,0] = cur[0,-2,0] + p | |
| pred_vals = scaler.inverse_transform(cur.reshape(window,1)).flatten() | |
| pred = pred_vals[-1] | |
| except: | |
| pred = close.iloc[-1] | |
| else: | |
| pred = close.iloc[-1] | |
| results['LSTM'] = fallback(pred, close.iloc[-1]) | |
| # remove models that produced NaN before preparing ensemble | |
| cleaned_results = {k: v for k,v in results.items() if pd.notna(v)} | |
| if len(cleaned_results) == 0: | |
| st.warning("All selected models produced missing predictions (NaN). Try fewer models, smaller lag, or more history.") | |
| else: | |
| # ensemble mean only across cleaned numeric values | |
| if 'EnsembleMean' in models_sel: | |
| numeric_vals = [v for v in cleaned_results.values() if isinstance(v, (int,float,np.floating)) and not np.isnan(v)] | |
| cleaned_results['EnsembleMean'] = float(np.mean(numeric_vals)) if numeric_vals else np.nan | |
| # remove if ensemble becomes NaN | |
| if pd.isna(cleaned_results['EnsembleMean']): | |
| cleaned_results.pop('EnsembleMean', None) | |
| # build dataframe and show | |
| res_df = pd.DataFrame.from_dict(cleaned_results, orient='index', columns=['pred']) | |
| res_df['last_close'] = float(hist['Close'].iloc[-1]) | |
| res_df['delta'] = res_df['pred'] - res_df['last_close'] | |
| # save for side-by-side display later | |
| st.session_state['forecast_results'] = res_df.copy() | |
| # Backtest (1-step walk-forward) | |
| if backtest_run: | |
| st.info("Running backtest β this may take time depending on models & steps.") | |
| close = hist['Close'].copy() | |
| n = len(close) | |
| train_window = int(bt_window) | |
| steps_to_run = int(min(bt_steps, max(0, n - train_window - 1))) | |
| if steps_to_run <= 0: | |
| st.warning(f"Not enough data for requested backtest window/steps. n={n}, train_window={train_window}. Adjust window or steps.") | |
| else: | |
| metrics = {} | |
| progress = st.progress(0) | |
| model_list = [m for m in models_sel] | |
| for mi, model_name in enumerate(model_list): | |
| y_trues = [] | |
| y_preds = [] | |
| for i in range(0, steps_to_run): | |
| train_start = i | |
| train_end = i + train_window | |
| test_idx = train_end | |
| if test_idx >= n: | |
| break | |
| train_series = close.iloc[train_start:train_end].copy() | |
| exog_hist = hist[['EMA10','EMA50','Volume_fill','sentiment']].iloc[train_start:train_end].copy() | |
| yhat = None | |
| try: | |
| if model_name == 'Naive': | |
| yhat = float(train_series.iloc[-1]) | |
| elif model_name == 'SeasonalNaive': | |
| period = 5 if len(train_series)>=5 else 1 | |
| idx = -period if len(train_series)>=period else -1 | |
| yhat = float(train_series.iloc[idx]) | |
| elif model_name == 'MovingAverage': | |
| yhat = float(train_series.rolling(20).mean().iloc[-1]) if len(train_series) >= 20 else float(train_series.mean()) | |
| elif model_name == 'LinearReg': | |
| df_lr = train_series.reset_index(drop=True).reset_index(); df_lr.columns=['t','y'] | |
| lr = LinearRegression().fit(df_lr[['t']].values, df_lr['y'].values) | |
| yhat = float(lr.predict([[len(df_lr)]])[0]) | |
| elif model_name in ('RandomForest','GradientBoosting','ExtraTrees','ElasticNet','KNN','SVR'): | |
| df_sup = create_supervised_features(train_series, lags=int(lags), exog=exog_hist) | |
| if df_sup.shape[0] < max(5, int(lags)+1): | |
| yhat = float(train_series.iloc[-1]) | |
| else: | |
| X_train = df_sup.drop(columns=['y']) | |
| y_train = df_sup['y'].values | |
| if model_name == 'RandomForest': | |
| mdl = RandomForestRegressor(n_estimators=100, random_state=0) | |
| elif model_name == 'GradientBoosting': | |
| mdl = GradientBoostingRegressor(n_estimators=100, random_state=0) | |
| elif model_name == 'ExtraTrees': | |
| mdl = ExtraTreesRegressor(n_estimators=100, random_state=0) | |
| elif model_name == 'ElasticNet': | |
| mdl = ElasticNet(random_state=0) | |
| elif model_name == 'KNN': | |
| mdl = KNeighborsRegressor(n_neighbors=5) | |
| elif model_name == 'SVR': | |
| mdl = SVR() | |
| else: | |
| mdl = RandomForestRegressor(n_estimators=100, random_state=0) | |
| mdl.fit(X_train, y_train) | |
| last_lags = train_series.iloc[-int(lags):].values[::-1] if len(train_series)>=int(lags) else np.concatenate([np.full(int(lags)-len(train_series), train_series.iloc[0]), train_series.values]) | |
| X_test_row = {} | |
| for col in X_train.columns: | |
| if col.startswith("lag_"): | |
| idx = int(col.split("_")[1]) - 1 | |
| X_test_row[col] = float(last_lags[idx]) if idx < len(last_lags) else float(last_lags[-1]) | |
| else: | |
| try: | |
| next_exog = hist[['EMA10','EMA50','Volume_fill','sentiment']].iloc[train_end] | |
| X_test_row[col] = float(next_exog[col]) | |
| except Exception: | |
| X_test_row[col] = float(exog_hist.iloc[-1][col]) if col in exog_hist.columns else 0.0 | |
| X_test_df = pd.DataFrame([X_test_row], columns=X_train.columns) | |
| yhat = float(mdl.predict(X_test_df)[0]) | |
| elif model_name == 'ARIMA': | |
| try: | |
| m = ARIMA(train_series, order=(5,1,0)).fit() | |
| yhat = float(m.forecast(steps=1).iloc[0]) | |
| except Exception: | |
| yhat = float(train_series.iloc[-1]) | |
| elif model_name == 'ARIMAX': | |
| try: | |
| ex = exog_hist | |
| m = SARIMAX(train_series, exog=ex, order=(1,1,1)).fit(disp=False) | |
| try: | |
| next_ex = hist[['EMA10','EMA50','Volume_fill','sentiment']].iloc[train_end].values.reshape(1,-1) | |
| except Exception: | |
| next_ex = ex.iloc[[-1]].values.reshape(1,-1) | |
| yhat = float(m.predict(start=len(train_series), end=len(train_series), exog=next_ex).iloc[0]) | |
| except Exception: | |
| yhat = float(train_series.iloc[-1]) | |
| elif model_name == 'HoltWinters': | |
| try: | |
| m = ExponentialSmoothing(train_series, trend="add", seasonal=None).fit() | |
| yhat = float(m.forecast(1).iloc[0]) | |
| except Exception: | |
| yhat = float(train_series.iloc[-1]) | |
| elif model_name == 'LSTM': | |
| if TENSORFLOW_AVAILABLE: | |
| try: | |
| seq = train_series.values.reshape(-1,1) | |
| scaler = MinMaxScaler() | |
| seq_s = scaler.fit_transform(seq) | |
| window = min(30, len(seq_s)-1) | |
| Xs, ys = [], [] | |
| for k in range(len(seq_s)-window): | |
| Xs.append(seq_s[k:k+window]) | |
| ys.append(seq_s[k+window][0]) | |
| if len(Xs) < 5: | |
| yhat = float(train_series.iloc[-1]) | |
| else: | |
| Xs = np.array(Xs).reshape(-1,window,1) | |
| ys = np.array(ys) | |
| m = Sequential([Input(shape=(window,1)), LSTM(32), Dense(1)]) | |
| m.compile(optimizer='adam', loss='mse') | |
| m.fit(Xs, ys, epochs=3, verbose=0) | |
| last_win = seq_s[-window:].reshape(1,window,1) | |
| p = m.predict(last_win, verbose=0)[0,0] | |
| yhat = float(scaler.inverse_transform(np.array([[p]]))[0,0]) | |
| except Exception: | |
| yhat = float(train_series.iloc[-1]) | |
| else: | |
| yhat = float(train_series.iloc[-1]) | |
| else: | |
| yhat = float(train_series.iloc[-1]) | |
| except Exception: | |
| yhat = float(train_series.iloc[-1]) | |
| y_trues.append(float(close.iloc[test_idx])) | |
| y_preds.append(float(yhat)) | |
| if len(y_trues) > 0: | |
| metrics[model_name] = evaluate_preds(y_trues, y_preds) | |
| else: | |
| metrics[model_name] = {"MAE": np.nan, "RMSE": np.nan, "MAPE": np.nan} | |
| progress.progress(int((mi+1)/max(1,len(model_list))*100)) | |
| st.write(f"Backtested {model_name} β points={len(y_trues)}") | |
| # filter out models where metrics are NaN (no numeric MAE) | |
| metrics_clean = {k:v for k,v in metrics.items() if pd.notna(v.get("MAE", np.nan))} | |
| if len(metrics_clean) == 0: | |
| st.warning("No backtest results (all models had insufficient data).") | |
| else: | |
| metrics_df = pd.DataFrame(metrics_clean).T | |
| st.subheader("Backtest Results (1-step walk-forward)") | |
| st.dataframe(metrics_df) | |
| # BEST MODELS (rank by MAE, tie-break RMSE) | |
| ranked = metrics_df.sort_values(by=["MAE","RMSE"]) | |
| best = ranked.head(10) | |
| # save for joint view | |
| st.session_state['best_backtest'] = best.copy() | |
| st.caption( | |
| "Models ranked primarily by MAE (accuracy). " | |
| "RMSE is used as a tie-breaker." | |
| ) | |
| # --- SIDE-BY-SIDE SUMMARY --- | |
| if 'forecast_results' in st.session_state or 'best_backtest' in st.session_state: | |
| st.markdown("## π Forecast vs Backtest β quick comparison") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.subheader(" Forecast Results") | |
| if 'forecast_results' in st.session_state: | |
| fr = st.session_state['forecast_results'].copy() | |
| def arrow_html(x): | |
| if pd.isna(x): | |
| return "β" | |
| if x > 0: | |
| return f"<span class='arrow-up'>β² {x:.2f}</span>" | |
| if x < 0: | |
| return f"<span class='arrow-down'>βΌ {abs(x):.2f}</span>" | |
| return f"{x:.2f}" | |
| html = """ | |
| <style> | |
| table {width:100%; border-collapse:collapse;} | |
| th, td {padding:6px 8px; border-bottom:1px solid #333;} | |
| .arrow-up {color:#2ecc71; font-weight:600;} | |
| .arrow-down {color:#e74c3c; font-weight:600;} | |
| th {text-align:left;} | |
| </style> | |
| """ | |
| html += "<table><tr><th>Model</th><th style='text-align:right'>Prediction</th><th style='text-align:right'>Delta vs last</th></tr>" | |
| for idx, row in fr.iterrows(): | |
| pred = f"{row['pred']:.2f}" if not pd.isna(row['pred']) else "β" | |
| html += ( | |
| f"<tr>" | |
| f"<td>{idx}</td>" | |
| f"<td style='text-align:right'>{pred}</td>" | |
| f"<td style='text-align:right'>{arrow_html(row['delta'])}</td>" | |
| f"</tr>" | |
| ) | |
| html += "</table>" | |
| st.markdown(html, unsafe_allow_html=True) | |
| else: | |
| st.info("Run a forecast first.") | |
| with col2: | |
| st.subheader(" Best Backtest Models") | |
| if 'best_backtest' in st.session_state: | |
| st.table(st.session_state['best_backtest']) | |
| else: | |
| st.info("Run the backtest to see best models.") | |
| st.markdown("---") | |
| st.caption("Notes: This app removes any models that return missing predictions (NaN) from final tables and ensembles. If many models are removed, reduce lag/backtest window or increase historical data. FinBERT/TF are optional and may slow startup.") |