|
| 1 | +from typing import Dict, Any, List |
| 2 | +import logging |
| 3 | + |
| 4 | +logger = logging.getLogger(__name__) |
| 5 | + |
| 6 | + |
| 7 | +def calculate_dcf(financials: Dict[str, Any], risk_free_rate: float = 0.04, scenario: Dict[str, Any] = None) -> Dict[str, Any]: |
| 8 | + """ |
| 9 | + Performs a simplified Discounted Cash Flow (DCF) analysis. |
| 10 | +
|
| 11 | + Args: |
| 12 | + financials: Dict containing 'fcf' (Free Cash Flow), 'growth_rate', 'beta'. |
| 13 | + risk_free_rate: The current risk-free rate (e.g., 10y Treasury). |
| 14 | + scenario: Optional dict for Scenario Injection (e.g., {'growth_rate': 0.02, 'market_risk_premium': 0.08}) |
| 15 | +
|
| 16 | + Returns: |
| 17 | + Dict with 'wacc', 'terminal_growth', 'intrinsic_value', 'share_price'. |
| 18 | + """ |
| 19 | + logger.info("Running DCF Analysis...") |
| 20 | + |
| 21 | + if scenario: |
| 22 | + logger.info(f"Injecting Scenario: {scenario}") |
| 23 | + |
| 24 | + # Extract or Default (Scenario overrides financials) |
| 25 | + scenario = scenario or {} |
| 26 | + |
| 27 | + fcf = scenario.get("fcf", financials.get("fcf", 1000)) |
| 28 | + growth_rate = scenario.get("growth_rate", financials.get("growth_rate", 0.05)) |
| 29 | + beta = scenario.get("beta", financials.get("beta", 1.2)) |
| 30 | + shares_outstanding = financials.get("shares_outstanding", 500) |
| 31 | + |
| 32 | + # Market Assumptions (Scenario overrides defaults) |
| 33 | + market_risk_premium = scenario.get("market_risk_premium", 0.05) |
| 34 | + cost_of_debt = scenario.get("cost_of_debt", 0.06) |
| 35 | + tax_rate = scenario.get("tax_rate", 0.21) |
| 36 | + |
| 37 | + # Capital Structure |
| 38 | + debt_equity_ratio = financials.get("debt_equity_ratio", 0.5) |
| 39 | + |
| 40 | + # 1. Calculate WACC |
| 41 | + # Scenario injection for risk_free_rate is handled by the argument, but check scenario dict too |
| 42 | + rfr = scenario.get("risk_free_rate", risk_free_rate) |
| 43 | + |
| 44 | + cost_of_equity = rfr + beta * market_risk_premium |
| 45 | + wacc = (cost_of_equity * (1 / (1 + debt_equity_ratio))) + \ |
| 46 | + (cost_of_debt * (1 - tax_rate) * (debt_equity_ratio / (1 + debt_equity_ratio))) |
| 47 | + |
| 48 | + # 2. Project FCF (5 years) |
| 49 | + projected_fcf = [] |
| 50 | + current_fcf = fcf |
| 51 | + for _ in range(5): |
| 52 | + current_fcf *= (1 + growth_rate) |
| 53 | + projected_fcf.append(current_fcf) |
| 54 | + |
| 55 | + # 3. Terminal Value |
| 56 | + terminal_growth = 0.025 |
| 57 | + terminal_value = (projected_fcf[-1] * (1 + terminal_growth)) / (wacc - terminal_growth) |
| 58 | + |
| 59 | + # 4. Discount to Present |
| 60 | + enterprise_value = 0 |
| 61 | + for t, cash_flow in enumerate(projected_fcf, 1): |
| 62 | + enterprise_value += cash_flow / ((1 + wacc) ** t) |
| 63 | + |
| 64 | + enterprise_value += terminal_value / ((1 + wacc) ** 5) |
| 65 | + |
| 66 | + # 5. Equity Value |
| 67 | + net_debt = financials.get("net_debt", 2000) |
| 68 | + equity_value = enterprise_value - net_debt |
| 69 | + intrinsic_share_price = equity_value / shares_outstanding |
| 70 | + |
| 71 | + return { |
| 72 | + "wacc": round(wacc, 4), |
| 73 | + "terminal_growth": terminal_growth, |
| 74 | + "intrinsic_value": round(equity_value, 2), |
| 75 | + "intrinsic_share_price": round(intrinsic_share_price, 2), |
| 76 | + "method": "5-Year DCF / Gordon Growth" |
| 77 | + } |
| 78 | + |
| 79 | + |
| 80 | +def calculate_multiples(financials: Dict[str, Any], peer_group: List[Dict[str, Any]]) -> Dict[str, Any]: |
| 81 | + """ |
| 82 | + Performs Relative Valuation using trading multiples. |
| 83 | + """ |
| 84 | + logger.info("Running Multiples Analysis...") |
| 85 | + |
| 86 | + ev = financials.get("enterprise_value", 10000) |
| 87 | + ebitda = financials.get("ebitda", 2000) |
| 88 | + |
| 89 | + current_ev_ebitda = ev / ebitda if ebitda else 0 |
| 90 | + |
| 91 | + # Peer Analysis |
| 92 | + peer_multiples = [p.get("ev_ebitda", 10.0) for p in peer_group] |
| 93 | + peer_median = sum(peer_multiples) / len(peer_multiples) if peer_multiples else 10.0 |
| 94 | + |
| 95 | + premium_discount = (current_ev_ebitda - peer_median) / peer_median |
| 96 | + |
| 97 | + return { |
| 98 | + "current_ev_ebitda": round(current_ev_ebitda, 2), |
| 99 | + "peer_median_ev_ebitda": round(peer_median, 2), |
| 100 | + "premium_discount_pct": round(premium_discount * 100, 2), |
| 101 | + "verdict": "Overvalued" if premium_discount > 0.1 else "Undervalued" if premium_discount < -0.1 else "Fairly Valued" |
| 102 | + } |
| 103 | + |
| 104 | + |
| 105 | +def get_price_targets(intrinsic_price: float, volatility: float) -> Dict[str, float]: |
| 106 | + """ |
| 107 | + Generates Bull, Base, and Bear case price targets. |
| 108 | + """ |
| 109 | + return { |
| 110 | + "bear_case": round(intrinsic_price * (1 - volatility * 2), 2), |
| 111 | + "base_case": round(intrinsic_price, 2), |
| 112 | + "bull_case": round(intrinsic_price * (1 + volatility * 2), 2) |
| 113 | + } |
0 commit comments