Papaflessas's picture
Deploy Signal Generator app
3fe0726
"""
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")