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:
# 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:
# 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?
What is the main benefit of implementing a chatbot interface for your value investing AI agent?