""" Valuation engine for stock analysis. Implements DCF, comparable multiples, and scenario analysis. """ import pandas as pd import numpy as np from typing import Dict, List, Optional, Tuple class ValuationEngine: """Performs stock valuation using multiple methodologies""" def __init__(self, financial_data: Dict, analysis_results: Dict): """ Initialize valuation engine Args: financial_data: Complete dataset from FinancialDataFetcher analysis_results: Results from FinancialAnalyzer """ self.ticker = financial_data.get('ticker') self.metrics = financial_data.get('metrics', {}) self.statements = financial_data.get('financial_statements', {}) self.sector_metrics = financial_data.get('sector_metrics', {}) self.analysis = analysis_results def calculate_intrinsic_value_dcf(self, growth_rate: float = 0.10, terminal_growth: float = 0.02, discount_rate: float = 0.10, years: int = 5) -> Dict: """ Calculate intrinsic value using DCF method Args: growth_rate: Expected FCF growth rate terminal_growth: Perpetual growth rate discount_rate: WACC / required return years: Forecast period Returns: DCF valuation results """ results = { 'method': 'DCF', 'assumptions': { 'growth_rate': growth_rate, 'terminal_growth': terminal_growth, 'discount_rate': discount_rate, 'forecast_years': years } } try: # Get current FCF current_fcf = self.metrics.get('free_cash_flow', 0) if current_fcf <= 0: results['error'] = 'Negative or zero FCF - DCF not applicable' return results # Project FCF projected_fcf = [] pv_fcf = [] for year in range(1, years + 1): fcf = current_fcf * ((1 + growth_rate) ** year) pv = fcf / ((1 + discount_rate) ** year) projected_fcf.append(fcf) pv_fcf.append(pv) # Terminal value terminal_fcf = projected_fcf[-1] * (1 + terminal_growth) terminal_value = terminal_fcf / (discount_rate - terminal_growth) pv_terminal = terminal_value / ((1 + discount_rate) ** years) # Enterprise value enterprise_value = sum(pv_fcf) + pv_terminal # Equity value net_debt = self.metrics.get('total_debt', 0) - self.metrics.get('total_cash', 0) equity_value = enterprise_value - net_debt # Per share value shares_outstanding = self.metrics.get('shares_outstanding', 1) fair_value_per_share = equity_value / shares_outstanding if shares_outstanding > 0 else 0 current_price = self.metrics.get('current_price', 0) upside = ((fair_value_per_share - current_price) / current_price) * 100 if current_price > 0 else 0 results.update({ 'current_fcf': current_fcf, 'pv_cash_flows': sum(pv_fcf), 'pv_terminal_value': pv_terminal, 'enterprise_value': enterprise_value, 'equity_value': equity_value, 'fair_value_per_share': fair_value_per_share, 'current_price': current_price, 'upside_percent': upside, 'recommendation': 'BUY' if upside > 15 else 'HOLD' if upside > -10 else 'SELL' }) except Exception as e: results['error'] = f'DCF calculation error: {str(e)}' return results def calculate_relative_valuation(self) -> Dict: """ Calculate valuation using comparable multiples Returns: Relative valuation results """ results = { 'method': 'Comparable Multiples' } current_price = self.metrics.get('current_price', 0) # P/E based valuation if self.metrics.get('trailing_pe') and self.metrics.get('eps_trailing'): sector_pe = self.sector_metrics.get('trailing_pe_median') if sector_pe: eps = self.metrics.get('eps_trailing') fair_value_pe = eps * sector_pe pe_upside = ((fair_value_pe - current_price) / current_price) * 100 if current_price > 0 else 0 results['pe_valuation'] = { 'company_pe': self.metrics.get('trailing_pe'), 'sector_pe_median': sector_pe, 'eps': eps, 'fair_value': fair_value_pe, 'current_price': current_price, 'upside_percent': pe_upside } # PEG based valuation if self.metrics.get('peg_ratio') and self.metrics.get('eps_forward'): sector_peg = self.sector_metrics.get('peg_ratio_median') earnings_growth = self.analysis.get('growth_analysis', {}).get('earnings_growth_ttm', 0) if sector_peg and earnings_growth: eps_forward = self.metrics.get('eps_forward') fair_pe = sector_peg * (earnings_growth * 100) fair_value_peg = eps_forward * fair_pe peg_upside = ((fair_value_peg - current_price) / current_price) * 100 if current_price > 0 else 0 results['peg_valuation'] = { 'company_peg': self.metrics.get('peg_ratio'), 'sector_peg_median': sector_peg, 'earnings_growth': earnings_growth, 'fair_pe': fair_pe, 'fair_value': fair_value_peg, 'current_price': current_price, 'upside_percent': peg_upside } # P/B based valuation if self.metrics.get('price_to_book') and self.metrics.get('book_value_per_share'): sector_pb = self.sector_metrics.get('price_to_book_median') if sector_pb: book_value = self.metrics.get('book_value_per_share') fair_value_pb = book_value * sector_pb pb_upside = ((fair_value_pb - current_price) / current_price) * 100 if current_price > 0 else 0 results['pb_valuation'] = { 'company_pb': self.metrics.get('price_to_book'), 'sector_pb_median': sector_pb, 'book_value_per_share': book_value, 'fair_value': fair_value_pb, 'current_price': current_price, 'upside_percent': pb_upside } # Calculate average upside from available methods upsides = [] if 'pe_valuation' in results: upsides.append(results['pe_valuation']['upside_percent']) if 'peg_valuation' in results: upsides.append(results['peg_valuation']['upside_percent']) if 'pb_valuation' in results: upsides.append(results['pb_valuation']['upside_percent']) if upsides: avg_upside = sum(upsides) / len(upsides) results['average_upside'] = avg_upside results['recommendation'] = 'BUY' if avg_upside > 15 else 'HOLD' if avg_upside > -10 else 'SELL' return results def scenario_analysis(self) -> Dict: """ Perform bull/base/bear scenario valuation Returns: Scenario analysis results """ # Base case: use historical/current growth rates base_growth = self.analysis.get('growth_analysis', {}).get('revenue_growth_ttm', 0.08) base_growth = max(0.05, min(base_growth, 0.20)) # Cap between 5% and 20% scenarios = {} # Bear case: 20% lower growth, higher risk (lower valuation) bear_growth = base_growth * 0.8 scenarios['bear'] = self.calculate_intrinsic_value_dcf( growth_rate=bear_growth, discount_rate=0.12 # Higher discount rate = higher risk = lower valuation ) # Base case scenarios['base'] = self.calculate_intrinsic_value_dcf( growth_rate=base_growth, discount_rate=0.10 ) # Bull case: 20% higher growth, lower risk (higher valuation) bull_growth = base_growth * 1.2 scenarios['bull'] = self.calculate_intrinsic_value_dcf( growth_rate=bull_growth, discount_rate=0.08 # Lower discount rate = lower risk = higher valuation ) # Summary results = { 'scenarios': scenarios, 'current_price': self.metrics.get('current_price', 0) } # Calculate price ranges bear_price = scenarios['bear'].get('fair_value_per_share', 0) base_price = scenarios['base'].get('fair_value_per_share', 0) bull_price = scenarios['bull'].get('fair_value_per_share', 0) results['price_range'] = { 'bear': bear_price, 'base': base_price, 'bull': bull_price, 'range': bull_price - bear_price } # Risk/reward assessment current_price = results['current_price'] if current_price > 0: downside = ((bear_price - current_price) / current_price) * 100 upside = ((bull_price - current_price) / current_price) * 100 results['risk_reward'] = { 'downside_percent': downside, 'upside_percent': upside, 'risk_reward_ratio': abs(upside / downside) if downside != 0 else 0, 'assessment': 'Favorable' if upside > abs(downside) else 'Unfavorable' } return results def calculate_margin_of_safety(self) -> Dict: """ Calculate margin of safety Returns: Margin of safety metrics """ results = {} current_price = self.metrics.get('current_price', 0) if current_price <= 0: return {'error': 'Invalid current price'} # Based on DCF dcf_result = self.calculate_intrinsic_value_dcf() if 'fair_value_per_share' in dcf_result: intrinsic_value = dcf_result['fair_value_per_share'] margin = ((intrinsic_value - current_price) / intrinsic_value) * 100 if intrinsic_value > 0 else 0 results['dcf_margin_of_safety'] = { 'intrinsic_value': intrinsic_value, 'current_price': current_price, 'margin_percent': margin, 'assessment': self._assess_margin_of_safety(margin) } # Based on book value book_value = self.metrics.get('book_value_per_share', 0) if book_value > 0: margin = ((book_value - current_price) / book_value) * 100 results['book_value_margin'] = { 'book_value': book_value, 'current_price': current_price, 'margin_percent': margin } return results def _assess_margin_of_safety(self, margin: float) -> str: """Assess margin of safety""" if margin >= 30: return "Excellent - Strong margin of safety" elif margin >= 20: return "Good - Adequate margin of safety" elif margin >= 10: return "Fair - Minimal margin of safety" elif margin >= 0: return "Weak - Little to no margin of safety" else: return "Overvalued - Negative margin of safety" def generate_valuation_report(self) -> Dict: """ Generate comprehensive valuation report Returns: Complete valuation analysis """ return { 'ticker': self.ticker, 'current_price': self.metrics.get('current_price', 0), 'dcf_valuation': self.calculate_intrinsic_value_dcf(), 'relative_valuation': self.calculate_relative_valuation(), 'scenario_analysis': self.scenario_analysis(), 'margin_of_safety': self.calculate_margin_of_safety() } if __name__ == "__main__": print("This module is meant to be imported and used with data from data_fetcher.py and financial_analyzer.py")