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