Spaces:
Runtime error
Runtime error
| import streamlit as st | |
| import os | |
| # --- SAFETY FIX 1: Map HF_TOKEN to the variable litellm expects --- | |
| # This prevents "Auth Error" even if the library ignores the passed key | |
| if "HF_TOKEN" in os.environ: | |
| os.environ["HUGGINGFACE_API_KEY"] = os.environ["HF_TOKEN"] | |
| from smolagents import CodeAgent, LiteLLMModel, tool | |
| import yfinance as yf | |
| import pandas_ta as ta | |
| import pandas as pd | |
| import numpy as np | |
| from prophet import Prophet | |
| from ddgs import DDGS | |
| import datetime | |
| # --- 1. SETUP PAGE CONFIG --- | |
| st.set_page_config(page_title="Indian Stock Analyst AI", layout="wide") | |
| # --- 2. TOOLS --- | |
| def get_deep_financials(ticker: str) -> dict: | |
| """ | |
| Fetches deep financial metrics: Valuation, Fundamentals, Shareholding, and Technicals. | |
| Args: | |
| ticker: The stock symbol (e.g., 'TATAMOTORS.NS'). | |
| """ | |
| try: | |
| stock = yf.Ticker(ticker) | |
| info = stock.info | |
| hist = stock.history(period="1y") | |
| if hist.empty: return {"error": "No data found"} | |
| # Technicals | |
| cmp = hist['Close'].iloc[-1] | |
| high_52 = hist['High'].max() | |
| low_52 = hist['Low'].min() | |
| drawdown = ((cmp - high_52) / high_52) * 100 | |
| hist.ta.rsi(length=14, append=True) | |
| rsi = hist['RSI_14'].iloc[-1] if 'RSI_14' in hist else 50 | |
| # Returns | |
| ret_1y = ((cmp - hist['Close'].iloc[0]) / hist['Close'].iloc[0]) * 100 | |
| ret_6m = ((cmp - hist['Close'].iloc[-126]) / hist['Close'].iloc[-126]) * 100 if len(hist) > 126 else 0 | |
| # Valuation | |
| eps = info.get('trailingEps', 0) | |
| book_val = info.get('bookValue', 0) | |
| graham_num = np.sqrt(22.5 * eps * book_val) if (eps > 0 and book_val > 0) else 0 | |
| # Shareholding | |
| promoters = info.get('heldPercentInsiders', 0) * 100 | |
| institutions = info.get('heldPercentInstitutions', 0) * 100 | |
| public = 100 - (promoters + institutions) | |
| def safe(key, default="N/A"): return info.get(key, default) | |
| return { | |
| "ticker": ticker, | |
| "cmp": round(cmp, 2), | |
| "high_52": round(high_52, 2), | |
| "low_52": round(low_52, 2), | |
| "drawdown": round(drawdown, 2), | |
| "mkt_cap_cr": round(safe('marketCap', 0) / 10000000, 2), | |
| "rsi": round(rsi, 2), | |
| "ret_1y": round(ret_1y, 2), | |
| "ret_6m": round(ret_6m, 2), | |
| "pe": safe('trailingPE'), | |
| "peg": safe('pegRatio'), | |
| "pb": safe('priceToBook'), | |
| "div_yield": safe('dividendYield', 0) * 100 if safe('dividendYield') else 0, | |
| "roe": round(safe('returnOnEquity', 0) * 100, 2), | |
| "roce_proxy": round(safe('returnOnAssets', 0) * 100, 2), | |
| "eps": eps, | |
| "book_value": book_val, | |
| "graham_number": round(graham_num, 2), | |
| "promoters": round(promoters, 2), | |
| "institutions": round(institutions, 2), | |
| "public": round(public, 2), | |
| "sector": safe('sector', 'Unknown'), | |
| "desc": safe('longBusinessSummary', '')[:300] | |
| } | |
| except Exception as e: | |
| return {"error": str(e)} | |
| def get_market_news(query: str) -> str: | |
| """ | |
| Searches for broad market news, including results, mergers, deals, and acquisitions. | |
| Args: | |
| query: The search term (e.g. 'HDFC Bank'). | |
| """ | |
| try: | |
| search_query = f"{query} latest stock news business mergers acquisitions deals" | |
| results = DDGS().text(search_query, max_results=6) | |
| return "\n".join([f"- {r['title']}: {r['body']}" for r in results]) if results else "No news found." | |
| except Exception as e: | |
| return f"Error fetching news: {e}" | |
| def get_price_prediction(ticker: str) -> dict: | |
| """ | |
| Runs Prophet to predict trend for the next 30 days. | |
| Args: | |
| ticker: The stock symbol. | |
| """ | |
| try: | |
| stock = yf.Ticker(ticker) | |
| df = stock.history(period="2y").reset_index()[['Date', 'Close']] | |
| if df.empty: return {"error": "Insufficient data"} | |
| df.columns = ['ds', 'y'] | |
| df['ds'] = df['ds'].dt.tz_localize(None) | |
| m = Prophet(daily_seasonality=True) | |
| m.fit(df) | |
| future = m.make_future_dataframe(periods=30) | |
| forecast = m.predict(future) | |
| future_price = forecast.iloc[-1]['yhat'] | |
| curr = df.iloc[-1]['y'] | |
| return {"current": round(curr, 2), "pred_30d": round(future_price, 2), "trend": "Bullish" if future_price > curr else "Bearish"} | |
| except Exception as e: | |
| return {"error": str(e)} | |
| # --- 3. DEFINE AGENT --- | |
| # Use LiteLLMModel to connect to Hugging Face's free API via their Qwen provider | |
| # We use Qwen2.5-Coder because it is free on HF, fast, and excellent at following agent instructions. | |
| model = LiteLLMModel( | |
| model_id="huggingface/Qwen/Qwen2.5-Coder-32B-Instruct", | |
| api_key=os.getenv("HF_TOKEN") # Uses the secret you set earlier | |
| ) | |
| # Note: For LiteLLM with HF, sometimes just model_id="huggingface/Qwen/..." works too, | |
| # but the above is safer for specific endpoints. | |
| agent = CodeAgent( | |
| tools=[get_deep_financials, get_market_news, get_price_prediction], | |
| model=model, | |
| add_base_tools=True | |
| ) | |
| STRICT_TEMPLATE = """ | |
| You must generate the output EXACTLY in this format. Do not change the headers. | |
| # {TICKER} | Investment Thesis | |
| [Insert Date] | |
| **Company Description** | |
| [Insert 2 sentence description] | |
| **Current Stock Performance** | |
| π **Current Market Price (CMP)**: {CURRENCY} [CMP] π **52-Week High**: {CURRENCY} [High] π **52-Week Low**: {CURRENCY} [Low] | |
| π **Drawdown from 52-Week High**: [Drawdown]% π **Market Cap**: [MCap] Cr | |
| π **Recent Performance**: [1 sentence summary of recent movement] | |
| π **1. Financial Performance** | |
| * **Revenue/Profit**: [Mention recent Revenue/Profit trends from news if available, else general health] | |
| * **Margins**: [Mention if margins are stable/improving based on news] | |
| * **EPS (TTM)**: {CURRENCY} [EPS] | |
| π **2. Stock Performance** | |
| * **1-Year Return**: [1Y Ret]% | |
| * **6-Month Return**: [6M Ret]% | |
| * **Prediction Model (30-Day)**: Predicted {CURRENCY} [Pred Price] ([Trend]) | |
| π **3. Technical Snapshot** | |
| * **Trading Range**: {CURRENCY} [Low_52] β {CURRENCY} [High_52] | |
| * **RSI**: [RSI] ([Interpret: Overbought > 70, Neutral 30-70, Oversold < 30]) | |
| * **Trend**: [Bullish/Bearish/Consolidating] | |
| π§Ύ **4. Valuation & Fundamentals** | |
| * **P/E Ratio**: [PE] | **Industry P/E**: [Estimate or N/A] | |
| * **PEG Ratio**: [PEG] | |
| * **P/B Ratio**: [PB] | |
| * **Dividend Yield**: [Div Yield]% | |
| * **ROE**: [ROE]% | **ROCE**: [ROCE_Proxy]% | |
| * **Verdict**: [Undervalued/Overvalued/Fairly Priced] | |
| π§βπΌ **5. Shareholding Pattern** | |
| * **Promoters**: [Promoter]% | |
| * **Institutions (FII/DII)**: [Inst]% | |
| * **Public**: [Public]% | |
| π‘ **Intrinsic Value Analysis** | |
| * **Book Value**: {CURRENCY} [BV] | |
| * **Graham Number**: {CURRENCY} [Graham Num] | |
| * **Valuation Check**: CMP is [Above/Below] Graham Number. | |
| π§ **Investment Rationale** | |
| * **Technical Setup**: [Comment on RSI/Trend] | |
| * **Fundamental View**: [Comment on PE/ROE] | |
| * **Growth Catalysts**: [Infer from News] | |
| * **Risks**: [Infer from News/Volatility] | |
| π― **Final Verdict**: [BUY / SELL / HOLD] | |
| [1-2 sentence final conclusion] | |
| --- | |
| **Recent News** | |
| [Bullet points of news] | |
| """ | |
| # --- 4. STREAMLIT UI --- | |
| st.title("AI Stock Analyst Agent") | |
| st.markdown("Generates deep investment thesis reports for Indian Stocks.") | |
| ticker_input = st.text_input("Enter Stock Symbol", placeholder="e.g. TATAMOTORS, HDFCBANK, INFY") | |
| if st.button("Generate Report"): | |
| if not ticker_input: | |
| st.warning("Please enter a stock symbol.") | |
| else: | |
| with st.spinner(f"Analyzing {ticker_input}... this may take 30-60 seconds."): | |
| try: | |
| # Logic to handle tickers | |
| clean_ticker = ticker_input.upper().strip() | |
| if clean_ticker in ["TATAMOTORS", "RELIANCE", "HDFCBANK", "INFY", "ITC", "SBI", "SBIN"]: | |
| ticker = f"{clean_ticker}.NS" | |
| currency = "Rs." | |
| elif any(x in clean_ticker for x in [".NS", ".BO"]): | |
| ticker = clean_ticker | |
| currency = "Rs." | |
| else: | |
| ticker = f"{clean_ticker}.NS" if clean_ticker.isalpha() else clean_ticker | |
| currency = "Rs." | |
| prompt = f""" | |
| You are a professional Financial Analyst. | |
| 1. Call 'get_deep_financials' for '{ticker}'. | |
| 2. Call 'get_price_prediction' for '{ticker}'. | |
| 3. Call 'get_market_news' for '{ticker}'. | |
| Using ALL this data, fill out the following template. | |
| TEMPLATE TO FILL: | |
| {STRICT_TEMPLATE} | |
| Notes: | |
| - Replace {{TICKER}} with {ticker}. | |
| - Replace {{CURRENCY}} with {currency}. | |
| """ | |
| response = agent.run(prompt) | |
| st.markdown(str(response)) | |
| except Exception as e: | |
| st.error(f"An error occurred: {str(e)}") |