""" Investment Decision Maker Main entry point: Input ticker → Output BUY/SELL/HOLD recommendation Uses composite scoring based on institutional methodology """ import pandas as pd import numpy as np from typing import Dict, Tuple, Optional, List from fundamental_analysis.calculator import calculate_metrics_for_ticker from fundamental_analysis.sector_analyzer import SectorAnalyzer import yfinance as yf import warnings warnings.filterwarnings('ignore') class InvestmentDecision: """Make BUY/SELL/HOLD decisions based on comprehensive analysis""" # Scoring weights based on institutional methodology WEIGHTS = { 'fcf_yield': 0.25, # 25% - HIGHEST priority 'roic': 0.25, # 25% - Quality indicator 'growth': 0.15, # 15% - Growth metrics 'valuation': 0.15, # 15% - Valuation ratios 'leverage': 0.10, # 10% - Financial health 'capital_allocation': 0.10 # 10% - Capital management } # Decision thresholds THRESHOLDS = { 'buy': 0.35, # Score >= 0.35 → BUY 'sell': -0.10 # Score < -0.10 → SELL } def __init__(self, ticker: str, compare_to_sector: bool = True): """ Initialize decision maker Args: ticker: Stock ticker symbol compare_to_sector: Whether to compare against sector peers """ self.ticker = ticker.upper() self.compare_to_sector = compare_to_sector self.metrics_df = pd.DataFrame() self.metrics_dict = {} self.sector_comparison = pd.DataFrame() self.sector_percentiles = {} self.scores = {} self.final_score = 0.0 self.recommendation = "HOLD" self.confidence = 0.0 self.reasoning = [] def analyze(self) -> Dict: """ Perform complete analysis and generate recommendation Returns: Dictionary with recommendation and detailed analysis """ print(f"\n{'='*80}") print(f"INVESTMENT ANALYSIS: {self.ticker}") print(f"{'='*80}\n") # Step 1: Calculate all metrics print("Step 1: Calculating financial metrics...") self.metrics_df, summary = calculate_metrics_for_ticker(self.ticker) if self.metrics_df.empty: return self._error_result("Failed to fetch data") # Convert metrics to dictionary for easier access self._build_metrics_dict() # Step 2: Get sector comparison if self.compare_to_sector: print("\nStep 2: Comparing to sector peers...") self._compare_to_sector() else: print("\nStep 2: Skipping sector comparison") # Step 3: Score each category print("\nStep 3: Scoring investment criteria...") self._score_all_categories() # Step 4: Calculate final score and recommendation print("\nStep 4: Generating recommendation...") self._calculate_final_score() self._determine_recommendation() # Step 5: Build reasoning self._build_reasoning() # Return complete analysis return self._build_result() def _build_metrics_dict(self): """Convert metrics DataFrame to dictionary for easier access""" for _, row in self.metrics_df.iterrows(): if row['Status'] == 'Available': metric_name = row['Metric'].replace(' ', '_').replace('/', '_').replace('(', '').replace(')', '').replace('%', 'pct') self.metrics_dict[metric_name] = row['Value'] def _get_metric(self, metric_name: str) -> Optional[float]: """Safely get metric value""" row = self.metrics_df[self.metrics_df['Metric'] == metric_name] if not row.empty and row.iloc[0]['Status'] == 'Available': return row.iloc[0]['Value'] return None def _compare_to_sector(self): """Compare stock to sector peers""" try: # Get company info to determine sector stock = yf.Ticker(self.ticker) info = stock.info sector = info.get('sector', 'Unknown') if sector == 'Unknown': print(" ⚠ Could not determine sector, skipping comparison") return print(f" Sector: {sector}") # Map yfinance sectors to our predefined sectors sector_mapping = { 'Technology': 'Technology', 'Financial Services': 'Financial', 'Healthcare': 'Healthcare', 'Consumer Cyclical': 'Consumer', 'Consumer Defensive': 'Consumer', 'Industrials': 'Industrial', 'Energy': 'Energy', 'Basic Materials': 'Materials', 'Real Estate': 'Real Estate', 'Communication Services': 'Communication' } mapped_sector = sector_mapping.get(sector) if not mapped_sector: print(f" ⚠ Sector '{sector}' not in predefined list") return # Analyze sector with a subset of stocks (to avoid long wait) analyzer = SectorAnalyzer(mapped_sector) tickers = analyzer.get_sector_tickers() # Limit to 10 stocks for faster analysis if len(tickers) > 10: tickers = tickers[:10] print(f" Analyzing {len(tickers)} peer stocks (limited sample)...") comparison_df = analyzer.calculate_sector_metrics(tickers) if not comparison_df.empty and self.ticker in comparison_df.index: self.sector_comparison = analyzer.compare_stock_to_peers(self.ticker, comparison_df) self.sector_percentiles = comparison_df.rank(pct=True).loc[self.ticker].to_dict() print(f" ✓ Sector comparison complete") else: print(f" ⚠ Could not compare to sector") except Exception as e: print(f" ⚠ Sector comparison error: {str(e)}") def _score_all_categories(self): """Score each investment category""" self.scores['fcf_yield'] = self._score_fcf_yield() self.scores['roic'] = self._score_roic() self.scores['growth'] = self._score_growth() self.scores['valuation'] = self._score_valuation() self.scores['leverage'] = self._score_leverage() self.scores['capital_allocation'] = self._score_capital_allocation() # Print scores for category, score in self.scores.items(): print(f" {category.replace('_', ' ').title()}: {score:+.2f}") def _score_fcf_yield(self) -> float: """ Score FCF Yield (HIGHEST PRIORITY - 25% weight) Threshold: >6% = BUY, 4-6% = HOLD, <3% = SELL """ fcf_yield = self._get_metric('FCF Yield (Enterprise) %') if fcf_yield is None: return 0.0 # Scoring rules if fcf_yield >= 6.0: score = 1.0 # Strong buy signal elif fcf_yield >= 4.0: score = 0.5 # Hold elif fcf_yield >= 3.0: score = 0.0 # Neutral else: score = -1.0 # Sell signal # Adjust based on sector percentile if available if 'FCF_Yield_%' in self.sector_percentiles: percentile = self.sector_percentiles['FCF_Yield_%'] if percentile > 0.75: score += 0.3 # Top quartile bonus elif percentile < 0.25: score -= 0.3 # Bottom quartile penalty return np.clip(score, -1.0, 1.0) def _score_roic(self) -> float: """ Score ROIC (VERY HIGH PRIORITY - 25% weight) Threshold: >15% = excellent, >10% = good, <10% = concern """ roic = self._get_metric('Return on Invested Capital (ROIC) %') if roic is None: return 0.0 # Scoring rules if roic >= 20.0: score = 1.0 # Exceptional elif roic >= 15.0: score = 0.7 # Excellent elif roic >= 10.0: score = 0.3 # Good elif roic >= 5.0: score = -0.3 # Mediocre else: score = -1.0 # Poor # Adjust based on sector percentile if 'ROIC_%' in self.sector_percentiles: percentile = self.sector_percentiles['ROIC_%'] if percentile > 0.75: score += 0.2 elif percentile < 0.25: score -= 0.2 return np.clip(score, -1.0, 1.0) def _score_growth(self) -> float: """ Score Growth Metrics (15% weight) Revenue growth, EPS growth """ rev_growth = self._get_metric('Revenue Growth (YoY) %') eps_growth = self._get_metric('EPS Growth (YoY) %') if rev_growth is None and eps_growth is None: return 0.0 scores = [] # Revenue growth scoring if rev_growth is not None: if rev_growth >= 20.0: scores.append(1.0) elif rev_growth >= 10.0: scores.append(0.5) elif rev_growth >= 5.0: scores.append(0.2) elif rev_growth >= 0.0: scores.append(-0.2) else: scores.append(-1.0) # Declining revenue # EPS growth scoring if eps_growth is not None: if eps_growth >= 20.0: scores.append(1.0) elif eps_growth >= 10.0: scores.append(0.5) elif eps_growth >= 5.0: scores.append(0.2) elif eps_growth >= 0.0: scores.append(-0.2) else: scores.append(-1.0) return np.clip(np.mean(scores) if scores else 0.0, -1.0, 1.0) def _score_valuation(self) -> float: """ Score Valuation Metrics (15% weight) P/E, PEG, EV/EBITDA relative to sector """ pe_ratio = self._get_metric('P/E Ratio (TTM)') peg_ratio = self._get_metric('PEG Ratio') ev_ebitda = self._get_metric('EV/EBITDA') scores = [] # PEG ratio scoring (most important valuation metric) if peg_ratio is not None: if peg_ratio < 0.8: scores.append(1.0) # Undervalued elif peg_ratio < 1.2: scores.append(0.3) # Fair value elif peg_ratio < 1.5: scores.append(-0.3) # Slightly expensive else: scores.append(-1.0) # Overvalued # P/E relative to sector if 'PE_Ratio' in self.sector_percentiles: percentile = self.sector_percentiles['PE_Ratio'] # Lower P/E is better (reverse percentile) if percentile < 0.33: scores.append(0.7) # Cheap relative to sector elif percentile < 0.67: scores.append(0.0) # Fair else: scores.append(-0.7) # Expensive # EV/EBITDA relative to sector if 'EV_EBITDA' in self.sector_percentiles: percentile = self.sector_percentiles['EV_EBITDA'] # Lower is better if percentile < 0.33: scores.append(0.5) elif percentile > 0.67: scores.append(-0.5) return np.clip(np.mean(scores) if scores else 0.0, -1.0, 1.0) def _score_leverage(self) -> float: """ Score Leverage/Financial Health (10% weight) Net Debt/EBITDA, Interest Coverage """ net_debt_ebitda = self._get_metric('Net Debt / EBITDA') current_ratio = self._get_metric('Current Ratio') cash_conversion = self._get_metric('Cash Conversion Ratio') scores = [] # Net Debt/EBITDA scoring if net_debt_ebitda is not None: if net_debt_ebitda < 1.0: scores.append(1.0) # Very low leverage elif net_debt_ebitda < 2.0: scores.append(0.5) # Moderate elif net_debt_ebitda < 3.0: scores.append(0.0) # Acceptable elif net_debt_ebitda < 4.0: scores.append(-0.5) # High else: scores.append(-1.0) # Very high risk # Current ratio if current_ratio is not None: if current_ratio >= 2.0: scores.append(0.5) elif current_ratio >= 1.5: scores.append(0.2) elif current_ratio >= 1.0: scores.append(-0.2) else: scores.append(-0.5) # Cash conversion (quality of earnings) if cash_conversion is not None: if cash_conversion >= 1.2: scores.append(0.5) elif cash_conversion >= 1.0: scores.append(0.2) elif cash_conversion >= 0.8: scores.append(-0.2) else: scores.append(-0.5) # Red flag return np.clip(np.mean(scores) if scores else 0.0, -1.0, 1.0) def _score_capital_allocation(self) -> float: """ Score Capital Allocation (10% weight) Dividends, buybacks, total payout ratio """ payout_ratio = self._get_metric('Payout Ratio %') total_payout = self._get_metric('Total Payout Ratio %') roe = self._get_metric('Return on Equity (ROE) %') scores = [] # Payout ratio - should be sustainable if payout_ratio is not None: if payout_ratio < 40.0: scores.append(0.5) # Low, room to grow elif payout_ratio < 60.0: scores.append(0.3) # Sustainable elif payout_ratio < 80.0: scores.append(-0.2) # High else: scores.append(-0.5) # Potentially unsustainable # Total payout (dividends + buybacks) if total_payout is not None and roe is not None: # Good capital allocation returns cash to shareholders while maintaining high ROE if roe > 15.0 and total_payout > 50.0: scores.append(0.5) # Strong returns + returning cash elif roe > 15.0: scores.append(0.3) # Strong returns, could return more elif total_payout > 50.0: scores.append(-0.3) # Returning cash but weak returns return np.clip(np.mean(scores) if scores else 0.0, -1.0, 1.0) def _calculate_final_score(self): """Calculate weighted final score""" self.final_score = sum( self.scores.get(category, 0.0) * weight for category, weight in self.WEIGHTS.items() ) # Calculate confidence based on data availability and sector comparison data_completeness = len([s for s in self.scores.values() if s != 0.0]) / len(self.scores) sector_bonus = 0.15 if not self.sector_comparison.empty else 0.0 self.confidence = min(data_completeness + sector_bonus, 1.0) def _determine_recommendation(self): """Determine BUY/SELL/HOLD based on final score""" if self.final_score >= self.THRESHOLDS['buy']: self.recommendation = "BUY" elif self.final_score < self.THRESHOLDS['sell']: self.recommendation = "SELL" else: self.recommendation = "HOLD" def _build_reasoning(self): """Build human-readable reasoning for the recommendation""" self.reasoning = [] # Overall assessment if self.final_score >= 0.5: self.reasoning.append("✓ Strong overall fundamentals") elif self.final_score >= 0.2: self.reasoning.append("✓ Positive fundamentals") elif self.final_score >= -0.2: self.reasoning.append("• Mixed fundamentals") else: self.reasoning.append("✗ Weak fundamentals") # FCF Yield fcf_yield = self._get_metric('FCF Yield (Enterprise) %') if fcf_yield: if fcf_yield >= 6.0: self.reasoning.append(f"✓ Excellent FCF yield: {fcf_yield:.2f}%") elif fcf_yield < 3.0: self.reasoning.append(f"✗ Low FCF yield: {fcf_yield:.2f}%") # ROIC roic = self._get_metric('Return on Invested Capital (ROIC) %') if roic: if roic >= 15.0: self.reasoning.append(f"✓ Strong ROIC: {roic:.2f}%") elif roic < 10.0: self.reasoning.append(f"✗ Weak ROIC: {roic:.2f}%") # Growth rev_growth = self._get_metric('Revenue Growth (YoY) %') if rev_growth: if rev_growth >= 15.0: self.reasoning.append(f"✓ Strong revenue growth: {rev_growth:.2f}%") elif rev_growth < 0: self.reasoning.append(f"✗ Declining revenue: {rev_growth:.2f}%") # Valuation peg_ratio = self._get_metric('PEG Ratio') if peg_ratio: if peg_ratio < 0.8: self.reasoning.append(f"✓ Undervalued (PEG: {peg_ratio:.2f})") elif peg_ratio > 1.5: self.reasoning.append(f"✗ Overvalued (PEG: {peg_ratio:.2f})") # Leverage net_debt_ebitda = self._get_metric('Net Debt / EBITDA') if net_debt_ebitda is not None: if net_debt_ebitda < 1.0: self.reasoning.append(f"✓ Low leverage: {net_debt_ebitda:.2f}x") elif net_debt_ebitda > 3.0: self.reasoning.append(f"✗ High leverage: {net_debt_ebitda:.2f}x") # Sector comparison if 'ROIC_%' in self.sector_percentiles: roic_pct = self.sector_percentiles['ROIC_%'] if roic_pct > 0.75: self.reasoning.append(f"✓ Top quartile ROIC vs peers (P{int(roic_pct*100)})") elif roic_pct < 0.25: self.reasoning.append(f"✗ Bottom quartile ROIC vs peers (P{int(roic_pct*100)})") def _build_result(self) -> Dict: """Build final result dictionary""" return { 'ticker': self.ticker, 'recommendation': self.recommendation, 'final_score': self.final_score, 'confidence': self.confidence, 'category_scores': self.scores, 'reasoning': self.reasoning, 'key_metrics': { 'FCF_Yield_%': self._get_metric('FCF Yield (Enterprise) %'), 'ROIC_%': self._get_metric('Return on Invested Capital (ROIC) %'), 'ROE_%': self._get_metric('Return on Equity (ROE) %'), 'Revenue_Growth_%': self._get_metric('Revenue Growth (YoY) %'), 'PEG_Ratio': self._get_metric('PEG Ratio'), 'Net_Debt_EBITDA': self._get_metric('Net Debt / EBITDA'), }, 'sector_percentiles': self.sector_percentiles if self.sector_percentiles else None } def _error_result(self, error_message: str) -> Dict: """Return error result""" return { 'ticker': self.ticker, 'recommendation': 'ERROR', 'error': error_message, 'final_score': 0.0, 'confidence': 0.0 } def print_analysis(self, result: Dict): """Print formatted analysis report""" print(f"\n{'='*80}") print(f"INVESTMENT RECOMMENDATION: {result['ticker']}") print(f"{'='*80}") if result['recommendation'] == 'ERROR': print(f"\n✗ ERROR: {result.get('error', 'Unknown error')}") return # Recommendation with color coding rec = result['recommendation'] if rec == 'BUY': print(f"\n🟢 RECOMMENDATION: {rec} (Score: {result['final_score']:+.2f})") elif rec == 'SELL': print(f"\n🔴 RECOMMENDATION: {rec} (Score: {result['final_score']:+.2f})") else: print(f"\n🟡 RECOMMENDATION: {rec} (Score: {result['final_score']:+.2f})") print(f"Confidence: {result['confidence']:.0%}") # Category scores print(f"\n{'-'*80}") print("CATEGORY SCORES (weighted)") print(f"{'-'*80}") for category, score in result['category_scores'].items(): weight = self.WEIGHTS[category] weighted_score = score * weight bar_length = int(abs(score) * 20) bar = '█' * bar_length print(f"{category.replace('_', ' ').title():25} {score:+.2f} ({weight:.0%}) → {weighted_score:+.3f} {bar}") # Key metrics print(f"\n{'-'*80}") print("KEY METRICS") print(f"{'-'*80}") for metric, value in result['key_metrics'].items(): if value is not None: print(f"{metric:30} {value:>12.2f}") # Reasoning print(f"\n{'-'*80}") print("INVESTMENT RATIONALE") print(f"{'-'*80}") for reason in result['reasoning']: print(f" {reason}") print(f"\n{'='*80}\n") def evaluate_stock(ticker: str, compare_to_sector: bool = True) -> Dict: """ Main function to evaluate a stock and get recommendation Args: ticker: Stock ticker symbol compare_to_sector: Whether to compare to sector peers Returns: Dictionary with recommendation and analysis """ decision = InvestmentDecision(ticker, compare_to_sector) result = decision.analyze() decision.print_analysis(result) return result if __name__ == "__main__": # Test with sample tickers test_tickers = ['AAPL', 'GOOGL', 'MSFT'] print("INVESTMENT DECISION MAKER TEST") print("=" * 80) results = [] for ticker in test_tickers: result = evaluate_stock(ticker, compare_to_sector=False) # Disable sector for speed results.append(result) # Summary comparison print("\n" + "=" * 80) print("SUMMARY COMPARISON") print("=" * 80) summary_df = pd.DataFrame([{ 'Ticker': r['ticker'], 'Recommendation': r['recommendation'], 'Score': r['final_score'], 'Confidence': r['confidence'], 'FCF Yield %': r['key_metrics']['FCF_Yield_%'], 'ROIC %': r['key_metrics']['ROIC_%'], 'PEG': r['key_metrics']['PEG_Ratio'] } for r in results]) print(summary_df.to_string(index=False)) # Save results summary_df.to_csv('investment_recommendations.csv', index=False) print(f"\n✓ Results saved to investment_recommendations.csv")