Building an AI Agent for Value Investing

A step-by-step guide for beginners

Step 7: Build the User Interface

Now that you've implemented the core functionality of your value investing AI agent, it's time to create a user interface that makes it accessible and useful. A well-designed interface will allow users to interact with your agent, input company information, and view analysis results in an intuitive way.

Choosing the Right Interface

There are several options for building the user interface for your value investing AI agent, each with its own advantages and trade-offs:

Interface Options

  • Streamlit: Easy, Python-native web interface
  • Flask/FastAPI + React: More customizable web application
  • Chatbot Interface: Conversational interaction using LangChain + GPT

Streamlit

Advantages:

  • Very easy to implement for Python developers
  • Minimal frontend knowledge required
  • Built-in components for data visualization
  • Quick prototyping and iteration

Disadvantages:

  • Less customizable than other options
  • Performance limitations for complex applications
  • Less suitable for production-grade applications

Best for: Beginners, data scientists, quick prototypes, and internal tools

Flask/FastAPI + React

Advantages:

  • Highly customizable interface
  • Better performance for complex applications
  • Separation of frontend and backend concerns
  • More suitable for production applications

Disadvantages:

  • Requires knowledge of both Python and JavaScript/React
  • More complex setup and development process
  • Longer development time

Best for: Production applications, complex interfaces, teams with frontend and backend expertise

Chatbot Interface

Advantages:

  • Natural language interaction
  • Can be integrated into existing messaging platforms
  • Familiar interface for many users
  • Good for guided analysis and explanations

Disadvantages:

  • May be less efficient for repeated tasks
  • Limited data visualization capabilities
  • Requires careful prompt engineering

Best for: Applications focused on explanation and guidance, integration with existing platforms

Building a Streamlit Interface

Let's start by implementing a simple but effective interface using Streamlit, which is perfect for beginners with Python experience:

Python: Streamlit Interface for Value Investing Agent
# streamlit_app.py

# Install required libraries (run this once)
# pip install streamlit pandas numpy matplotlib plotly yfinance

import streamlit as st
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import plotly.graph_objects as go
import plotly.express as px
import yfinance as yf
from datetime import datetime, timedelta
import os

# Import your value investing agent
# In a real application, you would import your agent class from another file
# For this example, we'll define a simplified version here

class SimpleValueInvestingAgent:
    """
    A simplified value investing agent for demonstration purposes.
    """
    
    def __init__(self):
        self.criteria = {
            'pe_ratio': {'max': 15, 'weight': 0.15, 'better': 'lower'},
            'pb_ratio': {'max': 3, 'weight': 0.15, 'better': 'lower'},
            'roe': {'min': 0.15, 'weight': 0.15, 'better': 'higher'},
            'debt_to_equity': {'max': 1.0, 'weight': 0.1, 'better': 'lower'},
            'fcf_yield': {'min': 0.02, 'weight': 0.15, 'better': 'higher'},
            'dividend_yield': {'min': 0.01, 'weight': 0.1, 'better': 'higher'},
            'earnings_growth': {'min': 0.05, 'weight': 0.1, 'better': 'higher'},
            'margin_of_safety': {'min': 0.2, 'weight': 0.1, 'better': 'higher'}
        }
    
    def fetch_data(self, ticker):
        """Fetch financial data for a given ticker."""
        try:
            stock = yf.Ticker(ticker)
            info = stock.info
            
            # Extract relevant financial metrics
            financial_data = {
                'ticker': ticker,
                'name': info.get('longName', 'Unknown'),
                'sector': info.get('sector', 'Unknown'),
                'industry': info.get('industry', 'Unknown'),
                'pe_ratio': info.get('trailingPE', np.nan),
                'forward_pe': info.get('forwardPE', np.nan),
                'pb_ratio': info.get('priceToBook', np.nan),
                'roe': info.get('returnOnEquity', np.nan),
                'debt_to_equity': info.get('debtToEquity', np.nan) / 100 if info.get('debtToEquity') else np.nan,
                'dividend_yield': info.get('dividendYield', 0),
                'market_cap': info.get('marketCap', np.nan),
                'price': info.get('currentPrice', np.nan),
                'fifty_two_week_high': info.get('fiftyTwoWeekHigh', np.nan),
                'fifty_two_week_low': info.get('fiftyTwoWeekLow', np.nan)
            }
            
            # Calculate FCF Yield (if available)
            if 'freeCashflow' in info and info['freeCashflow'] and 'marketCap' in info and info['marketCap']:
                financial_data['fcf_yield'] = info['freeCashflow'] / info['marketCap']
            else:
                financial_data['fcf_yield'] = np.nan
            
            # Calculate earnings growth (simplified)
            financial_data['earnings_growth'] = 0.08  # Placeholder value
            
            # Calculate margin of safety based on 52-week high
            if not np.isnan(financial_data.get('fifty_two_week_high', np.nan)) and not np.isnan(financial_data.get('price', np.nan)):
                financial_data['margin_of_safety'] = (financial_data['fifty_two_week_high'] - financial_data['price']) / financial_data['fifty_two_week_high']
            else:
                financial_data['margin_of_safety'] = np.nan
            
            return financial_data
            
        except Exception as e:
            st.error(f"Error fetching data for {ticker}: {e}")
            return None
    
    def analyze(self, financial_data):
        """Analyze company data using value investing criteria."""
        if not financial_data:
            return None
        
        # Initialize results
        results = {
            'company_name': financial_data.get('name', 'Unknown Company'),
            'ticker': financial_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, criterion in self.criteria.items():
            if metric_name in financial_data and not pd.isna(financial_data[metric_name]):
                value = financial_data[metric_name]
                score = 0
                
                # For metrics where lower is better (like P/E ratio)
                if criterion['better'] == 'lower' and 'max' in criterion:
                    if value <= criterion['max']:
                        # Scale the score based on how much below the max it is
                        reasonable_min = criterion['max'] * 0.2  # Assume 20% of max is reasonable minimum
                        normalized = 1 - max(0, min(1, (value - reasonable_min) / (criterion['max'] - reasonable_min)))
                        score = normalized * criterion['weight']
                        results['explanations'].append(f"{metric_name} of {value:.2f} is below the threshold of {criterion['max']}, which is positive.")
                    else:
                        results['explanations'].append(f"{metric_name} of {value:.2f} exceeds the threshold of {criterion['max']}, which is negative.")
                
                # For metrics where higher is better (like ROE)
                elif criterion['better'] == 'higher' and 'min' in criterion:
                    if value >= criterion['min']:
                        # Scale the score based on how much above the min it is
                        reasonable_max = criterion['min'] * 3  # Assume 3x min is reasonable maximum
                        normalized = min(1, (value - criterion['min']) / (reasonable_max - criterion['min']))
                        score = normalized * criterion['weight']
                        results['explanations'].append(f"{metric_name} of {value:.2f} is above the threshold of {criterion['min']}, which is positive.")
                    else:
                        results['explanations'].append(f"{metric_name} of {value:.2f} is below the threshold of {criterion['min']}, which is negative.")
                
                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 recommendation based on score
        score = results['percentage_score']
        if score >= 70:
            results['rating'] = "Strong Buy"
            results['color'] = "green"
            results['explanation'] = f"{results['company_name']} appears significantly undervalued based on value investing criteria."
        elif score >= 60:
            results['rating'] = "Buy"
            results['color'] = "lightgreen"
            results['explanation'] = f"{results['company_name']} appears moderately undervalued based on value investing criteria."
        elif score >= 40:
            results['rating'] = "Hold"
            results['color'] = "gold"
            results['explanation'] = f"{results['company_name']} appears fairly valued based on value investing criteria."
        elif score >= 30:
            results['rating'] = "Sell"
            results['color'] = "orange"
            results['explanation'] = f"{results['company_name']} appears moderately overvalued based on value investing criteria."
        else:
            results['rating'] = "Strong Sell"
            results['color'] = "red"
            results['explanation'] = f"{results['company_name']} appears significantly overvalued based on value investing criteria."
        
        return results

# Set up the Streamlit app
st.set_page_config(
    page_title="Value Investing AI Agent",
    page_icon="📈",
    layout="wide"
)

# Create sidebar
st.sidebar.title("Value Investing AI Agent")
st.sidebar.image("https://img.icons8.com/color/96/000000/investment.png", width=100)

# App navigation
page = st.sidebar.radio("Navigation", ["Single Stock Analysis", "Compare Stocks", "Portfolio Analysis", "About"])

# Initialize the agent
@st.cache_resource
def get_agent():
    return SimpleValueInvestingAgent()

agent = get_agent()

# Single Stock Analysis page
if page == "Single Stock Analysis":
    st.title("Value Investing Analysis")
    st.write("Enter a stock ticker to analyze its value investing potential.")
    
    # Input for stock ticker
    ticker = st.text_input("Stock Ticker Symbol (e.g., AAPL, MSFT)", "AAPL").upper()
    
    if st.button("Analyze"):
        with st.spinner(f"Analyzing {ticker}..."):
            # Fetch data
            financial_data = agent.fetch_data(ticker)
            
            if financial_data:
                # Analyze the data
                results = agent.analyze(financial_data)
                
                if results:
                    # Display results in a dashboard layout
                    col1, col2 = st.columns([2, 1])
                    
                    with col1:
                        # Company info and overall score
                        st.subheader(f"{results['company_name']} ({results['ticker']})")
                        st.write(f"Sector: {financial_data['sector']} | Industry: {financial_data['industry']}")
                        st.write(f"Current Price: ${financial_data['price']:.2f}")
                        
                        # Create a gauge chart for the value score
                        fig = go.Figure(go.Indicator(
                            mode="gauge+number",
                            value=results['percentage_score'],
                            domain={'x': [0, 1], 'y': [0, 1]},
                            title={'text': "Value Score"},
                            gauge={
                                'axis': {'range': [0, 100]},
                                'bar': {'color': results['color']},
                                'steps': [
                                    {'range': [0, 30], 'color': "lightgray"},
                                    {'range': [30, 40], 'color': "orange"},
                                    {'range': [40, 60], 'color': "gold"},
                                    {'range': [60, 70], 'color': "lightgreen"},
                                    {'range': [70, 100], 'color': "green"},
                                ],
                                'threshold': {
                                    'line': {'color': "red", 'width': 4},
                                    'thickness': 0.75,
                                    'value': 30
                                }
                            }
                        ))
                        
                        st.plotly_chart(fig)
                        
                        # Recommendation
                        st.subheader("Recommendation")
                        st.markdown(f"

{results['rating']}

", unsafe_allow_html=True) st.write(results['explanation']) # Key insights st.subheader("Key Insights") for explanation in results['explanations']: st.write(f"• {explanation}") with col2: # Metrics table st.subheader("Financial Metrics") metrics_df = pd.DataFrame({ 'Metric': [ 'P/E Ratio', 'P/B Ratio', 'Return on Equity', 'Debt-to-Equity', 'FCF Yield', 'Dividend Yield', 'Earnings Growth', 'Margin of Safety' ], 'Value': [ f"{financial_data.get('pe_ratio', 'N/A'):.2f}" if not pd.isna(financial_data.get('pe_ratio', np.nan)) else "N/A", f"{financial_data.get('pb_ratio', 'N/A'):.2f}" if not pd.isna(financial_data.get('pb_ratio', np.nan)) else "N/A", f"{financial_data.get('roe', 'N/A')*100:.2f}%" if not pd.isna(financial_data.get('roe', np.nan)) else "N/A", f"{financial_data.get('debt_to_equity', 'N/A'):.2f}" if not pd.isna(financial_data.get('debt_to_equity', np.nan)) else "N/A", f"{financial_data.get('fcf_yield', 'N/A')*100:.2f}%" if not pd.isna(financial_data.get('fcf_yield', np.nan)) else "N/A", f"{financial_data.get('dividend_yield', 'N/A')*100:.2f}%" if not pd.isna(financial_data.get('dividend_yield', np.nan)) else "N/A", f"{financial_data.get('earnings_growth', 'N/A')*100:.2f}%" if not pd.isna(financial_data.get('earnings_growth', np.nan)) else "N/A", f"{financial_data.get('margin_of_safety', 'N/A')*100:.2f}%" if not pd.isna(financial_data.get('margin_of_safety', np.nan)) else "N/A" ], 'Score': [ f"{results['metric_scores'].get('pe_ratio', 0)*100/agent.criteria['pe_ratio']['weight']:.0f}%" if 'pe_ratio' in results['metric_scores'] else "0%", f"{results['metric_scores'].get('pb_ratio', 0)*100/agent.criteria['pb_ratio']['weight']:.0f}%" if 'pb_ratio' in results['metric_scores'] else "0%", f"{results['metric_scores'].get('roe', 0)*100/agent.criteria['roe']['weight']:.0f}%" if 'roe' in results['metric_scores'] else "0%", f"{results['metric_scores'].get('debt_to_equity', 0)*100/agent.criteria['debt_to_equity']['weight']:.0f}%" if 'debt_to_equity' in results['metric_scores'] else "0%", f"{results['metric_scores'].get('fcf_yield', 0)*100/agent.criteria['fcf_yield']['weight']:.0f}%" if 'fcf_yield' in results['metric_scores'] else "0%", f"{results['metric_scores'].get('dividend_yield', 0)*100/agent.criteria['dividend_yield']['weight']:.0f}%" if 'dividend_yield' in results['metric_scores'] else "0%", f"{results['metric_scores'].get('earnings_growth', 0)*100/agent.criteria['earnings_growth']['weight']:.0f}%" if 'earnings_growth' in results['metric_scores'] else "0%", f"{results['metric_scores'].get('margin_of_safety', 0)*100/agent.criteria['margin_of_safety']['weight']:.0f}%" if 'margin_of_safety' in results['metric_scores'] else "0%" ] }) st.dataframe(metrics_df, hide_index=True) # Historical price chart st.subheader("Price History (1 Year)") end_date = datetime.now() start_date = end_date - timedelta(days=365) hist_data = yf.download(ticker, start=start_date, end=end_date) if not hist_data.empty: fig = px.line(hist_data, x=hist_data.index, y='Close', title=f"{ticker} Stock Price") fig.update_layout(xaxis_title="Date", yaxis_title="Price (USD)") st.plotly_chart(fig) # Disclaimer st.info("Disclaimer: This analysis is for educational purposes only and should not be considered investment advice. Always conduct your own research before making investment decisions.") # Compare Stocks page elif page == "Compare Stocks": st.title("Compare Stocks") st.write("Compare multiple stocks based on value investing criteria.") # Input for stock tickers tickers_input = st.text_input("Enter stock tickers separated by commas (e.g., AAPL, MSFT, GOOGL)", "AAPL, MSFT, GOOGL") tickers = [ticker.strip().upper() for ticker in tickers_input.split(",")] if st.button("Compare"): if len(tickers) > 0: with st.spinner("Analyzing stocks..."): # Analyze each stock results = [] for ticker in tickers: financial_data = agent.fetch_data(ticker) if financial_data: analysis = agent.analyze(financial_data) if analysis: results.append({ 'ticker': ticker, 'name': analysis['company_name'], 'value_score': analysis['percentage_score'], 'rating': analysis['rating'], 'pe_ratio': financial_data.get('pe_ratio', np.nan), 'pb_ratio': financial_data.get('pb_ratio', np.nan), 'roe': financial_data.get('roe', np.nan), 'debt_to_equity': financial_data.get('debt_to_equity', np.nan), 'fcf_yield': financial_data.get('fcf_yield', np.nan), 'dividend_yield': financial_data.get('dividend_yield', np.nan) }) if results: # Create comparison dataframe comparison_df = pd.DataFrame(results) # Sort by value score comparison_df = comparison_df.sort_values('value_score', ascending=False) # Display comparison table st.subheader("Stock Comparison") formatted_df = comparison_df.copy() formatted_df['value_score'] = formatted_df['value_score'].apply(lambda x: f"{x:.1f}%") formatted_df['roe'] = formatted_df['roe'].apply(lambda x: f"{x*100:.2f}%" if not pd.isna(x) else "N/A") formatted_df['fcf_yield'] = formatted_df['fcf_yield'].apply(lambda x: f"{x*100:.2f}%" if not pd.isna(x) else "N/A") formatted_df['dividend_yield'] = formatted_df['dividend_yield'].apply(lambda x: f"{x*100:.2f}%" if not pd.isna(x) else "N/A") st.dataframe(formatted_df, hide_index=True) # Visualization st.subheader("Value Score Comparison") fig = px.bar( comparison_df, x='ticker', y='value_score', color='value_score', color_continuous_scale=['red', 'orange', 'gold', 'lightgreen', 'green'], labels={'ticker': 'Stock', 'value_score': 'Value Score'}, text='value_score' ) fig.update_traces(texttemplate='%{text:.1f}%', textposition='outside') fig.update_layout(uniformtext_minsize=8, uniformtext_mode='hide') st.plotly_chart(fig) # Radar chart for comparing metrics st.subheader("Metrics Comparison") # Normalize metrics for radar chart radar_df = comparison_df.copy() metrics = ['pe_ratio', 'pb_ratio', 'roe', 'debt_to_equity', 'fcf_yield', 'dividend_yield'] # Invert metrics where lower is better for metric in ['pe_ratio', 'pb_ratio', 'debt_to_equity']: max_val = radar_df[metric].max() if not pd.isna(max_val) and max_val > 0: radar_df[metric] = 1 - (radar_df[metric] / max_val) # Normalize metrics where higher is better for metric in ['roe', 'fcf_yield', 'dividend_yield']: max_val = radar_df[metric].max() if not pd.isna(max_val) and max_val > 0: radar_df[metric] = radar_df[metric] / max_val # Replace NaN with 0 radar_df = radar_df.fillna(0) # Create radar chart fig = go.Figure() for i, row in radar_df.iterrows(): fig.add_trace(go.Scatterpolar( r=[row['pe_ratio'], row['pb_ratio'], row['roe'], row['debt_to_equity'], row['fcf_yield'], row['dividend_yield']], theta=['P/E Ratio', 'P/B Ratio', 'ROE', 'Debt/Equity', 'FCF Yield', 'Dividend Yield'], fill='toself', name=row['ticker'] )) fig.update_layout( polar=dict( radialaxis=dict( visible=True, range=[0, 1] )), showlegend=True ) st.plotly_chart(fig) else: st.error("Could not analyze any of the provided tickers. Please check the symbols and try again.") # Portfolio Analysis page elif page == "Portfolio Analysis": st.title("Portfolio Analysis") st.write("Analyze a portfolio of stocks based on value investing principles.") # Portfolio input st.subheader("Enter Your Portfolio") # Create a template dataframe for the portfolio template_data = { 'Ticker': ['AAPL', 'MSFT', 'GOOGL'], 'Shares': [10, 5, 2], 'Purchase Price': [150.00, 300.00, 2500.00] } template_df = pd.DataFrame(template_data) # Allow user to edit the portfolio portfolio_df = st.data_editor( template_df, num_rows="dynamic", column_config={ "Ticker": st.column_config.TextColumn("Ticker", help="Stock ticker symbol"), "Shares": st.column_config.NumberColumn("Shares", help="Number of shares", min_value=0, step=1), "Purchase Price": st.column_config.NumberColumn("Purchase Price", help="Price paid per share", min_value=0.01, format="$%.2f") } ) if st.button("Analyze Portfolio"): if not portfolio_df.empty: with st.spinner("Analyzing portfolio..."): # Analyze each stock in the portfolio portfolio_results = [] total_value = 0 total_cost = 0 for _, row in portfolio_df.iterrows(): ticker = row['Ticker'].strip().upper() shares = row['Shares'] purchase_price = row['Purchase Price'] financial_data = agent.fetch_data(ticker) if financial_data: analysis = agent.analyze(financial_data) if analysis: current_price = financial_data.get('price', 0) current_value = current_price * shares cost_basis = purchase_price * shares gain_loss = current_value - cost_basis gain_loss_pct = (gain_loss / cost_basis) * 100 if cost_basis > 0 else 0 portfolio_results.append({ 'ticker': ticker, 'name': analysis['company_name'], 'shares': shares, 'purchase_price': purchase_price, 'current_price': current_price, 'current_value': current_value, 'cost_basis': cost_basis, 'gain_loss': gain_loss, 'gain_loss_pct': gain_loss_pct, 'value_score': analysis['percentage_score'], 'rating': analysis['rating'] }) total_value += current_value total_cost += cost_basis if portfolio_results: # Create portfolio dataframe portfolio_results_df = pd.DataFrame(portfolio_results) # Calculate portfolio metrics total_gain_loss = total_value - total_cost total_gain_loss_pct = (total_gain_loss / total_cost) * 100 if total_cost > 0 else 0 # Display portfolio summary st.subheader("Portfolio Summary") col1, col2, col3 = st.columns(3) with col1: st.metric("Total Value", f"${total_value:,.2f}") with col2: st.metric("Total Cost", f"${total_cost:,.2f}") with col3: st.metric("Total Gain/Loss", f"${total_gain_loss:,.2f} ({total_gain_loss_pct:.2f}%)", delta=f"{total_gain_loss_pct:.2f}%") # Display portfolio composition st.subheader("Portfolio Composition") # Pie chart of portfolio allocation fig = px.pie( portfolio_results_df, values='current_value', names='ticker', title='Portfolio Allocation', hover_data=['name', 'current_value', 'value_score', 'rating'] ) st.plotly_chart(fig) # Display detailed portfolio table st.subheader("Portfolio Details") # Format the dataframe for display display_df = portfolio_results_df.copy() display_df['purchase_price'] = display_df['purchase_price'].apply(lambda x: f"${x:.2f}") display_df['current_price'] = display_df['current_price'].apply(lambda x: f"${x:.2f}") display_df['current_value'] = display_df['current_value'].apply(lambda x: f"${x:,.2f}") display_df['cost_basis'] = display_df['cost_basis'].apply(lambda x: f"${x:,.2f}") display_df['gain_loss'] = display_df['gain_loss'].apply(lambda x: f"${x:,.2f}") display_df['gain_loss_pct'] = display_df['gain_loss_pct'].apply(lambda x: f"{x:.2f}%") display_df['value_score'] = display_df['value_score'].apply(lambda x: f"{x:.1f}%") # Rename columns for display display_df = display_df.rename(columns={ 'ticker': 'Ticker', 'name': 'Company', 'shares': 'Shares', 'purchase_price': 'Purchase Price', 'current_price': 'Current Price', 'current_value': 'Current Value', 'cost_basis': 'Cost Basis', 'gain_loss': 'Gain/Loss', 'gain_loss_pct': 'Gain/Loss %', 'value_score': 'Value Score', 'rating': 'Rating' }) st.dataframe(display_df, hide_index=True) # Value analysis of portfolio st.subheader("Value Analysis") # Bar chart of value scores fig = px.bar( portfolio_results_df, x='ticker', y='value_score', color='value_score', color_continuous_scale=['red', 'orange', 'gold', 'lightgreen', 'green'], labels={'ticker': 'Stock', 'value_score': 'Value Score'}, title='Value Scores of Portfolio Holdings', text='value_score' ) fig.update_traces(texttemplate='%{text:.1f}%', textposition='outside') st.plotly_chart(fig) # Recommendations st.subheader("Portfolio Recommendations") # Group by rating rating_counts = portfolio_results_df.groupby('rating').size().reset_index(name='count') # Display recommendations col1, col2 = st.columns(2) with col1: fig = px.pie( rating_counts, values='count', names='rating', title='Ratings Distribution', color='rating', color_discrete_map={ 'Strong Buy': 'green', 'Buy': 'lightgreen', 'Hold': 'gold', 'Sell': 'orange', 'Strong Sell': 'red' } ) st.plotly_chart(fig) with col2: st.write("Based on value investing principles, consider:") # Stocks to consider buying more buy_stocks = portfolio_results_df[portfolio_results_df['rating'].isin(['Strong Buy', 'Buy'])] if not buy_stocks.empty: st.write("**Stocks to consider buying more:**") for _, row in buy_stocks.iterrows(): st.write(f"• {row['name']} ({row['ticker']}) - {row['rating']} (Value Score: {row['value_score']:.1f}%)") # Stocks to consider selling sell_stocks = portfolio_results_df[portfolio_results_df['rating'].isin(['Sell', 'Strong Sell'])] if not sell_stocks.empty: st.write("**Stocks to consider selling:**") for _, row in sell_stocks.iterrows(): st.write(f"• {row['name']} ({row['ticker']}) - {row['rating']} (Value Score: {row['value_score']:.1f}%)") # Portfolio diversification comment if len(portfolio_results_df) < 5: st.write("**Consider diversifying your portfolio** by adding more stocks across different sectors.") else: st.error("Could not analyze any of the stocks in your portfolio. Please check the ticker symbols and try again.") # About page else: st.title("About the Value Investing AI Agent") st.write(""" This application demonstrates a value investing AI agent that analyzes stocks based on fundamental value investing principles. ### Value Investing Approach The agent evaluates stocks using the following criteria: - **P/E Ratio**: Price-to-Earnings ratio, a measure of a company's current share price relative to its earnings per share. - **P/B Ratio**: Price-to-Book ratio, comparing a company's market value to its book value. - **ROE**: Return on Equity, measuring a company's profitability relative to shareholders' equity. - **Debt-to-Equity**: A measure of a company's financial leverage. - **FCF Yield**: Free Cash Flow Yield, indicating how much free cash flow a company generates relative to its market capitalization. - **Dividend Yield**: Annual dividends relative to share price. - **Earnings Growth**: Rate of growth in a company's earnings. - **Margin of Safety**: The difference between a stock's intrinsic value and its market price. ### How It Works 1. The agent fetches financial data for a given stock ticker. 2. It evaluates each metric against predefined value investing criteria. 3. It calculates a composite value score based on weighted metrics. 4. It generates investment recommendations based on the value score. ### Disclaimer This application is for educational purposes only and should not be considered investment advice. Always conduct your own research before making investment decisions. """) st.subheader("Technologies Used") col1, col2, col3 = st.columns(3) with col1: st.write("**Frontend:**") st.write("- Streamlit") st.write("- Plotly") st.write("- Matplotlib") with col2: st.write("**Data Processing:**") st.write("- Pandas") st.write("- NumPy") st.write("- yfinance") with col3: st.write("**AI Components:**") st.write("- Custom value investing algorithm") st.write("- Rule-based scoring system") st.write("- Data visualization") # Run the app with: streamlit run streamlit_app.py

Building a Flask + React Interface

For a more customizable and production-ready interface, you might want to use a combination of Flask (backend) and React (frontend):

Flask + React Architecture

Here's how to structure a Flask + React application for your value investing AI agent:

Project Structure


value_investing_app/
├── backend/                 # Flask backend
│   ├── app.py               # Main Flask application
│   ├── models/              # Data models
│   │   └── value_agent.py   # Value investing agent
│   ├── routes/              # API routes
│   │   └── api.py           # API endpoints
│   ├── utils/               # Utility functions
│   │   └── data_fetcher.py  # Financial data fetcher
│   └── requirements.txt     # Python dependencies
│
├── frontend/                # React frontend
│   ├── public/              # Static files
│   ├── src/                 # React source code
│   │   ├── components/      # React components
│   │   ├── pages/           # Page components
│   │   ├── services/        # API services
│   │   ├── App.js           # Main App component
│   │   └── index.js         # Entry point
│   ├── package.json         # Node.js dependencies
│   └── README.md            # Frontend documentation
│
└── README.md                # Project documentation
                    

Backend (Flask) Implementation

The Flask backend provides API endpoints for the React frontend to interact with your value investing agent:


# app.py

from flask import Flask, request, jsonify
from flask_cors import CORS
from models.value_agent import ValueInvestingAgent

app = Flask(__name__)
CORS(app)  # Enable CORS for all routes

# Initialize the agent
agent = ValueInvestingAgent()

@app.route('/api/analyze', methods=['POST'])
def analyze_stock():
    data = request.json
    ticker = data.get('ticker')
    
    if not ticker:
        return jsonify({'error': 'No ticker provided'}), 400
    
    try:
        # Fetch and analyze the stock
        financial_data = agent.fetch_data(ticker)
        if not financial_data:
            return jsonify({'error': f'Could not fetch data for {ticker}'}), 404
        
        analysis = agent.analyze(financial_data)
        if not analysis:
            return jsonify({'error': f'Could not analyze {ticker}'}), 500
        
        return jsonify({
            'financial_data': financial_data,
            'analysis': analysis
        })
    
    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/api/compare', methods=['POST'])
def compare_stocks():
    data = request.json
    tickers = data.get('tickers', [])
    
    if not tickers:
        return jsonify({'error': 'No tickers provided'}), 400
    
    try:
        results = []
        for ticker in tickers:
            financial_data = agent.fetch_data(ticker)
            if financial_data:
                analysis = agent.analyze(financial_data)
                if analysis:
                    results.append({
                        'ticker': ticker,
                        'financial_data': financial_data,
                        'analysis': analysis
                    })
        
        if not results:
            return jsonify({'error': 'Could not analyze any of the provided tickers'}), 404
        
        return jsonify({'results': results})
    
    except Exception as e:
        return jsonify({'error': str(e)}), 500

if __name__ == '__main__':
    app.run(debug=True)
                    

Frontend (React) Implementation

The React frontend provides a user-friendly interface for interacting with your value investing agent:


// src/services/api.js

const API_URL = 'http://localhost:5000/api';

export const analyzeStock = async (ticker) => {
  try {
    const response = await fetch(`${API_URL}/analyze`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ ticker }),
    });
    
    if (!response.ok) {
      const errorData = await response.json();
      throw new Error(errorData.error || 'Failed to analyze stock');
    }
    
    return await response.json();
  } catch (error) {
    console.error('Error analyzing stock:', error);
    throw error;
  }
};

export const compareStocks = async (tickers) => {
  try {
    const response = await fetch(`${API_URL}/compare`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ tickers }),
    });
    
    if (!response.ok) {
      const errorData = await response.json();
      throw new Error(errorData.error || 'Failed to compare stocks');
    }
    
    return await response.json();
  } catch (error) {
    console.error('Error comparing stocks:', error);
    throw error;
  }
};
                    

// src/components/StockAnalysis.js

import React, { useState } from 'react';
import { analyzeStock } from '../services/api';
import ValueScore from './ValueScore';
import FinancialMetrics from './FinancialMetrics';
import Recommendation from './Recommendation';

const StockAnalysis = () => {
  const [ticker, setTicker] = useState('');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [result, setResult] = useState(null);
  
  const handleAnalyze = async () => {
    if (!ticker) return;
    
    setLoading(true);
    setError(null);
    
    try {
      const data = await analyzeStock(ticker);
      setResult(data);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };
  
  return (
    

Value Investing Analysis

setTicker(e.target.value.toUpperCase())} placeholder="Enter stock ticker (e.g., AAPL)" />
{error &&
{error}
} {result && (

{result.financial_data.name} ({result.financial_data.ticker})

)}
); }; export default StockAnalysis;

Creating a Chatbot Interface

For a more conversational experience, you can implement a chatbot interface using LangChain and a language model:

Python: Chatbot Interface with Gradio
# chatbot_interface.py

# Install required libraries (run this once)
# pip install gradio langchain openai yfinance pandas numpy

import gradio as gr
import pandas as pd
import numpy as np
import yfinance as yf
from langchain.agents import Tool, initialize_agent, AgentType
from langchain.chat_models import ChatOpenAI
from langchain.memory import ConversationBufferMemory
from langchain.tools import BaseTool
from typing import Optional
import json
import os

# Note: This example uses OpenAI's API. You would need an API key to run it.
# For educational purposes, we'll show the code structure without requiring an actual key.
# os.environ["OPENAI_API_KEY"] = "your-api-key"

# Import your value investing agent
# In a real application, you would import your agent class from another file
# For this example, we'll define a simplified version here

class SimpleValueInvestingAgent:
    """
    A simplified value investing agent for demonstration purposes.
    """
    
    def __init__(self):
        self.criteria = {
            'pe_ratio': {'max': 15, 'weight': 0.15, 'better': 'lower'},
            'pb_ratio': {'max': 3, 'weight': 0.15, 'better': 'lower'},
            'roe': {'min': 0.15, 'weight': 0.15, 'better': 'higher'},
            'debt_to_equity': {'max': 1.0, 'weight': 0.1, 'better': 'lower'},
            'fcf_yield': {'min': 0.02, 'weight': 0.15, 'better': 'higher'},
            'dividend_yield': {'min': 0.01, 'weight': 0.1, 'better': 'higher'},
            'earnings_growth': {'min': 0.05, 'weight': 0.1, 'better': 'higher'},
            'margin_of_safety': {'min': 0.2, 'weight': 0.1, 'better': 'higher'}
        }
    
    def fetch_data(self, ticker):
        """Fetch financial data for a given ticker."""
        try:
            stock = yf.Ticker(ticker)
            info = stock.info
            
            # Extract relevant financial metrics
            financial_data = {
                'ticker': ticker,
                'name': info.get('longName', 'Unknown'),
                'sector': info.get('sector', 'Unknown'),
                'industry': info.get('industry', 'Unknown'),
                'pe_ratio': info.get('trailingPE', np.nan),
                'forward_pe': info.get('forwardPE', np.nan),
                'pb_ratio': info.get('priceToBook', np.nan),
                'roe': info.get('returnOnEquity', np.nan),
                'debt_to_equity': info.get('debtToEquity', np.nan) / 100 if info.get('debtToEquity') else np.nan,
                'dividend_yield': info.get('dividendYield', 0),
                'market_cap': info.get('marketCap', np.nan),
                'price': info.get('currentPrice', np.nan),
                'fifty_two_week_high': info.get('fiftyTwoWeekHigh', np.nan),
                'fifty_two_week_low': info.get('fiftyTwoWeekLow', np.nan)
            }
            
            # Calculate FCF Yield (if available)
            if 'freeCashflow' in info and info['freeCashflow'] and 'marketCap' in info and info['marketCap']:
                financial_data['fcf_yield'] = info['freeCashflow'] / info['marketCap']
            else:
                financial_data['fcf_yield'] = np.nan
            
            # Placeholder values for demonstration
            financial_data['earnings_growth'] = 0.08
            
            # Calculate margin of safety based on 52-week high
            if not np.isnan(financial_data.get('fifty_two_week_high', np.nan)) and not np.isnan(financial_data.get('price', np.nan)):
                financial_data['margin_of_safety'] = (financial_data['fifty_two_week_high'] - financial_data['price']) / financial_data['fifty_two_week_high']
            else:
                financial_data['margin_of_safety'] = np.nan
            
            return financial_data
            
        except Exception as e:
            print(f"Error fetching data for {ticker}: {e}")
            return None
    
    def analyze(self, financial_data):
        """Analyze company data using value investing criteria."""
        if not financial_data:
            return None
        
        # Initialize results
        results = {
            'company_name': financial_data.get('name', 'Unknown Company'),
            'ticker': financial_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, criterion in self.criteria.items():
            if metric_name in financial_data and not pd.isna(financial_data[metric_name]):
                value = financial_data[metric_name]
                score = 0
                
                # For metrics where lower is better (like P/E ratio)
                if criterion['better'] == 'lower' and 'max' in criterion:
                    if value <= criterion['max']:
                        # Scale the score based on how much below the max it is
                        reasonable_min = criterion['max'] * 0.2  # Assume 20% of max is reasonable minimum
                        normalized = 1 - max(0, min(1, (value - reasonable_min) / (criterion['max'] - reasonable_min)))
                        score = normalized * criterion['weight']
                        results['explanations'].append(f"{metric_name} of {value:.2f} is below the threshold of {criterion['max']}, which is positive.")
                    else:
                        results['explanations'].append(f"{metric_name} of {value:.2f} exceeds the threshold of {criterion['max']}, which is negative.")
                
                # For metrics where higher is better (like ROE)
                elif criterion['better'] == 'higher' and 'min' in criterion:
                    if value >= criterion['min']:
                        # Scale the score based on how much above the min it is
                        reasonable_max = criterion['min'] * 3  # Assume 3x min is reasonable maximum
                        normalized = min(1, (value - criterion['min']) / (reasonable_max - criterion['min']))
                        score = normalized * criterion['weight']
                        results['explanations'].append(f"{metric_name} of {value:.2f} is above the threshold of {criterion['min']}, which is positive.")
                    else:
                        results['explanations'].append(f"{metric_name} of {value:.2f} is below the threshold of {criterion['min']}, which is negative.")
                
                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 recommendation based on score
        score = results['percentage_score']
        if score >= 70:
            results['rating'] = "Strong Buy"
            results['explanation'] = f"{results['company_name']} appears significantly undervalued based on value investing criteria."
        elif score >= 60:
            results['rating'] = "Buy"
            results['explanation'] = f"{results['company_name']} appears moderately undervalued based on value investing criteria."
        elif score >= 40:
            results['rating'] = "Hold"
            results['explanation'] = f"{results['company_name']} appears fairly valued based on value investing criteria."
        elif score >= 30:
            results['rating'] = "Sell"
            results['explanation'] = f"{results['company_name']} appears moderately overvalued based on value investing criteria."
        else:
            results['rating'] = "Strong Sell"
            results['explanation'] = f"{results['company_name']} appears significantly overvalued based on value investing criteria."
        
        return results

# Initialize the agent
agent = SimpleValueInvestingAgent()

# Define LangChain tools

class StockAnalysisTool(BaseTool):
    name = "analyze_stock"
    description = "Analyze a stock based on value investing principles"
    
    def _run(self, ticker: str) -> str:
        """Analyze a stock ticker."""
        try:
            # Clean the ticker
            ticker = ticker.strip().upper()
            
            # Fetch and analyze the stock
            financial_data = agent.fetch_data(ticker)
            if not financial_data:
                return f"Could not fetch data for {ticker}. Please check the ticker symbol and try again."
            
            analysis = agent.analyze(financial_data)
            if not analysis:
                return f"Could not analyze {ticker}. Please try again later."
            
            # Format the response
            response = f"""
            Analysis for {analysis['company_name']} ({ticker}):
            
            Value Score: {analysis['percentage_score']:.1f}%
            Recommendation: {analysis['rating']}
            
            {analysis['explanation']}
            
            Key Metrics:
            - P/E Ratio: {financial_data.get('pe_ratio', 'N/A'):.2f if not pd.isna(financial_data.get('pe_ratio', np.nan)) else 'N/A'}
            - P/B Ratio: {financial_data.get('pb_ratio', 'N/A'):.2f if not pd.isna(financial_data.get('pb_ratio', np.nan)) else 'N/A'}
            - ROE: {financial_data.get('roe', 'N/A')*100:.2f}% if not pd.isna(financial_data.get('roe', np.nan)) else 'N/A'
            - Debt-to-Equity: {financial_data.get('debt_to_equity', 'N/A'):.2f if not pd.isna(financial_data.get('debt_to_equity', np.nan)) else 'N/A'}
            - FCF Yield: {financial_data.get('fcf_yield', 'N/A')*100:.2f}% if not pd.isna(financial_data.get('fcf_yield', np.nan)) else 'N/A'
            - Dividend Yield: {financial_data.get('dividend_yield', 'N/A')*100:.2f}% if not pd.isna(financial_data.get('dividend_yield', np.nan)) else 'N/A'
            
            Key Insights:
            """
            
            for explanation in analysis['explanations'][:5]:  # Limit to top 5 insights
                response += f"- {explanation}\n"
            
            return response
            
        except Exception as e:
            return f"Error analyzing stock: {str(e)}"
    
    def _arun(self, ticker: str) -> str:
        """Async version of _run."""
        # For simplicity, we'll just call the synchronous version
        return self._run(ticker)

class CompareStocksTool(BaseTool):
    name = "compare_stocks"
    description = "Compare multiple stocks based on value investing principles"
    
    def _run(self, tickers_str: str) -> str:
        """Compare multiple stocks."""
        try:
            # Parse tickers
            tickers = [ticker.strip().upper() for ticker in tickers_str.split(',')]
            
            if not tickers:
                return "No tickers provided. Please provide comma-separated ticker symbols (e.g., AAPL, MSFT, GOOGL)."
            
            # Analyze each stock
            results = []
            for ticker in tickers:
                financial_data = agent.fetch_data(ticker)
                if financial_data:
                    analysis = agent.analyze(financial_data)
                    if analysis:
                        results.append({
                            'ticker': ticker,
                            'name': analysis['company_name'],
                            'value_score': analysis['percentage_score'],
                            'rating': analysis['rating'],
                            'pe_ratio': financial_data.get('pe_ratio', np.nan),
                            'pb_ratio': financial_data.get('pb_ratio', np.nan),
                            'roe': financial_data.get('roe', np.nan),
                        })
            
            if not results:
                return "Could not analyze any of the provided tickers. Please check the symbols and try again."
            
            # Sort by value score
            results.sort(key=lambda x: x['value_score'], reverse=True)
            
            # Format the response
            response = "Stock Comparison (Ranked by Value Score):\n\n"
            
            for result in results:
                response += f"{result['name']} ({result['ticker']})\n"
                response += f"Value Score: {result['value_score']:.1f}% | Rating: {result['rating']}\n"
                response += f"P/E: {result['pe_ratio']:.2f if not pd.isna(result['pe_ratio']) else 'N/A'} | "
                response += f"P/B: {result['pb_ratio']:.2f if not pd.isna(result['pb_ratio']) else 'N/A'} | "
                response += f"ROE: {result['roe']*100:.2f}% if not pd.isna(result['roe']) else 'N/A'\n\n"
            
            # Add summary
            best_stock = results[0]
            response += f"The highest-ranked stock is {best_stock['name']} ({best_stock['ticker']}) "
            response += f"with a value score of {best_stock['value_score']:.1f}% and a rating of {best_stock['rating']}."
            
            return response
            
        except Exception as e:
            return f"Error comparing stocks: {str(e)}"
    
    def _arun(self, tickers_str: str) -> str:
        """Async version of _run."""
        # For simplicity, we'll just call the synchronous version
        return self._run(tickers_str)

class GetStockInfoTool(BaseTool):
    name = "get_stock_info"
    description = "Get basic information about a stock"
    
    def _run(self, ticker: str) -> str:
        """Get basic information about a stock."""
        try:
            # Clean the ticker
            ticker = ticker.strip().upper()
            
            # Fetch stock data
            stock = yf.Ticker(ticker)
            info = stock.info
            
            # Format the response
            response = f"""
            Information for {info.get('longName', ticker)} ({ticker}):
            
            Sector: {info.get('sector', 'N/A')}
            Industry: {info.get('industry', 'N/A')}
            Current Price: ${info.get('currentPrice', 'N/A'):.2f if info.get('currentPrice') else 'N/A'}
            Market Cap: ${info.get('marketCap', 'N/A'):,} if info.get('marketCap') else 'N/A'
            52-Week Range: ${info.get('fiftyTwoWeekLow', 'N/A'):.2f if info.get('fiftyTwoWeekLow') else 'N/A'} - ${info.get('fiftyTwoWeekHigh', 'N/A'):.2f if info.get('fiftyTwoWeekHigh') else 'N/A'}
            
            Business Summary:
            {info.get('longBusinessSummary', 'No business summary available.')}
            """
            
            return response
            
        except Exception as e:
            return f"Error getting stock info: {str(e)}"
    
    def _arun(self, ticker: str) -> str:
        """Async version of _run."""
        # For simplicity, we'll just call the synchronous version
        return self._run(ticker)

# Set up LangChain agent
def setup_agent():
    # Define tools
    tools = [
        StockAnalysisTool(),
        CompareStocksTool(),
        GetStockInfoTool()
    ]
    
    # Set up memory
    memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True)
    
    # Set up language model
    # Note: In a real application, you would use your API key
    llm = ChatOpenAI(temperature=0.7)
    
    # Initialize agent
    agent_chain = initialize_agent(
        tools,
        llm,
        agent=AgentType.CHAT_CONVERSATIONAL_REACT_DESCRIPTION,
        verbose=True,
        memory=memory
    )
    
    return agent_chain

# Set up Gradio interface
def chatbot(message, history):
    # Initialize agent if not already done
    if not hasattr(chatbot, "agent"):
        chatbot.agent = setup_agent()
    
    # Process the message
    try:
        response = chatbot.agent.run(message)
        return response
    except Exception as e:
        return f"Error: {str(e)}"

# Create Gradio interface
demo = gr.ChatInterface(
    chatbot,
    title="Value Investing AI Assistant",
    description="Ask me to analyze stocks based on value investing principles. Try commands like 'analyze AAPL', 'compare AAPL, MSFT, GOOGL', or 'tell me about TSLA'.",
    theme="soft",
    examples=[
        ["Analyze Apple stock"],
        ["Compare AAPL, MSFT, and GOOGL"],
        ["What is value investing?"],
        ["Tell me about Tesla"],
        ["What stocks would you recommend for a value investor?"]
    ]
)

# Launch the app
if __name__ == "__main__":
    demo.launch()

Choosing the Right Interface for Your Needs

When deciding which interface to implement, consider your target users and their needs:

Interface Selection Guide

Here's a guide to help you choose the right interface for your value investing AI agent:

Choose Streamlit if:

  • You're a beginner with primarily Python experience
  • You need to build a prototype quickly
  • You want built-in data visualization components
  • You're building an internal tool or personal project
  • You don't need complex customization

Choose Flask + React if:

  • You need a production-grade application
  • You want full control over the UI design
  • You're comfortable with both Python and JavaScript
  • You need to scale to many users
  • You want to create a professional, polished interface

Choose a Chatbot Interface if:

  • Your users prefer conversational interactions
  • You want to focus on explanations and guidance
  • You want to integrate with existing messaging platforms
  • Your users have varying levels of financial knowledge
  • You want to provide a more personalized experience

Hybrid Approach:

You can also combine these approaches. For example:

  • A Flask + React application with a chatbot component for guided analysis
  • A Streamlit app for quick analysis with a more detailed Flask + React interface for advanced users
  • A chatbot for initial interaction that can redirect to a web interface for detailed visualizations

Knowledge Check

Which of the following is NOT a key advantage of using Streamlit for your value investing AI agent interface?

  • Easy to implement for Python developers
  • Built-in components for data visualization
  • Superior performance for high-traffic production applications
  • Quick prototyping and iteration

What is the main benefit of implementing a chatbot interface for your value investing AI agent?

  • It requires less code to implement than other interfaces
  • It provides a natural language interaction that can guide users and explain complex concepts
  • It offers better data visualization capabilities than web interfaces
  • It eliminates the need for financial data APIs
77%
Introduction Completed
Steps 1-6 Completed
Step 7 Current
Steps 8-9 Pending