import json import logging from backend.utils.rag import FinancialRAG from backend.utils.pdf_parser import PDFParser from backend.utils.table_extractor import FinancialTableExtractor from backend.models.schemas import FundamentalMetrics from backend.utils.ai_helper import generate_content_with_fallback logger = logging.getLogger(__name__) class FundamentalAnalyzer: def __init__(self): from backend.utils.rag import get_rag self.rag = get_rag() self.pdf_parser = PDFParser self.table_extractor = FinancialTableExtractor() def process_and_store(self, pdf_path: str, company_name: str, report_type: str, job_id: str): import time t_start = time.time() # Extract Text print(f"[Fundamental Analyzer] Starting PDF Extraction for {pdf_path}...") parser = self.pdf_parser(pdf_path) text = parser.extract_text() t_pdf = time.time() print(f"[Fundamental Analyzer] PDF Extraction took {t_pdf - t_start:.2f}s. Length: {len(text)} chars.") # Store in RAG print(f"[Fundamental Analyzer] Starting RAG Ingestion...") self.rag.add_document(text, company_name, report_type, job_id) t_rag = time.time() print(f"[Fundamental Analyzer] RAG Ingestion took {t_rag - t_pdf:.2f}s.") print(f"[Fundamental Analyzer] Total Process Time: {t_rag - t_start:.2f}s.") # Tables (optional for now, can add to context later) # tables = parser.extract_tables() # id_tables = self.table_extractor.identify_financial_tables(tables) def analyze(self, company_name: str) -> FundamentalMetrics: print(f"\n[Fundamental Analyzer] Starting Mixed Analysis for {company_name}...") # 1. Fetch Reliable CSV Data # Import here to avoid circular dependency if any from backend.utils.ticker_db import get_ticker_db db = get_ticker_db() csv_data = db.get_company_details(company_name) or {} print(f"[Fundamental Analyzer] CSV Data Found: {bool(csv_data)}") # 2. Retrieve Qualitative Context via RAG context = self.rag.query_context( f"What is the management outlook, future growth plans, strategic direction, and key risks for {company_name}?", company_name ) print(f"[Fundamental Analyzer] Retrieved {len(context)} characters of context.") # 3. LLM Extraction for Qualitative Fields & Missing Quantitative # We explicitly ask for the missing CSV metrics (Debt, Growth, Margin) here prompt = f""" You are a financial analyst. I have some quantitative data but I am missing key metrics. Context from Annual/Quarterly Report: {context[:32000]} Task: 1. IDENTIFY THE SECTOR (e.g., IT, Pharma, Banking, Oil & Gas). 2. SEARCH the text for "Financial Highlights" or "Consolidated Results" tables. 3. EXTRACT the following missing metrics. - **Raw Financials (Crucial)**: Extract the actual numbers for Current Year and Previous Year Revenue/Sales and Net Profit to calculate growth accurately. - **Debt-to-Equity**: Look for "D/E ratio" or "Gearing". 4. Qualitative extraction: - "Management Outlook": Tone? Future guidance? - "Future Plans": Capex, expansions? - "Strengths" & "Concerns": Key risks/moats. - "Health Score": 0-10. CRITICAL: - If a number is NOT found, return 0.0. - Normalize numbers to CRORES if possible, or keep consistent units (e.g., both in Millions) so division works. Return JSON object (no markdown): {{ "sector": "", "revenue_current": , "revenue_prior": , "profit_current": , "profit_prior": , "revenue_growth_pct": , "debt_to_equity": , "management_outlook": "", "future_plans": "", "strengths": ["", ""], "concerns": ["", ""], "health_score": }} """ llm_data = { "sector": "Unknown Sector", "revenue_current": 0.0, "revenue_prior": 0.0, "profit_current": 0.0, "profit_prior": 0.0, "revenue_growth_pct": 0.0, "debt_to_equity": 0.0, "management_outlook": "Data not available in report.", "future_plans": "Data not available in report.", "strengths": [], "concerns": [], "health_score": 5 } try: if context and len(context) > 100: print("[Fundamental Analyzer] Asking AI for qualitative insights + math inputs...") response_text = generate_content_with_fallback(prompt) extracted = json.loads(response_text.replace("```json", "").replace("```", "")) llm_data.update(extracted) else: print("[Fundamental Analyzer] RAG Context empty. Using defaults.") except Exception as e: logger.error(f"Qualitative analysis failed: {e}") llm_data["strengths"].append(f"AI Extraction Error: {str(e)}") # --- MATH VERIFICATION --- # Calculate Growth if raw numbers exist calc_rev_growth = 0.0 if llm_data['revenue_current'] > 0 and llm_data['revenue_prior'] > 0: calc_rev_growth = ((llm_data['revenue_current'] - llm_data['revenue_prior']) / llm_data['revenue_prior']) * 100 elif llm_data['revenue_growth_pct'] != 0: calc_rev_growth = llm_data['revenue_growth_pct'] # Calculate Margin calc_margin = 0.0 if llm_data['profit_current'] > 0 and llm_data['revenue_current'] > 0: calc_margin = (llm_data['profit_current'] / llm_data['revenue_current']) * 100 # 4. Merge Data (CSV takes precedence for numbers, BUT fill gaps with LLM) metrics = FundamentalMetrics( # CSV Quantitative market_cap=csv_data.get('Market Cap (Cr.)', 0.0), pe_ratio=csv_data.get('PE Ratio', 0.0), industry_pe=csv_data.get('Industry PE', 0.0), roe=csv_data.get('ROE', 0.0), roce=csv_data.get('ROCE', 0.0), eps=csv_data.get('EPS', 0.0), pb_ratio=csv_data.get('PB Ratio', 0.0), dividend_yield=csv_data.get('Dividend', 0.0), # Fill voids with LLM data debt_to_equity=float(llm_data.get('debt_to_equity', 0.0)), sector=llm_data.get('sector', 'Unknown Sector'), # Returns returns_1m=csv_data.get('1M Returns', 0.0), returns_3m=csv_data.get('3M Returns', 0.0), returns_1y=csv_data.get('1 Yr Returns', 0.0), returns_3y=csv_data.get('3 Yr Returns', 0.0), returns_5y=csv_data.get('5 Yr Returns', 0.0), # Technicals fifty_dma=csv_data.get('50 DMA', 0.0), two_hundred_dma=csv_data.get('200 DMA', 0.0), rsi=csv_data.get('RSI', 0.0), # LLM Qualitative health_score=llm_data.get('health_score', 5), strengths=llm_data.get('strengths', []), concerns=llm_data.get('concerns', []), management_outlook=llm_data.get('management_outlook'), future_plans=llm_data.get('future_plans'), # Computed Math revenue_growth=round(calc_rev_growth, 2), profit_margin=round(calc_margin, 2), # Hidden Raw revenue_current=llm_data.get('revenue_current', 0.0), revenue_prior=llm_data.get('revenue_prior', 0.0), profit_current=llm_data.get('profit_current', 0.0), profit_prior=llm_data.get('profit_prior', 0.0) ) return metrics