Spaces:
Running
Running
| """ | |
| 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") | |