""" Investment decision engine. Combines all analyses to generate BUY/SELL/HOLD recommendations. """ import numpy as np from typing import Dict, List, Tuple from datetime import datetime class InvestmentDecisionEngine: """Generates investment recommendations based on comprehensive analysis""" def __init__(self, financial_data: Dict, fundamental_analysis: Dict, sector_analysis: Dict, valuation_analysis: Dict): """ Initialize decision engine Args: financial_data: Raw financial data fundamental_analysis: Results from FinancialAnalyzer sector_analysis: Results from SectorAnalyzer valuation_analysis: Results from ValuationEngine """ self.ticker = financial_data.get('ticker') self.company_info = financial_data.get('company_info', {}) self.current_price = financial_data.get('metrics', {}).get('current_price', 0) self.fundamental = fundamental_analysis self.sector = sector_analysis self.valuation = valuation_analysis self.score_weights = { 'fundamental': 0.35, 'sector': 0.25, 'valuation': 0.40 } def score_fundamentals(self) -> Dict: """ Score company fundamentals (0-100) Returns: Fundamental scores and assessment """ scores = {} # Growth score (0-25) growth = self.fundamental.get('growth_analysis', {}) growth_quality = growth.get('growth_quality', 'Insufficient Data') growth_scores = { 'Strong': 25, 'Good': 18, 'Moderate': 12, 'Weak': 5, 'Insufficient Data': 10 } scores['growth'] = growth_scores.get(growth_quality, 10) # Margin score (0-25) margins = self.fundamental.get('margin_analysis', {}) margin_quality = margins.get('margin_quality', 'Insufficient Data') margin_scores = { 'Excellent': 25, 'Good': 18, 'Moderate': 12, 'Weak': 5, 'Insufficient Data': 10 } scores['margins'] = margin_scores.get(margin_quality, 10) # Returns score (0-25) returns = self.fundamental.get('returns_analysis', {}) returns_quality = returns.get('returns_quality', 'Insufficient Data') returns_scores = { 'Excellent': 25, 'Good': 18, 'Moderate': 12, 'Weak': 5, 'Insufficient Data': 10 } scores['returns'] = returns_scores.get(returns_quality, 10) # Cash flow score (0-25) cash_flow = self.fundamental.get('cash_flow_analysis', {}) cf_quality = cash_flow.get('cash_flow_quality', 'Insufficient Data') cf_scores = { 'Excellent': 25, 'Good': 18, 'Moderate': 12, 'Weak': 5, 'Negative': 0, 'Insufficient Data': 10 } scores['cash_flow'] = cf_scores.get(cf_quality, 10) total_score = sum(scores.values()) return { 'scores': scores, 'total': total_score, 'max': 100, 'percentage': total_score, 'grade': self._get_grade(total_score), 'assessment': self._assess_fundamentals(total_score) } def score_sector_position(self) -> Dict: """ Score company's position within sector (0-100) Returns: Sector position scores and assessment """ scores = {} # Sector sentiment (0-30) sentiment = self.sector.get('sector_sentiment', {}) overall_sentiment = sentiment.get('overall_sentiment', 'Neutral') if 'Positive' in overall_sentiment or 'tailwinds' in overall_sentiment: scores['sector_sentiment'] = 30 elif 'Negative' in overall_sentiment or 'headwinds' in overall_sentiment: scores['sector_sentiment'] = 10 else: scores['sector_sentiment'] = 20 # Competitive position (0-35) ranking = self.sector.get('sector_ranking', {}) overall_position = ranking.get('overall_position', 'Unknown') position_scores = { 'Top 20%': 35, 'Top 40%': 25, 'Middle': 18, 'Bottom 40%': 10, 'Bottom 20%': 5, 'Unknown': 18 } scores['competitive_position'] = position_scores.get(overall_position, 18) # Relative profitability (0-20) profitability_comp = self.sector.get('profitability_comparison', {}) prof_vs_sector = profitability_comp.get('profitability_vs_sector', 'Unknown') prof_scores = { 'Top Performer': 20, 'Above Average': 15, 'Below Average': 8, 'Bottom Performer': 3, 'Unknown': 10 } scores['relative_profitability'] = prof_scores.get(prof_vs_sector, 10) # Relative growth (0-15) growth_comp = self.sector.get('growth_comparison', {}) growth_vs_sector = growth_comp.get('growth_vs_sector', 'Unknown') growth_scores = { 'Fast Grower': 15, 'Above Average Growth': 11, 'Below Average Growth': 6, 'Lagging Sector': 2, 'Unknown': 8 } scores['relative_growth'] = growth_scores.get(growth_vs_sector, 8) total_score = sum(scores.values()) return { 'scores': scores, 'total': total_score, 'max': 100, 'percentage': total_score, 'grade': self._get_grade(total_score), 'assessment': self._assess_sector_position(total_score, overall_sentiment) } def score_valuation(self) -> Dict: """ Score valuation attractiveness (0-100) Returns: Valuation scores and assessment """ scores = {} # DCF valuation (0-40) dcf = self.valuation.get('dcf_valuation', {}) dcf_upside = dcf.get('upside_percent', 0) if 'error' not in dcf: if dcf_upside > 30: scores['dcf'] = 40 elif dcf_upside > 15: scores['dcf'] = 30 elif dcf_upside > 0: scores['dcf'] = 20 elif dcf_upside > -15: scores['dcf'] = 10 else: scores['dcf'] = 0 else: scores['dcf'] = 20 # Neutral if DCF not applicable # Relative valuation (0-40) relative = self.valuation.get('relative_valuation', {}) avg_upside = relative.get('average_upside', 0) if avg_upside != 0: if avg_upside > 25: scores['relative'] = 40 elif avg_upside > 10: scores['relative'] = 30 elif avg_upside > 0: scores['relative'] = 20 elif avg_upside > -15: scores['relative'] = 10 else: scores['relative'] = 0 else: scores['relative'] = 20 # Neutral if not available # Margin of safety (0-20) mos = self.valuation.get('margin_of_safety', {}) dcf_mos = mos.get('dcf_margin_of_safety', {}) mos_percent = dcf_mos.get('margin_percent', 0) if mos_percent > 30: scores['margin_of_safety'] = 20 elif mos_percent > 20: scores['margin_of_safety'] = 15 elif mos_percent > 10: scores['margin_of_safety'] = 10 elif mos_percent > 0: scores['margin_of_safety'] = 5 else: scores['margin_of_safety'] = 0 total_score = sum(scores.values()) return { 'scores': scores, 'total': total_score, 'max': 100, 'percentage': total_score, 'grade': self._get_grade(total_score), 'assessment': self._assess_valuation(total_score) } def calculate_overall_score(self) -> Dict: """ Calculate weighted overall investment score Returns: Overall score and breakdown """ fundamental_score = self.score_fundamentals() sector_score = self.score_sector_position() valuation_score = self.score_valuation() # Calculate weighted score weighted_score = ( fundamental_score['percentage'] * self.score_weights['fundamental'] + sector_score['percentage'] * self.score_weights['sector'] + valuation_score['percentage'] * self.score_weights['valuation'] ) return { 'overall_score': weighted_score, 'max_score': 100, 'grade': self._get_grade(weighted_score), 'breakdown': { 'fundamental': { 'score': fundamental_score['percentage'], 'weight': self.score_weights['fundamental'], 'weighted_score': fundamental_score['percentage'] * self.score_weights['fundamental'], 'details': fundamental_score }, 'sector': { 'score': sector_score['percentage'], 'weight': self.score_weights['sector'], 'weighted_score': sector_score['percentage'] * self.score_weights['sector'], 'details': sector_score }, 'valuation': { 'score': valuation_score['percentage'], 'weight': self.score_weights['valuation'], 'weighted_score': valuation_score['percentage'] * self.score_weights['valuation'], 'details': valuation_score } } } def generate_recommendation(self) -> str: """ Generate BUY/SELL/HOLD recommendation Returns: Investment recommendation """ overall = self.calculate_overall_score() score = overall['overall_score'] # Base recommendation on score if score >= 70: base_rec = "STRONG BUY" elif score >= 60: base_rec = "BUY" elif score >= 50: base_rec = "HOLD" elif score >= 40: base_rec = "SELL" else: base_rec = "STRONG SELL" # Adjust based on specific red flags red_flags = self.identify_red_flags() if red_flags['critical_issues']: if base_rec in ["STRONG BUY", "BUY"]: base_rec = "HOLD" elif base_rec == "HOLD": base_rec = "SELL" return base_rec def identify_red_flags(self) -> Dict: """ Identify critical red flags Returns: Red flags and warnings """ red_flags = { 'critical_issues': [], 'warnings': [], 'positive_signs': [] } # Check cash flow cf_analysis = self.fundamental.get('cash_flow_analysis', {}) if cf_analysis.get('free_cash_flow', 0) < 0: red_flags['critical_issues'].append("Negative free cash flow") # Check earnings growth = self.fundamental.get('growth_analysis', {}) if growth.get('net_income_growth_yoy', 0) < -0.10: red_flags['warnings'].append("Declining earnings (>10% drop)") # Check margins margins = self.fundamental.get('margin_analysis', {}) if margins.get('operating_margin_trend', 0) < -0.02: red_flags['warnings'].append("Contracting operating margins") # Check debt health = self.fundamental.get('financial_health', {}) interest_coverage = health.get('interest_coverage') if interest_coverage and interest_coverage < 2: red_flags['critical_issues'].append("Low interest coverage (<2x)") # Check valuation valuation_score = self.score_valuation() if valuation_score['percentage'] < 25: red_flags['warnings'].append("Expensive valuation") # Check sector sector_sentiment = self.sector.get('sector_sentiment', {}) if 'Negative' in sector_sentiment.get('overall_sentiment', ''): red_flags['warnings'].append("Sector facing headwinds") # Positive signs if cf_analysis.get('fcf_positive_trend', False): red_flags['positive_signs'].append("Growing free cash flow") if margins.get('operating_leverage', False): red_flags['positive_signs'].append("Expanding operating margins") returns = self.fundamental.get('returns_analysis', {}) if returns.get('roic', 0) > 0.15: red_flags['positive_signs'].append("Strong return on invested capital (>15%)") return red_flags def generate_confidence_score(self) -> Dict: """ Generate confidence level in recommendation Returns: Confidence metrics """ confidence_factors = [] # Data quality fundamental = self.score_fundamentals() if fundamental['grade'] != 'F': confidence_factors.append(0.3) # Sector data available if self.sector.get('peer_count', 0) > 0: confidence_factors.append(0.25) # Valuation methods available valuation = self.valuation.get('relative_valuation', {}) methods_available = sum([ 'pe_valuation' in valuation, 'peg_valuation' in valuation, 'pb_valuation' in valuation ]) confidence_factors.append(methods_available * 0.15) # Score consistency overall = self.calculate_overall_score() breakdown = overall['breakdown'] scores = [ breakdown['fundamental']['score'], breakdown['sector']['score'], breakdown['valuation']['score'] ] std_dev = np.std(scores) if std_dev < 15: # Scores are consistent confidence_factors.append(0.15) confidence = sum(confidence_factors) return { 'confidence_score': min(confidence, 1.0), 'confidence_level': self._get_confidence_level(confidence), 'factors': confidence_factors } def _get_grade(self, score: float) -> str: """Convert score to letter grade""" if score >= 90: return 'A+' elif score >= 80: return 'A' elif score >= 70: return 'B' elif score >= 60: return 'C' elif score >= 50: return 'D' else: return 'F' def _get_confidence_level(self, confidence: float) -> str: """Convert confidence score to level""" if confidence >= 0.8: return "Very High" elif confidence >= 0.6: return "High" elif confidence >= 0.4: return "Moderate" else: return "Low" def _assess_fundamentals(self, score: float) -> str: """Assess fundamental strength""" if score >= 80: return "Excellent fundamentals - Strong business" elif score >= 60: return "Good fundamentals - Solid business" elif score >= 40: return "Moderate fundamentals - Average business" else: return "Weak fundamentals - Concerning business" def _assess_sector_position(self, score: float, sentiment: str) -> str: """Assess sector position""" if score >= 70: return f"Leading position in sector ({sentiment})" elif score >= 50: return f"Average position in sector ({sentiment})" else: return f"Weak position in sector ({sentiment})" def _assess_valuation(self, score: float) -> str: """Assess valuation""" if score >= 70: return "Attractive valuation - Undervalued" elif score >= 50: return "Fair valuation - Reasonably priced" elif score >= 30: return "Full valuation - Fairly valued to slightly expensive" else: return "Expensive valuation - Overvalued" def generate_investment_thesis(self) -> str: """ Generate investment thesis narrative Returns: Investment thesis text """ recommendation = self.generate_recommendation() overall = self.calculate_overall_score() red_flags = self.identify_red_flags() thesis = [] # Opening thesis.append(f"**{recommendation}** - Overall Score: {overall['overall_score']:.1f}/100 ({overall['grade']})") thesis.append("") # Fundamental assessment fund_score = overall['breakdown']['fundamental']['details'] thesis.append(f"**Fundamentals ({fund_score['percentage']:.0f}/100):** {fund_score['assessment']}") # Sector position sector_score = overall['breakdown']['sector']['details'] thesis.append(f"**Sector Position ({sector_score['percentage']:.0f}/100):** {sector_score['assessment']}") # Valuation val_score = overall['breakdown']['valuation']['details'] thesis.append(f"**Valuation ({val_score['percentage']:.0f}/100):** {val_score['assessment']}") thesis.append("") # Key positives if red_flags['positive_signs']: thesis.append("**Key Strengths:**") for sign in red_flags['positive_signs']: thesis.append(f" ✓ {sign}") thesis.append("") # Key concerns if red_flags['critical_issues'] or red_flags['warnings']: thesis.append("**Key Concerns:**") for issue in red_flags['critical_issues']: thesis.append(f" ⚠ {issue}") for warning in red_flags['warnings']: thesis.append(f" • {warning}") thesis.append("") # Confidence confidence = self.generate_confidence_score() thesis.append(f"**Confidence Level:** {confidence['confidence_level']} ({confidence['confidence_score']*100:.0f}%)") return "\n".join(thesis) def generate_decision_report(self) -> Dict: """ Generate comprehensive investment decision report Returns: Complete decision analysis """ return { 'ticker': self.ticker, 'company_name': self.company_info.get('company_name', self.ticker), 'current_price': self.current_price, 'analysis_date': datetime.now().isoformat(), 'recommendation': self.generate_recommendation(), 'overall_score': self.calculate_overall_score(), 'confidence': self.generate_confidence_score(), 'red_flags': self.identify_red_flags(), 'investment_thesis': self.generate_investment_thesis() } if __name__ == "__main__": print("This module is meant to be imported and used with results from other analyzers")