Building an AI Agent for Value Investing

A step-by-step guide for beginners

Step 6: Fine-Tune an LLM or Rule-Based Engine

Now that you've implemented the basic workflow for your value investing AI agent, it's time to enhance its decision-making capabilities. In this step, we'll explore different approaches to powering your agent's analysis and recommendation engine, from simple rule-based systems to advanced language models.

Choosing the Right Approach

There are several approaches to implementing the decision-making engine for your value investing AI agent, each with its own advantages and trade-offs:

Decision Engine Options

  • Rule-Based Systems: If-else logic based on metrics thresholds
  • LLM-Powered Systems: Using models like GPT or Claude for analysis and explanations
  • Hybrid Approaches: Combining rule-based calculations with LLM-generated explanations

Rule-Based Systems

Advantages:

  • Transparent and explainable decisions
  • No dependency on external API services
  • Consistent and predictable behavior
  • Lower computational requirements

Disadvantages:

  • Limited flexibility for handling nuanced situations
  • Requires manual updating of rules as market conditions change
  • Can become complex and difficult to maintain as rules multiply

LLM-Powered Systems

Advantages:

  • Can generate natural language explanations and insights
  • Able to incorporate contextual information and market trends
  • Can adapt to new information without explicit reprogramming
  • Better at handling qualitative factors like economic moats

Disadvantages:

  • Potential "black box" decision-making
  • Dependency on external API services (cost and availability)
  • May introduce inconsistency or hallucinations
  • Higher computational and financial costs

Hybrid Approaches

Advantages:

  • Combines the reliability of rule-based calculations with the flexibility of LLMs
  • Can use LLMs for explanation generation while keeping core logic transparent
  • More robust against LLM hallucinations by constraining outputs

Disadvantages:

  • More complex architecture to implement and maintain
  • Still has some dependency on external services
  • Requires careful integration to ensure consistency

Implementing a Rule-Based Engine

Let's start by implementing a rule-based engine for our value investing agent. This approach uses explicit if-else logic based on financial metrics and predefined thresholds:

Python: Rule-Based Value Investing Engine
# rule_based_engine.py

import pandas as pd
import numpy as np

class RuleBasedValueEngine:
    """
    A rule-based decision engine for value investing analysis.
    """
    
    def __init__(self, criteria=None):
        """
        Initialize the rule-based engine with value investing criteria.
        
        Parameters:
        -----------
        criteria : dict, optional
            Dictionary defining value investing criteria and thresholds
        """
        # Set default criteria if none provided
        self.criteria = criteria or {
            'pe_ratio': {'max': 15, 'weight': 0.15},
            'pb_ratio': {'max': 3, 'weight': 0.15},
            'roe': {'min': 0.15, 'weight': 0.15},
            'debt_to_equity': {'max': 1.0, 'weight': 0.1},
            'fcf_yield': {'min': 0.02, 'weight': 0.15},
            'dividend_yield': {'min': 0.01, 'weight': 0.1},
            'earnings_growth': {'min': 0.05, 'weight': 0.1},
            'margin_of_safety': {'min': 0.2, 'weight': 0.1}
        }
    
    def evaluate_metric(self, metric_name, value):
        """
        Evaluate a single metric against criteria.
        
        Parameters:
        -----------
        metric_name : str
            Name of the metric to evaluate
        value : float
            Value of the metric
            
        Returns:
        --------
        tuple
            (score, explanation)
        """
        if metric_name not in self.criteria or pd.isna(value):
            return 0, f"No data available for {metric_name}"
        
        criterion = self.criteria[metric_name]
        score = 0
        
        # For metrics where lower is better (like P/E ratio)
        if 'max' in criterion:
            if value <= criterion['max']:
                # Calculate score as percentage of maximum (inverted)
                score = criterion['weight'] * (1 - value / criterion['max'])
                if score < 0:
                    score = 0
                explanation = f"{metric_name} of {value:.2f} is below the threshold of {criterion['max']}, which is positive."
            else:
                explanation = f"{metric_name} of {value:.2f} exceeds the threshold of {criterion['max']}, which is negative."
        
        # For metrics where higher is better (like ROE)
        elif 'min' in criterion:
            if value >= criterion['min']:
                # Calculate score as percentage above minimum
                score = criterion['weight'] * min(1, (value - criterion['min']) / criterion['min'])
                explanation = f"{metric_name} of {value:.2f} is above the threshold of {criterion['min']}, which is positive."
            else:
                explanation = f"{metric_name} of {value:.2f} is below the threshold of {criterion['min']}, which is negative."
        
        return score, explanation
    
    def analyze(self, company_data):
        """
        Analyze company data using rule-based criteria.
        
        Parameters:
        -----------
        company_data : dict
            Dictionary containing company financial metrics
            
        Returns:
        --------
        dict
            Analysis results including scores and recommendation
        """
        # Initialize results
        results = {
            'company_name': company_data.get('name', 'Unknown Company'),
            'ticker': company_data.get('ticker', 'Unknown'),
            'total_score': 0,
            'max_possible_score': sum(criterion['weight'] for criterion in self.criteria.values()),
            'metric_scores': {},
            'explanations': []
        }
        
        # Evaluate each metric
        for metric_name in self.criteria.keys():
            if metric_name in company_data:
                score, explanation = self.evaluate_metric(metric_name, company_data[metric_name])
                results['metric_scores'][metric_name] = score
                results['total_score'] += score
                results['explanations'].append(explanation)
        
        # Calculate percentage score
        if results['max_possible_score'] > 0:
            results['percentage_score'] = (results['total_score'] / results['max_possible_score']) * 100
        else:
            results['percentage_score'] = 0
        
        # Generate recommendation based on score
        results['recommendation'] = self._generate_recommendation(results)
        
        return results
    
    def _generate_recommendation(self, results):
        """
        Generate investment recommendation based on analysis results.
        
        Parameters:
        -----------
        results : dict
            Analysis results
            
        Returns:
        --------
        dict
            Recommendation details
        """
        score = results['percentage_score']
        
        # Define recommendation thresholds
        if score >= 70:
            rating = "Strong Buy"
            explanation = f"{results['company_name']} ({results['ticker']}) appears significantly undervalued based on our value investing criteria, with a score of {score:.1f}%."
            action = "Consider allocating a larger position in your portfolio, subject to your overall investment strategy and risk tolerance."
        elif score >= 60:
            rating = "Buy"
            explanation = f"{results['company_name']} ({results['ticker']}) appears moderately undervalued based on our value investing criteria, with a score of {score:.1f}%."
            action = "Consider initiating or adding to a position, while maintaining diversification."
        elif score >= 40:
            rating = "Hold"
            explanation = f"{results['company_name']} ({results['ticker']}) appears fairly valued based on our value investing criteria, with a score of {score:.1f}%."
            action = "If you already own shares, consider holding. If not, it may be worth watching for a better entry point."
        elif score >= 30:
            rating = "Sell"
            explanation = f"{results['company_name']} ({results['ticker']}) appears moderately overvalued based on our value investing criteria, with a score of {score:.1f}%."
            action = "Consider reducing your position or looking for better value opportunities elsewhere."
        else:
            rating = "Strong Sell"
            explanation = f"{results['company_name']} ({results['ticker']}) appears significantly overvalued based on our value investing criteria, with a score of {score:.1f}%."
            action = "Consider exiting your position and reallocating to better value opportunities."
        
        # Compile key insights
        strengths = []
        weaknesses = []
        
        for metric, score in results['metric_scores'].items():
            criterion = self.criteria[metric]
            max_score = criterion['weight']
            
            if score > max_score * 0.7:
                strengths.append(metric)
            elif score < max_score * 0.3:
                weaknesses.append(metric)
        
        strengths_text = ""
        if strengths:
            strengths_text = "Key strengths: " + ", ".join(strengths) + "."
            
        weaknesses_text = ""
        if weaknesses:
            weaknesses_text = "Areas of concern: " + ", ".join(weaknesses) + "."
        
        return {
            'rating': rating,
            'explanation': explanation,
            'action': action,
            'strengths': strengths_text,
            'weaknesses': weaknesses_text
        }

# Example usage
if __name__ == "__main__":
    # Create the engine
    engine = RuleBasedValueEngine()
    
    # Sample company data
    company_data = {
        'name': 'Value Corp',
        'ticker': 'VALU',
        'pe_ratio': 12.5,
        'pb_ratio': 1.8,
        'roe': 0.18,
        'debt_to_equity': 0.7,
        'fcf_yield': 0.04,
        'dividend_yield': 0.025,
        'earnings_growth': 0.08,
        'margin_of_safety': 0.25
    }
    
    # Analyze the company
    results = engine.analyze(company_data)
    
    # Print results
    print(f"Analysis Results for {results['company_name']} ({results['ticker']})")
    print(f"Overall Score: {results['percentage_score']:.1f}%")
    print(f"Recommendation: {results['recommendation']['rating']}")
    print(f"Explanation: {results['recommendation']['explanation']}")
    print(f"Suggested Action: {results['recommendation']['action']}")
    
    if results['recommendation']['strengths']:
        print(results['recommendation']['strengths'])
    
    if results['recommendation']['weaknesses']:
        print(results['recommendation']['weaknesses'])
    
    print("\nDetailed Metric Scores:")
    for metric, score in results['metric_scores'].items():
        max_score = engine.criteria[metric]['weight']
        print(f"- {metric}: {score:.2f}/{max_score:.2f}")

Implementing an LLM-Powered Engine

Now let's explore how to implement an LLM-powered engine for more sophisticated analysis and natural language explanations:

Python: LLM-Powered Value Investing Engine
# llm_powered_engine.py

import pandas as pd
import numpy as np
import json
import requests
from datetime import datetime

class LLMValueEngine:
    """
    An LLM-powered decision engine for value investing analysis.
    """
    
    def __init__(self, api_key=None, model="gpt-4", criteria=None):
        """
        Initialize the LLM-powered engine.
        
        Parameters:
        -----------
        api_key : str, optional
            API key for the LLM service
        model : str, optional
            Model to use for analysis
        criteria : dict, optional
            Dictionary defining value investing criteria and thresholds
        """
        self.api_key = api_key
        self.model = model
        
        # Set default criteria if none provided
        self.criteria = criteria or {
            'pe_ratio': {'max': 15, 'weight': 0.15},
            'pb_ratio': {'max': 3, 'weight': 0.15},
            'roe': {'min': 0.15, 'weight': 0.15},
            'debt_to_equity': {'max': 1.0, 'weight': 0.1},
            'fcf_yield': {'min': 0.02, 'weight': 0.15},
            'dividend_yield': {'min': 0.01, 'weight': 0.1},
            'earnings_growth': {'min': 0.05, 'weight': 0.1},
            'margin_of_safety': {'min': 0.2, 'weight': 0.1}
        }
    
    def analyze(self, company_data):
        """
        Analyze company data using LLM-powered analysis.
        
        Parameters:
        -----------
        company_data : dict
            Dictionary containing company financial metrics
            
        Returns:
        --------
        dict
            Analysis results including scores and recommendation
        """
        # First, calculate basic scores using rule-based approach
        results = self._calculate_base_scores(company_data)
        
        # Then, enhance with LLM analysis
        if self.api_key:
            llm_analysis = self._get_llm_analysis(company_data, results)
            results.update(llm_analysis)
        else:
            # If no API key, provide a message about LLM enhancement
            results['llm_enhanced'] = False
            results['note'] = "Analysis is based on rule-based calculations only. Provide an API key to enable LLM-enhanced analysis."
        
        return results
    
    def _calculate_base_scores(self, company_data):
        """
        Calculate base scores using rule-based approach.
        
        Parameters:
        -----------
        company_data : dict
            Dictionary containing company financial metrics
            
        Returns:
        --------
        dict
            Base analysis results
        """
        # Initialize results
        results = {
            'company_name': company_data.get('name', 'Unknown Company'),
            'ticker': company_data.get('ticker', 'Unknown'),
            'total_score': 0,
            'max_possible_score': sum(criterion['weight'] for criterion in self.criteria.values()),
            'metric_scores': {},
            'metrics_data': {}
        }
        
        # Evaluate each metric
        for metric_name in self.criteria.keys():
            if metric_name in company_data and not pd.isna(company_data[metric_name]):
                value = company_data[metric_name]
                criterion = self.criteria[metric_name]
                score = 0
                
                # Store the metric value for LLM context
                results['metrics_data'][metric_name] = value
                
                # For metrics where lower is better (like P/E ratio)
                if 'max' in criterion:
                    if value <= criterion['max']:
                        # Calculate score as percentage of maximum (inverted)
                        score = criterion['weight'] * (1 - value / criterion['max'])
                        if score < 0:
                            score = 0
                    # else score remains 0
                
                # For metrics where higher is better (like ROE)
                elif 'min' in criterion:
                    if value >= criterion['min']:
                        # Calculate score as percentage above minimum
                        score = criterion['weight'] * min(1, (value - criterion['min']) / criterion['min'])
                    # else score remains 0
                
                results['metric_scores'][metric_name] = score
                results['total_score'] += score
        
        # Calculate percentage score
        if results['max_possible_score'] > 0:
            results['percentage_score'] = (results['total_score'] / results['max_possible_score']) * 100
        else:
            results['percentage_score'] = 0
        
        # Generate basic recommendation based on score
        score = results['percentage_score']
        if score >= 70:
            results['base_rating'] = "Strong Buy"
        elif score >= 60:
            results['base_rating'] = "Buy"
        elif score >= 40:
            results['base_rating'] = "Hold"
        elif score >= 30:
            results['base_rating'] = "Sell"
        else:
            results['base_rating'] = "Strong Sell"
        
        return results
    
    def _get_llm_analysis(self, company_data, base_results):
        """
        Get enhanced analysis from LLM.
        
        Parameters:
        -----------
        company_data : dict
            Dictionary containing company financial metrics
        base_results : dict
            Base analysis results from rule-based calculation
            
        Returns:
        --------
        dict
            LLM-enhanced analysis
        """
        # In a real implementation, this would call an LLM API
        # For this example, we'll simulate the LLM response
        
        # Prepare the prompt for the LLM
        prompt = self._prepare_llm_prompt(company_data, base_results)
        
        try:
            # This is where you would make the actual API call
            # For example, using OpenAI's API:
            """
            response = requests.post(
                "https://api.openai.com/v1/chat/completions",
                headers={
                    "Authorization": f"Bearer {self.api_key}",
                    "Content-Type": "application/json"
                },
                json={
                    "model": self.model,
                    "messages": [
                        {"role": "system", "content": "You are a value investing expert."},
                        {"role": "user", "content": prompt}
                    ],
                    "temperature": 0.3
                }
            )
            llm_response = response.json()["choices"][0]["message"]["content"]
            """
            
            # For this example, we'll simulate the LLM response
            llm_response = self._simulate_llm_response(company_data, base_results)
            
            # Parse the LLM response
            # In a real implementation, you would need to ensure the LLM returns a structured format
            # or use a parsing function to extract the relevant information
            try:
                llm_analysis = json.loads(llm_response)
            except:
                # Fallback if the response isn't valid JSON
                llm_analysis = {
                    "recommendation": {
                        "rating": base_results['base_rating'],
                        "explanation": "Unable to parse LLM response. Using base rating instead.",
                        "insights": []
                    }
                }
            
            # Mark as LLM-enhanced
            llm_analysis['llm_enhanced'] = True
            
            return llm_analysis
            
        except Exception as e:
            # If there's an error with the LLM, fall back to the base results
            return {
                "llm_enhanced": False,
                "recommendation": {
                    "rating": base_results['base_rating'],
                    "explanation": f"Error getting LLM analysis: {str(e)}. Using base rating instead.",
                    "insights": []
                }
            }
    
    def _prepare_llm_prompt(self, company_data, base_results):
        """
        Prepare the prompt for the LLM.
        
        Parameters:
        -----------
        company_data : dict
            Dictionary containing company financial metrics
        base_results : dict
            Base analysis results from rule-based calculation
            
        Returns:
        --------
        str
            Prompt for the LLM
        """
        # Format the company data and base results into a prompt
        prompt = f"""
        You are a value investing expert analyzing {company_data.get('name', 'a company')} ({company_data.get('ticker', 'Unknown')}).
        
        Here are the key financial metrics:
        """
        
        # Add metrics data
        for metric, value in base_results['metrics_data'].items():
            if metric == 'pe_ratio':
                prompt += f"- P/E Ratio: {value:.2f}\n"
            elif metric == 'pb_ratio':
                prompt += f"- P/B Ratio: {value:.2f}\n"
            elif metric == 'roe':
                prompt += f"- Return on Equity (ROE): {value*100:.2f}%\n"
            elif metric == 'debt_to_equity':
                prompt += f"- Debt-to-Equity Ratio: {value:.2f}\n"
            elif metric == 'fcf_yield':
                prompt += f"- Free Cash Flow Yield: {value*100:.2f}%\n"
            elif metric == 'dividend_yield':
                prompt += f"- Dividend Yield: {value*100:.2f}%\n"
            elif metric == 'earnings_growth':
                prompt += f"- Earnings Growth Rate: {value*100:.2f}%\n"
            elif metric == 'margin_of_safety':
                prompt += f"- Margin of Safety: {value*100:.2f}%\n"
            else:
                prompt += f"- {metric}: {value}\n"
        
        # Add base score and rating
        prompt += f"""
        Based on our value investing criteria, this company has a score of {base_results['percentage_score']:.1f}% and a base rating of {base_results['base_rating']}.
        
        Please provide an in-depth value investing analysis of this company. Include:
        1. A final investment recommendation (Strong Buy, Buy, Hold, Sell, or Strong Sell)
        2. A detailed explanation of your recommendation
        3. Key insights about the company's strengths and weaknesses from a value investing perspective
        4. Any potential risks or opportunities that might not be captured by the metrics alone
        
        Return your analysis in the following JSON format:
        {{
            "recommendation": {{
                "rating": "Your final rating",
                "explanation": "Your detailed explanation",
                "insights": [
                    "Key insight 1",
                    "Key insight 2",
                    ...
                ],
                "risks": [
                    "Risk 1",
                    "Risk 2",
                    ...
                ],
                "opportunities": [
                    "Opportunity 1",
                    "Opportunity 2",
                    ...
                ]
            }}
        }}
        """
        
        return prompt
    
    def _simulate_llm_response(self, company_data, base_results):
        """
        Simulate an LLM response for demonstration purposes.
        
        Parameters:
        -----------
        company_data : dict
            Dictionary containing company financial metrics
        base_results : dict
            Base analysis results from rule-based calculation
            
        Returns:
        --------
        str
            Simulated LLM response
        """
        # Get company name and ticker
        company_name = company_data.get('name', 'the company')
        ticker = company_data.get('ticker', 'Unknown')
        
        # Get base score and rating
        score = base_results['percentage_score']
        base_rating = base_results['base_rating']
        
        # Simulate different responses based on the score
        if score >= 70:
            rating = "Strong Buy"
            explanation = f"{company_name} ({ticker}) presents a compelling value investment opportunity. With a strong score of {score:.1f}%, the company exhibits multiple characteristics of an undervalued stock according to traditional value investing principles."
            insights = [
                f"The P/E ratio of {base_results['metrics_data'].get('pe_ratio', 'N/A'):.2f} is significantly below industry averages, suggesting potential undervaluation.",
                f"Strong return on equity (ROE) of {base_results['metrics_data'].get('roe', 0)*100:.2f}% indicates efficient use of shareholder capital.",
                f"The company maintains a healthy balance sheet with a manageable debt-to-equity ratio of {base_results['metrics_data'].get('debt_to_equity', 'N/A'):.2f}."
            ]
            risks = [
                "Market sentiment shifts could temporarily impact stock price despite strong fundamentals.",
                "Potential industry disruption should be monitored for long-term impact."
            ]
            opportunities = [
                "Current undervaluation provides a substantial margin of safety.",
                "Consistent dividend yield of {:.2f}% offers income potential while waiting for market recognition.".format(base_results['metrics_data'].get('dividend_yield', 0)*100)
            ]
        elif score >= 60:
            rating = "Buy"
            explanation = f"{company_name} ({ticker}) represents a good value investment opportunity with a score of {score:.1f}%. While not extremely undervalued, the company demonstrates solid fundamentals and reasonable valuation metrics."
            insights = [
                f"The P/E ratio of {base_results['metrics_data'].get('pe_ratio', 'N/A'):.2f} is below our threshold, indicating reasonable valuation.",
                f"Return on equity (ROE) of {base_results['metrics_data'].get('roe', 0)*100:.2f}% shows good profitability.",
                f"Free cash flow yield of {base_results['metrics_data'].get('fcf_yield', 0)*100:.2f}% suggests the company generates sufficient cash relative to its valuation."
            ]
            risks = [
                "Moderate valuation means less margin of safety compared to 'Strong Buy' recommendations.",
                "Changes in interest rates could affect relative attractiveness of the stock."
            ]
            opportunities = [
                "Potential for valuation multiple expansion if growth exceeds current expectations.",
                "Dividend growth potential based on strong free cash flow generation."
            ]
        elif score >= 40:
            rating = "Hold"
            explanation = f"{company_name} ({ticker}) appears fairly valued with a score of {score:.1f}%. The company shows a mix of positive and negative factors from a value investing perspective."
            insights = [
                "Current valuation metrics are neither particularly attractive nor concerning.",
                f"Return on equity (ROE) of {base_results['metrics_data'].get('roe', 0)*100:.2f}% is adequate but not exceptional.",
                "The company's financial position appears stable but doesn't offer a compelling value proposition at current prices."
            ]
            risks = [
                "Limited margin of safety at current valuation levels.",
                "May underperform in a market correction due to fair valuation."
            ]
            opportunities = [
                "Could become attractive on any significant price pullbacks.",
                "Worth monitoring for changes in fundamentals that might improve value metrics."
            ]
        elif score >= 30:
            rating = "Sell"
            explanation = f"{company_name} ({ticker}) appears overvalued with a score of {score:.1f}%. Several key metrics suggest the stock may not represent a good value at current prices."
            insights = [
                f"The P/E ratio of {base_results['metrics_data'].get('pe_ratio', 'N/A'):.2f} is above our value threshold, indicating potential overvaluation.",
                f"Return on equity (ROE) of {base_results['metrics_data'].get('roe', 0)*100:.2f}% is below what we typically look for in value investments.",
                "Current valuation appears to price in optimistic growth expectations."
            ]
            risks = [
                "Elevated valuation leaves little room for execution errors.",
                "Potential for significant price correction if growth slows."
            ]
            opportunities = [
                "Consider revisiting if the stock price decreases substantially.",
                "Potential for short-term trading opportunities, though not aligned with value investing principles."
            ]
        else:
            rating = "Strong Sell"
            explanation = f"{company_name} ({ticker}) appears significantly overvalued with a low score of {score:.1f}%. The company fails to meet most of our value investing criteria."
            insights = [
                f"The P/E ratio of {base_results['metrics_data'].get('pe_ratio', 'N/A'):.2f} is substantially above value thresholds.",
                f"Low return on equity (ROE) of {base_results['metrics_data'].get('roe', 0)*100:.2f}% indicates potential issues with profitability.",
                f"High debt-to-equity ratio of {base_results['metrics_data'].get('debt_to_equity', 'N/A'):.2f} raises concerns about financial stability."
            ]
            risks = [
                "High valuation creates significant downside risk.",
                "Current price appears to be disconnected from fundamental value."
            ]
            opportunities = [
                "Consider revisiting only after a substantial correction or fundamental improvement.",
                "Capital might be better allocated to more attractively valued alternatives."
            ]
        
        # Construct the simulated response
        response = {
            "recommendation": {
                "rating": rating,
                "explanation": explanation,
                "insights": insights,
                "risks": risks,
                "opportunities": opportunities
            }
        }
        
        return json.dumps(response, indent=2)

# Example usage
if __name__ == "__main__":
    # Create the engine (without API key for simulation)
    engine = LLMValueEngine()
    
    # Sample company data
    company_data = {
        'name': 'Value Corp',
        'ticker': 'VALU',
        'pe_ratio': 12.5,
        'pb_ratio': 1.8,
        'roe': 0.18,
        'debt_to_equity': 0.7,
        'fcf_yield': 0.04,
        'dividend_yield': 0.025,
        'earnings_growth': 0.08,
        'margin_of_safety': 0.25
    }
    
    # Analyze the company
    results = engine.analyze(company_data)
    
    # Print results
    print(f"Analysis Results for {results['company_name']} ({results['ticker']})")
    print(f"Overall Score: {results['percentage_score']:.1f}%")
    
    if 'llm_enhanced' in results and results['llm_enhanced']:
        print("\nLLM-Enhanced Analysis:")
        print(f"Recommendation: {results['recommendation']['rating']}")
        print(f"Explanation: {results['recommendation']['explanation']}")
        
        print("\nKey Insights:")
        for insight in results['recommendation']['insights']:
            print(f"- {insight}")
        
        print("\nRisks:")
        for risk in results['recommendation']['risks']:
            print(f"- {risk}")
        
        print("\nOpportunities:")
        for opportunity in results['recommendation']['opportunities']:
            print(f"- {opportunity}")
    else:
        print(f"\nBase Recommendation: {results['base_rating']}")
        if 'note' in results:
            print(f"Note: {results['note']}")
    
    print("\nDetailed Metric Scores:")
    for metric, score in results['metric_scores'].items():
        max_score = engine.criteria[metric]['weight']
        print(f"- {metric}: {score:.2f}/{max_score:.2f}")

Implementing a Hybrid Approach

Finally, let's look at how to implement a hybrid approach that combines the strengths of both rule-based and LLM-powered systems:

Hybrid Engine Architecture

A hybrid approach typically follows this architecture:

1. Rule-Based Core

The core analysis and scoring is handled by a transparent, rule-based system:

  • Calculates value scores based on predefined criteria
  • Applies consistent thresholds to financial metrics
  • Generates initial recommendations based on scores
  • Ensures reliability and consistency in the core analysis

2. LLM Enhancement Layer

The LLM is used to enhance the output of the rule-based system:

  • Generates natural language explanations of the analysis
  • Identifies nuanced insights not captured by simple rules
  • Provides context-aware recommendations
  • Considers qualitative factors like economic moats and management quality

3. Integration Controller

A controller component manages the interaction between the two systems:

  • Feeds rule-based results to the LLM for enhancement
  • Validates LLM outputs against rule-based constraints
  • Falls back to rule-based results if LLM fails or produces inconsistent results
  • Combines the strengths of both approaches while mitigating their weaknesses

Implementation Example


class HybridValueEngine:
    def __init__(self):
        # Initialize both engines
        self.rule_engine = RuleBasedValueEngine()
        self.llm_engine = LLMValueEngine()
    
    def analyze(self, company_data):
        # Get rule-based analysis
        rule_results = self.rule_engine.analyze(company_data)
        
        # Try to get LLM enhancement
        try:
            llm_results = self.llm_engine.analyze(company_data)
            
            # Validate LLM results against rule-based constraints
            if self._validate_llm_results(rule_results, llm_results):
                # Combine results
                final_results = self._combine_results(rule_results, llm_results)
            else:
                # Fall back to rule-based results with a note
                final_results = rule_results
                final_results['note'] = "LLM results were inconsistent with rule-based analysis. Using rule-based results only."
        
        except Exception as e:
            # Fall back to rule-based results on error
            final_results = rule_results
            final_results['note'] = f"Error in LLM analysis: {str(e)}. Using rule-based results only."
        
        return final_results
                    

Knowledge Check

What is the main advantage of using a rule-based engine for value investing analysis?

  • It can generate more creative investment ideas
  • It provides transparent and explainable decisions based on clear criteria
  • It requires less programming knowledge to implement
  • It automatically adapts to changing market conditions

In a hybrid approach to value investing analysis, what role does the LLM typically play?

  • It completely replaces the rule-based system for all decisions
  • It only handles data collection and preprocessing
  • It enhances rule-based analysis with natural language explanations and contextual insights
  • It is only used for marketing the investment recommendations
66%
Introduction Completed
Steps 1-5 Completed
Step 6 Current
Steps 7-9 Pending