CoolFace
Apppublic

kussssh/IPO-Analyzer

sourceHugging Faceupdated 5mo agoView on Hugging Face
0likes
m4_valuation.py135 linesDownload Raw Back to modules
1"""2Module 4 — Valuation vs Peers Analysis3OPTIMIZED: Expanded retrieval queries + increased context + Python median/premium math.4"""5from langchain_core.prompts import ChatPromptTemplate6 7from backend.schemas import ValuationAnalysis8from backend.modules.base import get_llm, _invoke_with_retry9from backend.retriever import AnalysisContext10 11 12def analyze_valuation(13    ensemble_retriever=None,14    reranker=None,15    doc_type: str = "drhp",16    analysis_context: AnalysisContext | None = None,17):18    """Direct retrieval with expanded queries for peer comparison."""19    print("\n▶ Running Module: 4. Valuation vs Peers Analysis")20 21    llm = get_llm()22 23    context = analysis_context or AnalysisContext(ensemble_retriever, reranker)24    all_context_docs = []25    seen = set()26 27    queries = [28        "Comparison with Listed Industry Peers P/E EPS ROE RONW",29        "comparable companies valuation peer group table",30        "peer group comparison listed peers revenue face value EPS earnings per share",31        "price band face value equity share IPO price",32        "Basis for Issue Price weighted average earnings per share diluted EPS",33        "market capitalisation enterprise value EV EBITDA",34        "P/E ratio price earnings listed peer comparison",35        "issue price per share pre-money post-money valuation",36        "industry benchmark comparable listed companies returns",37        "our company versus peers listed comparison table financial metrics",38    ]39 40    # ── Parallel retrieval across all queries ──────────────────────────41    import concurrent.futures42 43    def _fetch(q: str):44        try:45            return context.raw_search(q)46        except Exception:47            return []48 49    with concurrent.futures.ThreadPoolExecutor(max_workers=6) as pool:50        futures = [pool.submit(_fetch, q) for q in queries]51        for future in concurrent.futures.as_completed(futures):52            for doc in future.result()[:8]:53                content_hash = hash(doc.page_content[:200])54                if content_hash not in seen:55                    seen.add(content_hash)56                    all_context_docs.append(doc)57 58    all_context_docs = all_context_docs[:20]59    print(f"  Retrieved {len(all_context_docs)} unique chunks for valuation analysis")60 61 62    context_parts = []63    for i, doc in enumerate(all_context_docs):64        context_parts.append(f"[Chunk {i+1}]\n{doc.page_content}")65    context = "\n\n" + "=" * 60 + "\n\n".join(context_parts)66 67    structured_llm = llm.with_structured_output(ValuationAnalysis)68    doc_label = doc_type.upper()69    price_note = (70        "Price band may NOT be available in DRHP stage. Return null if not found."71        if doc_type == "drhp"72        else "Price band SHOULD be explicitly stated in an RHP/Prospectus. Extract it carefully."73    )74 75    prompt = ChatPromptTemplate.from_messages([76        ("system", f"""You are an equity research analyst extracting valuation data from an Indian IPO {doc_label}.77Rules:78- Extract ONLY explicitly stated values. Never invent numbers.79- {price_note}80- UNIT CONVERSION (MANDATORY): First, check the table header to determine the unit ("Lakhs", "Millions", or "Crores"). All monetary figures must be in INR Crores.81  * If the document header says '₹ in Lakhs' — divide by 100 to get Crores (e.g., 1000 Lakhs = 10 Crores). CRITICAL: Do NOT divide by 1000.82  * If the document header says '₹ in Millions' — divide by 10 to get Crores (e.g., 1000 Millions = 100 Crores).83  * If '₹ in Crores' — use as-is.84- The peer comparison table may be SPLIT across multiple pages/chunks. Scan ALL chunks to find all rows.85- Merge fragmented table rows into one complete peers list."""),86        ("human", """{task}\n\n{doc_label} CONTEXT:\n{context}""")87    ])88 89    chain = prompt | structured_llm90 91    result = _invoke_with_retry(chain, {92        "task": """93Extract valuation and peer comparison data from ALL provided chunks:94 951. price_band_lower / price_band_upper: Extract if explicitly stated. Return null if not found (common in DRHP).962. market_cap_cr: Implied market capitalisation at upper band in INR Crores, if mentioned.973. ipo_pe: The company's P/E ratio ONLY if explicitly stated in the document.98   CRITICAL: If the document states "Not Applicable", "NA", "Not Disclosed", "N.A.", "[●]", or leaves the P/E row blank — set ipo_pe to null. DO NOT calculate or infer the P/E ratio yourself.994. ipo_ev_ebitda: EV/EBITDA multiple. Search THOROUGHLY across the peer table and "Basis for Issue Price" section. If absolutely not stated, return null.100 1015. peers: SCAN EVERY CHUNK for the peer comparison table. It may be titled:102   "Comparison with Listed Industry Peers", "Peer Group Comparison", "Listed Peer Set", or similar.103   The table rows may be spread across 2-3 chunks/pages — collect ALL rows.104   For EACH peer company extract:105   - company_name (exact name), pe_ratio (P/E), ev_ebitda, price_to_sales, roe_pct (ROE/RONW %)106   1076. Leave peer_median_pe and premium_vs_peers_pct as null (calculated in code).108 1097. signal and signal_reasoning (write 2-3 sentences with SPECIFIC multiples from data):110   - POSITIVE: IPO P/E <= peer median OR premium justified by clearly superior growth/margins111   - NEUTRAL: Priced at up to 20% premium vs peers, OR price band not yet set (DRHP stage), OR P/E not disclosed112   - NEGATIVE: Priced >30% premium vs peers without superior fundamentals113""",114        "doc_label": doc_label,115        "context": context116    })117 118    # Python post-processing: compute peer_median_pe and premium119    peer_pes = [p.pe_ratio for p in result.peers if p.pe_ratio is not None]120    if peer_pes:121        sorted_pes = sorted(peer_pes)122        mid = len(sorted_pes) // 2123        if len(sorted_pes) % 2 == 0:124            result.peer_median_pe = round((sorted_pes[mid - 1] + sorted_pes[mid]) / 2, 2)125        else:126            result.peer_median_pe = sorted_pes[mid]127 128    if result.ipo_pe is not None and result.peer_median_pe is not None and result.peer_median_pe != 0:129        result.premium_vs_peers_pct = round(130            ((result.ipo_pe - result.peer_median_pe) / result.peer_median_pe) * 100, 2131        )132 133    print(f"  ✅ 4. Valuation vs Peers Analysis complete — Signal: {result.signal}")134    return result135