Spaces:
Running
Running
Ramkumar Shanmugam
feat: handle non-trading days, track csv upload time, and refine news headlines
2cd125d | """Module for interacting with the Gemini API for AI and Search Grounding.""" | |
| import os | |
| import json | |
| import time | |
| from datetime import datetime | |
| import google.generativeai as genai | |
| from dotenv import load_dotenv | |
| # 1. Load the GEMINI_API_KEY from the .env file | |
| load_dotenv() | |
| GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") | |
| if GEMINI_API_KEY: | |
| genai.configure(api_key=GEMINI_API_KEY) | |
| def build_tcr_prompt(portfolio_text, today_date, strategy_context): | |
| """ | |
| Builds the PortIQ persona prompt containing the portfolio and the specific JSON output structure. | |
| """ | |
| return f""" | |
| You are PortIQ (Portfolio Intelligence), a personal Indian stock portfolio advisor for Ram. | |
| Ram is a disciplined SIP investor who adds 1-2 qty per day with a long-term perspective. | |
| Today's date: {today_date} | |
| Strategy Context (IMPORTANT — read carefully): | |
| {strategy_context} | |
| NOTE: A "PRICE DIP ALERT" above only means the stock's current price is significantly below Ram's average cost. | |
| It is NOT a buy recommendation. You MUST do a full fundamental and news check before confirming any BUY signal. | |
| Portfolio: | |
| {portfolio_text} | |
| Instructions: | |
| 1. Search for real-time news for EVERY stock in the portfolio (no stock should be skipped). | |
| 2. For each stock, find the most recent and relevant news. Each headline must be between 60–80 words — complete and informative. Include: (a) what exactly happened, (b) why it matters for this stock, and (c) its direct impact on Ram's portfolio position. Do NOT write vague one-liners. | |
| 3. Assign each news item a global rank (1 = most important across entire portfolio) based on urgency and portfolio impact. | |
| 4. Assign an impact level: "Critical" (market-moving, act-now), "High" (significant, watch closely), "Medium" (notable but not urgent), "Low" (minor or long-term). | |
| 5. Search for today's Nifty, Sensex, crude oil price, and FII/DII data. | |
| 6. Provide a global & domestic news summary (max 150 words total) explaining today's key news and how it impacts Ram's portfolio. Use simple, easily understandable words and present them as a list of bullet points. | |
| ⚠️ CRITICAL — FALLING KNIFE GATE (apply this BEFORE assigning BUY or top_picks): | |
| Ram's biggest risk is blindly adding to stocks just because they are below his average cost. | |
| Before signaling BUY or including a stock in top_picks, you MUST search and evaluate ALL the following using Google Search: | |
| a) VALUATION METRICS — Is the stock cheap or expensive at current price? | |
| - Stock P/E vs Industry P/E: If Stock P/E > Industry P/E → overvalued, be cautious. | |
| Industry benchmarks: Banking ~12-15 | IT ~25-30 | Pharma ~25-35 | FMCG ~45-55 | Auto ~20-25 | Infra ~15-20 | |
| - Price to Book Value (P/B): Compares price to net assets. | |
| Banking: P/B <1.5 is attractive | Manufacturing: <3 is fair | IT/Pharma: <5 is acceptable | |
| If P/B is extremely high with no earnings growth → dangerous. | |
| - EPS (Earnings Per Share): Is EPS growing YoY? Shrinking or negative EPS → serious red flag. | |
| - Dividend Yield: >2% indicates a mature, cash-generating business. Bonus factor for BUY. | |
| - 52-Week High/Low: If price is near 52-week low → check WHY (dip or structural decline?). | |
| If near 52-week high → don't chase; wait for pullback. | |
| b) PROFITABILITY METRICS — Is this a good quality business? | |
| - ROE (Return on Equity): >15% is good | >20% is excellent | <12% → weak management efficiency. | |
| Exception: Capital-intensive sectors (Infra, Metals) can have lower ROE (10%+) acceptably. | |
| - ROCE (Return on Capital Employed): >15% is healthy | >20% is strong. | |
| For Banking stocks use ROA (Return on Assets) >1% instead of ROCE. | |
| - OPM (Operating Profit Margin): Measures operational efficiency. | |
| IT: >20% | Pharma: >18% | FMCG: >15% | Auto: >8% | Infra: >12% | Banking: N/A (use NIM >3%) | |
| Declining OPM over 3 quarters → cost pressure, be cautious. | |
| - Net Profit: Is absolute net profit growing YoY? Declining net profit = deteriorating business. | |
| - Profit Var 3Yrs (3-Year Profit Growth): >10% CAGR → healthy and compounding. | |
| <5% or negative → stagnant or declining business, not a BUY candidate. | |
| - Qtr Profit Var (Latest Quarter Profit Change): Positive → momentum building. | |
| Negative for 2+ consecutive quarters → deteriorating fundamentals, strong AVOID signal. | |
| - Qtr Sales Var (Latest Quarter Revenue Change): Positive → demand growing. | |
| Negative → top-line pressure, investigate before buying. | |
| c) FINANCIAL SAFETY METRICS — Is the company financially stable? | |
| - Debt to Equity (D/E): <0.5 is excellent (low leverage) | 0.5-1 is manageable | >1.5 is high risk. | |
| Exception: Banking & NBFC stocks naturally carry high leverage; use NPA ratio and CAR instead. | |
| Infrastructure stocks can have D/E up to 2 if cash flows are stable. | |
| - Promoter Holding: >50% is positive (founders have skin in the game). | |
| Promoter holding DECLINING quarter over quarter → major red flag, signals insider exit. | |
| Pledged promoter holding >30% → financial stress signal, be very cautious. | |
| - Market Cap: Large cap (>₹20,000 Cr.) → more stable, lower risk. | |
| Mid cap (₹5,000-20,000 Cr.) → higher growth potential but more volatile. | |
| Small cap (<₹5,000 Cr.) → high risk, only buy on very strong fundamentals. | |
| d) TECHNICAL / MOMENTUM SIGNALS: | |
| - RSI (Relative Strength Index): | |
| RSI <30 → oversold, potential reversal zone (supports BUY if fundamentals are good) | |
| RSI 30-50 → neutral, accumulation zone | |
| RSI >70 → overbought, DO NOT add; wait for pullback | |
| - Price vs 52-week High: If stock is >40% below its 52-week high → investigate structural reasons. | |
| e) ROOT CAUSE CHECK — Search WHY the stock is below Ram's avg cost. | |
| Is it: (i) Temporary macro/sector correction or broad market fall → possible dip buy opportunity | |
| (ii) Earnings miss, revenue decline, rising debt, management change, regulatory issues, | |
| sector-level disruption → FALLING KNIFE, do NOT recommend BUY. | |
| f) QUALITY GATE — Only signal BUY if the stock clears ALL of: | |
| - Valuation: Stock P/E is at or below Industry P/E (or justified by growth) | |
| - Profitability: ROE >12%, ROCE >12%, OPM stable or improving | |
| - Safety: D/E <1 (or sector-appropriate), Promoter holding stable/increasing | |
| - Momentum: RSI not overbought; Qtr Profit Var and Qtr Sales Var not both negative | |
| - Narrative: Reason for decline is temporary, not fundamental deterioration | |
| - News: Neutral or positive sentiment, no major red flags | |
| If the stock fails 2 or more checks → signal WATCH (monitor for recovery) or AVOID. | |
| g) RANKING RULE for top_picks — DO NOT rank on negative P&L% alone. Rank on: | |
| (1) Fundamental quality composite: ROE + ROCE + OPM + Profit growth (higher = rank higher) | |
| (2) Valuation attractiveness: Stock P/E vs Industry P/E (more undervalued = rank higher) | |
| (3) Financial safety: D/E, Promoter holding stability | |
| (4) Reason for dip: Temporary > Structural | |
| (5) Technical: RSI in buy zone (<50 preferred) | |
| (6) News sentiment and near-term catalysts | |
| (7) Only last: Price vs avg cost as a tiebreaker — never the primary factor. | |
| 7. Respond ONLY in valid JSON with this exact structure (no markdown, no extra text): | |
| {{ | |
| "mood": {{ | |
| "market": "Bullish/Bearish/Cautious/Neutral", | |
| "nifty_call": "gap up/flat/gap down", | |
| "key_driver": "one short phrase", | |
| "tcr_mood": "Chai sip day/Active day/Wait and watch" | |
| }}, | |
| "news_summary": [ | |
| "Simple bullet point summarizing key news (domestic/global) and its specific impact on the portfolio." | |
| ], | |
| "portfolio": [ | |
| {{"symbol": "X", "signal": "BUY/HOLD/AVOID/WATCH", "signal_reason": "one sentence"}} | |
| ], | |
| "top_picks": [ | |
| {{ | |
| "rank": 1, | |
| "symbol": "X", | |
| "action": "Add 1 qty", | |
| "reason": "2-3 sentences on why this is a quality stock worth adding now.", | |
| "why_now": "Specific trigger — why today, not just because it is below avg cost.", | |
| "fundamental_verdict": "Strong/Moderate/Weak — based on PE, ROE, ROCE, D/E searched.", | |
| "risk": "One sentence on the key risk if this call goes wrong." | |
| }} | |
| ], | |
| "avoid_today": [ | |
| {{"symbol": "X", "reason": "one sentence"}} | |
| ], | |
| "market_summary": [ | |
| {{"label": "Key event", "value": "description"}} | |
| ], | |
| "news": [ | |
| {{ | |
| "rank": 1, | |
| "symbol": "X", | |
| "headline": "60–80 words. State clearly: (1) what happened — the actual event or development, (2) why it matters for this specific stock — fundamental, regulatory, or macro reason, (3) what it means for Ram's position — should he be concerned, watch, or is this a positive catalyst? Be specific, not vague.", | |
| "sentiment": "positive/negative/neutral", | |
| "impact": "Critical/High/Medium/Low" | |
| }} | |
| ], | |
| "dividends": [ | |
| {{"symbol": "X", "amount": "₹X per share", "record_date": "date or upcoming"}} | |
| ] | |
| }} | |
| """ | |
| def call_gemini(prompt, api_key=None): | |
| """ | |
| Calls the Gemini API. | |
| Includes a single retry after 3 seconds on failure. | |
| """ | |
| active_key = api_key or GEMINI_API_KEY | |
| if not active_key or active_key == "your_gemini_api_key_here": | |
| raise ValueError("Valid GEMINI_API_KEY not found in .env file or Settings.") | |
| try: | |
| model = genai.GenerativeModel( | |
| model_name='gemini-2.5-flash', | |
| tools='google_search', | |
| generation_config=genai.types.GenerationConfig(temperature=0.3) | |
| ) | |
| except Exception as setup_err: | |
| print(f"Warning: Tool configuration error during setup: {setup_err}. Falling back to default model config.") | |
| model = genai.GenerativeModel( | |
| model_name='gemini-2.5-flash', | |
| generation_config=genai.types.GenerationConfig(temperature=0.3) | |
| ) | |
| # Isolated Client Configuration (Thread-safe for dynamic/custom keys in multi-user Streamlit) | |
| import google.generativeai.client as client | |
| mgr = client._ClientManager() | |
| mgr.configure(api_key=active_key) | |
| model._client = mgr.get_default_client("generative") | |
| for attempt in range(2): | |
| try: | |
| response = model.generate_content(prompt) | |
| return response.text | |
| except Exception as e: | |
| if attempt == 0: | |
| print(f"API Call failed on first attempt: {e}. Retrying in 3 seconds...") | |
| time.sleep(3) | |
| else: | |
| print(f"API Call failed on second attempt: {e}") | |
| return None | |
| return None | |
| def parse_response(raw_text): | |
| """ | |
| Cleans up the raw JSON string returned by the AI and parses it into a Python dict. | |
| """ | |
| if not raw_text: | |
| return None | |
| text = raw_text.strip() | |
| # Strip markdown code blocks if the AI included them despite instructions | |
| if text.startswith("```json"): | |
| text = text[7:] | |
| elif text.startswith("```"): | |
| text = text[3:] | |
| if text.endswith("```"): | |
| text = text[:-3] | |
| text = text.strip() | |
| try: | |
| data = json.loads(text) | |
| return data | |
| except json.JSONDecodeError as e: | |
| print(f"Failed to parse JSON response: {e}") | |
| print(f"Raw text was:\n{text}") | |
| return None | |
| def enrich_with_signal_prices(data, portfolio_df): | |
| """ | |
| Enriches the daily report data dictionary with _signal_prices by mapping | |
| each signal in data["portfolio"] to the stock's LTP and quantity, | |
| along with the Nifty 50 level from the market summary. | |
| """ | |
| if not data or portfolio_df is None or portfolio_df.empty: | |
| return data | |
| import re | |
| # 1. Parse Nifty 50 on the day | |
| nifty_on_day = None | |
| market_summary = data.get("market_summary", []) | |
| for item in market_summary: | |
| label = item.get("label", "").lower() | |
| if "nifty 50" in label or "nifty50" in label: | |
| raw = item.get("value", "").replace(",", "") | |
| match = re.search(r"[\d]+\.?\d*", raw) | |
| if match: | |
| try: | |
| nifty_on_day = float(match.group()) | |
| except ValueError: | |
| pass | |
| break | |
| # 2. Build maps for LTP and Qty | |
| ltp_map = {} | |
| qty_map = {} | |
| for _, row in portfolio_df.iterrows(): | |
| sym = row.get("symbol") | |
| if sym: | |
| ltp_map[sym] = float(row.get("ltp") or 0) | |
| qty_map[sym] = float(row.get("qty") or 0) | |
| # 3. Build signal prices dict | |
| signal_prices = {} | |
| portfolio_signals = data.get("portfolio", []) | |
| for entry in portfolio_signals: | |
| sym = entry.get("symbol") | |
| sig = entry.get("signal", "") | |
| if sym and sig in ["BUY", "AVOID", "HOLD", "WATCH"] and sym in ltp_map and ltp_map[sym] > 0: | |
| signal_prices[sym] = { | |
| "signal": sig, | |
| "price_on_day": ltp_map[sym], | |
| "qty": qty_map.get(sym, 1.0), | |
| "nifty_on_day": nifty_on_day | |
| } | |
| data["_signal_prices"] = signal_prices | |
| return data | |
| try: | |
| import streamlit as st | |
| cache_decorator = st.cache_data(ttl=1800) | |
| except ImportError: | |
| from functools import lru_cache | |
| cache_decorator = lru_cache(maxsize=16) | |
| def run_analysis(portfolio_text, strategy_context="No specific strategy context provided.", api_key=None): | |
| """ | |
| Orchestrates the analysis flow: | |
| Builds the prompt, calls Gemini, and parses the response. | |
| Returns (data_dict, error_message) tuple. error_message is None on success. | |
| """ | |
| try: | |
| today_date = datetime.now().strftime("%Y-%m-%d") | |
| prompt = build_tcr_prompt(portfolio_text, today_date, strategy_context) | |
| raw_response = call_gemini(prompt, api_key=api_key) | |
| if not raw_response: | |
| return None, "Failed to get a valid response from the Gemini API." | |
| parsed_data = parse_response(raw_response) | |
| if not parsed_data: | |
| return None, "Failed to parse the JSON response from Gemini." | |
| return parsed_data, None | |
| except Exception as e: | |
| return None, f"Analysis failed with error: {e}" | |
| def run_chat_assistant(query, chat_history, active_date, active_report_data, active_portfolio_df, api_key=None): | |
| """ | |
| Runs the chat assistant to answer user questions about the dashboard data. | |
| """ | |
| try: | |
| portfolio_text = "" | |
| if active_portfolio_df is not None and not active_portfolio_df.empty: | |
| portfolio_text = active_portfolio_df.to_string(index=False) | |
| report_text = "" | |
| if active_report_data: | |
| report_copy = active_report_data.copy() | |
| report_copy.pop("_portfolio_snapshot", None) | |
| report_text = json.dumps(report_copy, indent=2) | |
| history_text = "" | |
| for msg in chat_history: | |
| role = "User" if msg["role"] == "user" else "Assistant (PortIQ AI)" | |
| history_text += f"{role}: {msg['content']}\n" | |
| prompt = f""" | |
| You are PortIQ (Portfolio Intelligence), Ram's personal Indian stock portfolio AI advisor. | |
| Ram is asking you questions about the stock market dashboard and briefing showing for the date: {active_date}. | |
| Here is the current dashboard data showing on Ram's screen for {active_date}: | |
| --- Portfolio Snapshot --- | |
| {portfolio_text} | |
| --- Briefing Analysis (JSON) --- | |
| {report_text} | |
| --- Chat History --- | |
| {history_text} | |
| Ram's New Question: | |
| {query} | |
| Instructions: | |
| 1. Answer Ram's question clearly, concisely, and with high financial intelligence. | |
| 2. Reference the specific data shown on the dashboard (e.g., signals, news, top picks, metrics) to explain your reasoning. | |
| 3. If Ram asks about a stock suggestion (e.g. why buy, why avoid, or concerns about metrics like PE ratio), explain the context. If the dashboard suggested buying a stock, explain the reasoning from the "top_picks" or "portfolio" signal reasons, but also think critically about Ram's concerns (like a high PE ratio). Acknowledge their concern and provide a balanced view (e.g., high PE can indicate growth expectation, or high quality, or could indeed be a risk for short-term). | |
| 4. Keep the tone helpful, professional, yet conversational (like a friendly but sharp investment buddy). | |
| 5. Do NOT output JSON. Output regular formatting with paragraphs, bullet points, or simple markdown. Avoid verbose essays; keep it readable. | |
| 6. If the question requires current stock prices, PE ratios, or latest news not present in the dashboard, use your Google Search tool to fetch it. | |
| """ | |
| raw_response = call_gemini(prompt, api_key=api_key) | |
| if not raw_response: | |
| return "Failed to get a valid response from the Gemini API." | |
| return raw_response | |
| except Exception as e: | |
| return f"Chat assistant encountered an error: {e}" | |
| def build_recommendations_prompt(portfolio_symbols: list[str], today_date: str) -> str: | |
| excluded_str = ", ".join(portfolio_symbols) if portfolio_symbols else "None" | |
| return f""" | |
| You are PortIQ (Portfolio Intelligence), an expert Indian stock market analyst and portfolio advisor. | |
| Today's date: {today_date} | |
| The user is looking for new investment opportunities that are NOT in their current portfolio. | |
| Here are the stock symbols currently in the user's portfolio: {excluded_str} | |
| Your task is to recommend the top 5 high-quality stocks listed on the NSE/BSE that are NOT in the list above. | |
| These recommendations must strictly follow the fundamental valuation strategy. | |
| Instructions: | |
| 1. Search the web for today's top Indian stock market ideas, sector performance, and potential bargains. | |
| 2. Filter out any stock that is in the excluded list: {excluded_str}. | |
| 3. Apply the following VALUATION STRATEGY rules to select the top 5 recommendations: | |
| - VALUATION: The stock should be cheap or reasonably valued. Stock P/E should be close to or below its industry benchmark (e.g., Banking ~12-15 | IT ~25-30 | Pharma ~25-35 | FMCG ~45-55 | Auto ~20-25 | Infra ~15-20). The Price-to-Book (P/B) ratio should be attractive. | |
| - PROFITABILITY: The company must be highly profitable. ROE > 15% and ROCE > 15% (or ROA > 1% for banks). Operating Profit Margin (OPM) must be stable or improving. | |
| - FINANCIAL SAFETY: Low leverage. Debt-to-Equity (D/E) should be < 0.5 (or < 1.0, and up to 2.0 for infrastructure). Promoter holding should ideally be > 50% and stable. | |
| - MOMENTUM: Technical RSI should be in a healthy accumulation zone (< 60 preferred, not overbought). | |
| - RECENT NEWS: Ensure there are no major governance, regulatory, or operational red flags in the news today. | |
| 4. Respond ONLY in valid JSON with this exact structure (no markdown, no extra text, strictly valid JSON): | |
| {{ | |
| "recommendations": [ | |
| {{ | |
| "rank": 1, | |
| "symbol": "SYMBOL", | |
| "company_name": "Full Company Name", | |
| "sector": "Sector/Industry", | |
| "price": 1234.50, | |
| "pe": 22.4, | |
| "pb": 4.2, | |
| "roe": 19.5, | |
| "debt_to_equity": 0.1, | |
| "reason": "1-2 sentences detailing the core fundamental strength and why it aligns with the valuation strategy.", | |
| "why_now": "1-2 sentences on the near-term catalyst or reason today is a good entry point.", | |
| "risks": "One key risk to monitor for this company." | |
| }} | |
| ] | |
| }} | |
| """ | |
| def run_recommendations_analysis(portfolio_symbols: list[str], api_key: str = None): | |
| """ | |
| Orchestrates search-grounded recommendation retrieval. | |
| """ | |
| try: | |
| today_date = datetime.now().strftime("%Y-%m-%d") | |
| prompt = build_recommendations_prompt(portfolio_symbols, today_date) | |
| raw_response = call_gemini(prompt, api_key=api_key) | |
| if not raw_response: | |
| return None, "Failed to get a response from the Gemini API." | |
| parsed_data = parse_response(raw_response) | |
| if not parsed_data: | |
| return None, "Failed to parse the recommendation JSON." | |
| return parsed_data, None | |
| except Exception as e: | |
| return None, f"Recommendations fetch failed with error: {e}" | |