Stock-Agent / src /app.py
suvinava's picture
Update src/app.py
9f06c0e verified
Raw
History Blame Contribute Delete
9.16 kB
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 ---
@tool
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)}
@tool
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}"
@tool
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)}")