"""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) @cache_decorator 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}"