AI-Powered Portfolio Risk Dashboard
Build a Streamlit dashboard that analyzes stock portfolio risk with AI.
Introduction
30 Second Summary
Every investment portfolio carries risk. The challenge is not just measuring that risk, but explaining it clearly to people who need to make decisions based on it.
In this project, you will build an interactive dashboard that pulls real stock market data, calculates professional-grade risk metrics, and uses an AI model to translate the numbers into plain-English insights anyone can understand.
What You'll Build
A Streamlit web application with a portfolio analytics dashboard and an AI-powered chat advisor that turns quantitative risk metrics into clear investment memos.
By the end of this project, you'll have:
- A live Streamlit dashboard that fetches real stock data and displays portfolio risk metrics with interactive visualizations.
- An integrated AI chatbot powered by Google Gemini that explains portfolio metrics in plain English for non-technical stakeholders.
- Context-engineered prompts that feed live portfolio data to an LLM for grounded, data-specific responses.
- Secret Mission: A Monte Carlo Value at Risk simulation with interactive controls and a distribution visualization.
Are there any prerequisites?
You should have basic Python familiarity and some experience with pandas and numpy. You do not need any prior finance knowledge. The project explains every metric as you build it.
Before We Start
Before building anything, you need the right tools in place. This project pulls real stock market data, calculates risk metrics, visualizes results, and uses an AI model to explain it all in plain English.
In this step, you will install the Python packages that power the dashboard, grab a free API key for Google's Gemini model, and set up your project folder.
In this step, get ready to:
- Install the Python packages your dashboard needs.
- Get a free Gemini API key from Google AI Studio.
- Create the project folder and verify everything works.
Install the required Python packages
Your dashboard relies on four packages. Streamlit builds the web interface, yfinance fetches stock data, google-genai connects to the Gemini AI model, and matplotlib renders charts.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type VS Code and press Enter to open it.
- Open the integrated terminal by pressing Ctrl+` (macOS and Windows).
- Install all four packages by running this command:
pip install streamlit yfinance google-genai matplotlib
What does this command do?
- streamlit gives you a web framework that turns Python scripts into interactive apps.
- yfinance pulls free stock market data from Yahoo Finance with no API key required.
- google-genai is Google's official SDK for calling the Gemini language model.
- matplotlib draws static charts like the correlation heatmap you will build later.
- Verify Streamlit installed correctly by running:
streamlit --version
You should see a version number printed in the terminal (e.g. 1.59.1).
Seeing 'command not found'?
This usually means pip installed the packages to a different Python version than the one your terminal is using. Try running python3 -m pip install streamlit instead, then re-run the version check.
help me fix a 'command not found' error after pip installing streamlit
Get your free Gemini API key
The AI advisor in your dashboard uses Google's Gemini model to generate plain-English explanations of portfolio metrics. You need an API key to authenticate your requests.
- Open your browser and go to aistudio.google.com.
- Sign in with your Google account.
- Click Get API key in the left sidebar.
- Click Create API key.
- Copy the key that appears. You will paste it into your app's sidebar later.
Is this really free?
Yes. The Gemini free tier requires no credit card and no billing setup. You get 10 requests per minute and 1,500 requests per day, which is more than enough for this project.
Keep your key handy
Paste the key into a temporary text file or note for now. You will enter it directly into the app's sidebar input field when you run the dashboard. Do not share it publicly or commit it to a repository.
Create the project structure
Now you will create a dedicated folder for your project and add the single file the dashboard will live in.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir portfolio-risk-analyzer
cd portfolio-risk-analyzer
- Create the main application file by running:
touch app.py
- Confirm the file exists by running:
ls
You should see app.py in the output.
- Open this folder in VS Code by clicking File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the portfolio-risk-analyzer folder, and click Open.
Why open the folder this way?
Opening the folder (not just the file) tells VS Code to treat portfolio-risk-analyzer as your workspace. The integrated terminal will default to this directory, and the file explorer sidebar will show your project files.
- Open a new terminal in VS Code by pressing Ctrl+`.
- Verify your setup by running:
streamlit run app.py
Your browser should open automatically and show a blank page with the Streamlit logo in the tab. This confirms that Streamlit is installed and can serve your (currently empty) app file.
What should I see?
A mostly blank browser page with a small Streamlit menu in the top-right corner. Your terminal will also show a local URL (usually http://localhost:8501). Leave this terminal running for now.
Browser didn't open?
If the browser did not open automatically, copy the URL from the terminal output and paste it into your browser manually. If you see an error about the port being in use, stop any other running Streamlit instances first.
help me troubleshoot streamlit run not opening in my browser
- Press Ctrl+C in the terminal to stop the Streamlit server for now.
Your environment is ready. Next up, you will build the data engine that fetches real stock prices and calculates portfolio risk metrics.
Build the Portfolio Data and Risk Engine
Your environment is ready and app.py is waiting for code. Now it's time to build the core engine that powers the entire dashboard.
Before you can visualize anything or ask an AI for explanations, you need data. This step builds the backbone: a sidebar where users configure their portfolio, a function that fetches real stock prices, and a risk calculator that produces the metrics financial analysts actually care about.
In this step, get ready to:
- Configure the Streamlit page layout and sidebar controls for user input.
- Fetch real stock data from Yahoo Finance with caching.
- Calculate portfolio risk metrics including Sharpe ratio, VaR, and correlation.
Set up the Streamlit page and sidebar controls
Every Streamlit app starts with page configuration and user inputs. You'll set up a wide layout, a title, and a sidebar where users enter their stock tickers, choose a time period, set a risk-free rate, and paste their Gemini API key.
- Set up the app shell with imports and page configuration by pasting this code into app.py:
import streamlit as st
import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from google import genai
# Page config
st.set_page_config(page_title="Portfolio Risk Analyzer", layout="wide")
st.title("AI-Powered Portfolio Risk Analyzer")
What does this code do?
The imports bring in the libraries you installed in Step 1. streamlit provides the web app framework. yfinance fetches stock data. pandas and numpy handle data manipulation. matplotlib draws charts. genai connects to Google Gemini (used later).
st.set_page_config() sets the browser tab title and forces a wide layout so your dashboard uses the full screen width. st.title() renders a large heading at the top of the app.
- Save app.py.
- Switch to your terminal and run the app by entering this command:
streamlit run app.py
- Your browser should open automatically. You should see the heading "AI-Powered Portfolio Risk Analyzer" on a wide page.
Seeing an import error?
If the terminal shows ModuleNotFoundError, one of the packages did not install correctly. Re-run pip install streamlit yfinance google-genai matplotlib and restart the app.
Help me fix my ModuleNotFoundError
- Add the sidebar controls below the title line in app.py:
# Sidebar configuration
st.sidebar.header("Portfolio Settings")
tickers_input = st.sidebar.text_input(
"Enter tickers (comma-separated)",
value="AAPL, MSFT, GOOGL, AMZN"
)
tickers = [t.strip().upper() for t in tickers_input.split(",") if t.strip()]
period = st.sidebar.selectbox(
"Time Period",
options=["6mo", "1y", "2y", "5y"],
index=1
)
risk_free_rate = st.sidebar.number_input(
"Risk-Free Rate (%)",
min_value=0.0,
max_value=10.0,
value=4.5,
step=0.1
) / 100
api_key = st.sidebar.text_input("Gemini API Key", type="password")
What does this code do?
st.sidebar places widgets in a collapsible side panel. The text input lets users type stock ticker symbols separated by commas. The list comprehension on tickers splits, strips whitespace, and uppercases each ticker into a clean Python list.
The selectbox offers preset time periods (6 months to 5 years). The number input accepts a risk-free rate as a percentage, then divides by 100 to convert it to a decimal for calculations. The password input hides the API key as dots.
- Save app.py and check your browser (Streamlit auto-reloads on save).
- You should see a sidebar on the left with four inputs: a text field pre-filled with AAPL, MSFT, GOOGL, AMZN, a dropdown set to 1y, a number field showing 4.5, and a password field for the API key.
Sidebar not showing up?
Make sure your code uses st.sidebar.text_input (not just st.text_input). Without the sidebar prefix, widgets appear in the main content area instead.
Help me debug my sidebar
Fetch real stock data with caching
Every time a user interacts with a widget, Streamlit re-runs the entire script. Without caching, the app would re-download stock data on every single click. The @st.cache_data decorator solves this by storing results and reusing them when the inputs haven't changed.
- Add the data-fetching function below the api_key line in app.py:
# Data fetching with caching
@st.cache_data(ttl=3600)
def fetch_portfolio_data(tickers, period):
data = yf.download(tickers, period=period, progress=False)
if len(tickers) == 1:
return pd.DataFrame(data["Close"], columns=tickers)
return data["Close"]
What does this code do?
@st.cache_data(ttl=3600) tells Streamlit to cache this function's results for 3600 seconds (one hour). If the same tickers and period are requested again within that window, the cached data is returned instantly instead of hitting Yahoo Finance again.
yf.download() fetches historical closing prices from yfinance. The if len(tickers) == 1 check handles a quirk: when you pass a single ticker, yfinance returns a differently shaped DataFrame. This normalizes it so the rest of the code always expects column names matching the ticker symbols.
- Save app.py.
- You won't see a visual change yet because the function is defined but not called. The next chunk wires everything together.
Calculate portfolio risk metrics
With stock data accessible, you need a function that turns raw prices into the metrics that portfolio managers actually use: annualized return, volatility, Sharpe ratio, Value at Risk, and a correlation matrix.
- Add the risk calculation function below fetch_portfolio_data in app.py:
# Risk calculations
def calculate_risk_metrics(prices, risk_free_rate):
returns = prices.pct_change().dropna()
annual_returns = returns.mean() * 252
annual_volatility = returns.std() * np.sqrt(252)
sharpe_ratio = (annual_returns - risk_free_rate) / annual_volatility
var_95 = returns.quantile(0.05)
correlation = returns.corr()
# Equal-weight portfolio
n = len(prices.columns)
weights = np.array([1 / n] * n)
portfolio_return = np.dot(weights, annual_returns)
portfolio_volatility = np.sqrt(
np.dot(weights.T, np.dot(returns.cov() * 252, weights))
)
portfolio_sharpe = (portfolio_return - risk_free_rate) / portfolio_volatility
return {
"returns": returns,
"annual_returns": annual_returns,
"annual_volatility": annual_volatility,
"sharpe_ratio": sharpe_ratio,
"var_95": var_95,
"correlation": correlation,
"portfolio_return": portfolio_return,
"portfolio_volatility": portfolio_volatility,
"portfolio_sharpe": portfolio_sharpe,
}
What does this code do?
pct_change() converts raw prices into daily percentage returns. Multiplying by 252 (the number of US trading days per year) annualizes the mean return. Multiplying the standard deviation by the square root of 252 annualizes volatility.
The Sharpe ratio measures return per unit of risk: how much extra return you earn above the risk-free rate for each unit of volatility. A higher number means better risk-adjusted performance.
var_95 grabs the 5th percentile of daily returns. This is the historical Value at Risk at 95% confidence. It tells you the worst daily loss you can expect to exceed only 5% of the time.
The equal-weight portfolio section assumes you hold equal dollar amounts in each stock. It computes a combined return, volatility (using the covariance matrix), and Sharpe ratio for the portfolio as a whole.
- Save app.py.
- Again, no visible change yet. The function is defined but not called. The next block connects everything.
- Add the execution block that calls both functions and builds a summary DataFrame. Paste this below calculate_risk_metrics:
# Fetch data
try:
prices = fetch_portfolio_data(tickers, period)
if prices.empty:
st.error("No data found for the given tickers. Please check your input.")
st.stop()
metrics = calculate_risk_metrics(prices, risk_free_rate)
except Exception as e:
st.error(f"Error fetching data: {str(e)}")
st.stop()
# Build metrics DataFrame for display and AI context
metrics_df = pd.DataFrame({
"Annual Return": metrics["annual_returns"],
"Volatility": metrics["annual_volatility"],
"Sharpe Ratio": metrics["sharpe_ratio"],
"VaR (95%, 1-day)": metrics["var_95"],
})
What does this code do?
The try/except block calls fetch_portfolio_data() to download prices and calculate_risk_metrics() to crunch the numbers. If anything goes wrong (invalid ticker, network error), the app shows a friendly error message and stops instead of crashing.
metrics_df packages the per-stock metrics into a pandas DataFrame. This will be displayed as a table in the dashboard and also fed to the AI advisor as context in a later step.
- Save app.py and check your browser.
- The app should reload without errors. You should still see the sidebar controls and the title. Check your terminal for any error messages.
Seeing a NameError or data error?
If you see NameError: name 'tickers' is not defined, make sure the sidebar code that creates the tickers list appears above the try block. Streamlit runs your file top to bottom, so order matters.
If you see a network-related error, check your internet connection. yfinance needs to reach Yahoo Finance to download data.
Help me debug my stock data error
✔️ Awesome, I've got everything!
Great. Double check you've saved app.py and that your terminal shows no errors.
ⓧ I'd like to double check the full code
Here is the complete app.py so far. Compare it line by line with your file:
import streamlit as st
import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from google import genai
# Page config
st.set_page_config(page_title="Portfolio Risk Analyzer", layout="wide")
st.title("AI-Powered Portfolio Risk Analyzer")
# Sidebar configuration
st.sidebar.header("Portfolio Settings")
tickers_input = st.sidebar.text_input(
"Enter tickers (comma-separated)",
value="AAPL, MSFT, GOOGL, AMZN"
)
tickers = [t.strip().upper() for t in tickers_input.split(",") if t.strip()]
period = st.sidebar.selectbox(
"Time Period",
options=["6mo", "1y", "2y", "5y"],
index=1
)
risk_free_rate = st.sidebar.number_input(
"Risk-Free Rate (%)",
min_value=0.0,
max_value=10.0,
value=4.5,
step=0.1
) / 100
api_key = st.sidebar.text_input("Gemini API Key", type="password")
# Data fetching with caching
@st.cache_data(ttl=3600)
def fetch_portfolio_data(tickers, period):
data = yf.download(tickers, period=period, progress=False)
if len(tickers) == 1:
return pd.DataFrame(data["Close"], columns=tickers)
return data["Close"]
# Risk calculations
def calculate_risk_metrics(prices, risk_free_rate):
returns = prices.pct_change().dropna()
annual_returns = returns.mean() * 252
annual_volatility = returns.std() * np.sqrt(252)
sharpe_ratio = (annual_returns - risk_free_rate) / annual_volatility
var_95 = returns.quantile(0.05)
correlation = returns.corr()
# Equal-weight portfolio
n = len(prices.columns)
weights = np.array([1 / n] * n)
portfolio_return = np.dot(weights, annual_returns)
portfolio_volatility = np.sqrt(
np.dot(weights.T, np.dot(returns.cov() * 252, weights))
)
portfolio_sharpe = (portfolio_return - risk_free_rate) / portfolio_volatility
return {
"returns": returns,
"annual_returns": annual_returns,
"annual_volatility": annual_volatility,
"sharpe_ratio": sharpe_ratio,
"var_95": var_95,
"correlation": correlation,
"portfolio_return": portfolio_return,
"portfolio_volatility": portfolio_volatility,
"portfolio_sharpe": portfolio_sharpe,
}
# Fetch data
try:
prices = fetch_portfolio_data(tickers, period)
if prices.empty:
st.error("No data found for the given tickers. Please check your input.")
st.stop()
metrics = calculate_risk_metrics(prices, risk_free_rate)
except Exception as e:
st.error(f"Error fetching data: {str(e)}")
st.stop()
# Build metrics DataFrame for display and AI context
metrics_df = pd.DataFrame({
"Annual Return": metrics["annual_returns"],
"Volatility": metrics["annual_volatility"],
"Sharpe Ratio": metrics["sharpe_ratio"],
"VaR (95%, 1-day)": metrics["var_95"],
})
Your data engine is live. The app is fetching real market data and calculating institutional-grade risk metrics behind the scenes. Next up, you'll make all of those numbers visible with interactive charts and metric cards.
Create the Dashboard Visualizations
Your data engine is running and calculating risk metrics behind the scenes. Now it's time to make those numbers visible.
In this step, you'll build the visual layer of your dashboard. You'll create metric cards, charts, and a correlation heatmap that transform raw calculations into an at-a-glance portfolio overview.
In this step, get ready to:
- Display portfolio summary metrics as cards.
- Add a normalized price chart and individual stock metrics table.
- Create a correlation heatmap with annotated values.
Build the portfolio summary metrics row
Streamlit supports tabbed layouts that let you organize your app into logical sections. You'll create two tabs now: one for the visual dashboard and one for the AI advisor (which you'll build in the next step).
- In app.py, scroll to the very bottom of the file (below the metrics_df block).
- Add the following code to create two tabs and display portfolio summary metrics inside the Dashboard tab:
# Create tabs
tab1, tab2 = st.tabs(["Dashboard", "AI Advisor"])
# Dashboard tab
with tab1:
st.subheader("Portfolio Summary (Equal Weight)")
col1, col2, col3 = st.columns(3)
col1.metric("Annual Return", f"{metrics['portfolio_return']:.2%}")
col2.metric("Annual Volatility", f"{metrics['portfolio_volatility']:.2%}")
col3.metric("Sharpe Ratio", f"{metrics['portfolio_sharpe']:.2f}")
What does this code do?
st.tabs() creates a tabbed layout. Each tab acts as its own container. The with tab1: block means everything indented below it appears only in the Dashboard tab.
st.columns(3) splits the page into three side-by-side columns. Each column's .metric() call displays a large, styled number card. These give a stakeholder an instant read on portfolio health without scrolling.
- Save app.py.
- Switch to your browser and refresh the Streamlit app. You should see two tabs at the top ("Dashboard" and "AI Advisor") and three metric cards showing percentages for Annual Return, Annual Volatility, and a decimal for Sharpe Ratio.
Don't see the metric cards?
- Make sure the new code is placed after the metrics_df DataFrame block, not inside the try/except block above it.
- Check for an IndentationError in your terminal. Everything inside with tab1: must be indented by 4 spaces.
- If you see "No data found", the default tickers might be failing. Check your internet connection.
Help me debug my Streamlit metric cards not showing up
Add the price chart and individual stock table
Metric cards give a snapshot, but investors want to see how prices moved over time. A normalized price chart lets you compare stocks on the same scale regardless of their actual price.
- In app.py, add the following code inside the with tab1: block, directly below the col3.metric() line:
st.subheader("Normalized Price History")
normalized = prices / prices.iloc[0] * 100
st.line_chart(normalized)
st.subheader("Individual Stock Metrics")
st.dataframe(metrics_df.style.format("{:.4f}"), use_container_width=True)
What does this code do?
Normalizing prices means dividing every price by the first day's price and multiplying by 100. This rebases all stocks to start at 100, so a stock that shows 120 gained 20% regardless of whether it started at $50 or $3,000.
st.line_chart() renders an interactive chart with hover tooltips. Below it, st.dataframe() shows the per-stock risk table with values formatted to four decimal places.
- Save app.py and refresh the browser.
- You should see a line chart with colored lines for each ticker (all starting near 100) and a formatted table below it showing Annual Return, Volatility, Sharpe Ratio, and VaR for each stock.
Chart or table not appearing?
- Confirm the new code is indented at the same level as the st.subheader("Portfolio Summary (Equal Weight)") line above it (4 spaces inside the with tab1: block).
- If the chart shows but the table is blank, check that metrics_df is defined above the tabs block (not inside it).
Help me debug my Streamlit charts not rendering
Create the correlation heatmap
A correlation matrix shows how closely each pair of stocks moves together. A heatmap makes this instantly readable by mapping correlation values to colors.
- Still inside the with tab1: block, add the following code below the st.dataframe() line:
st.subheader("Correlation Matrix")
fig, ax = plt.subplots(figsize=(8, 6))
im = ax.imshow(
metrics["correlation"].values, cmap="RdYlGn", vmin=-1, vmax=1
)
ax.set_xticks(range(len(tickers)))
ax.set_yticks(range(len(tickers)))
ax.set_xticklabels(tickers, rotation=45, ha="right")
ax.set_yticklabels(tickers)
for i in range(len(tickers)):
for j in range(len(tickers)):
ax.text(
j, i, f"{metrics['correlation'].iloc[i, j]:.2f}",
ha="center", va="center", fontsize=10
)
plt.colorbar(im, ax=ax)
plt.tight_layout()
st.pyplot(fig)
What does this code do?
ax.imshow() renders the correlation matrix as a grid of colored cells. The RdYlGn colormap means red for negative correlation, yellow for zero, and green for positive correlation.
The nested for loop prints the actual numeric value in each cell so readers don't have to guess from color alone. st.pyplot(fig) passes the finished matplotlib figure to Streamlit for display.
- Save app.py and refresh the browser.
- Scroll down in the Dashboard tab. You should see a colored grid with ticker names on both axes and correlation values printed in each cell. The diagonal cells should all show 1.00 (every stock is perfectly correlated with itself).
Heatmap not rendering?
- If you see a UserWarning about tight layout in your terminal, the chart still renders correctly. This is cosmetic, not an error.
- Check that import matplotlib.pyplot as plt is at the top of your file. Without it, plt.subplots() will throw a NameError.
Help me fix my matplotlib heatmap in Streamlit
Why does correlation matter for a portfolio?
If two stocks are highly correlated (close to 1.0), they tend to rise and fall together. That means holding both doesn't reduce your risk much. The best diversification comes from stocks with low or negative correlations.
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
Here is the full app.py up to this point. Compare it against your file:
import streamlit as st
import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from google import genai
# Page config
st.set_page_config(page_title="Portfolio Risk Analyzer", layout="wide")
st.title("AI-Powered Portfolio Risk Analyzer")
# Sidebar configuration
st.sidebar.header("Portfolio Settings")
tickers_input = st.sidebar.text_input(
"Enter tickers (comma-separated)",
value="AAPL, MSFT, GOOGL, AMZN"
)
tickers = [t.strip().upper() for t in tickers_input.split(",") if t.strip()]
period = st.sidebar.selectbox(
"Time Period",
options=["6mo", "1y", "2y", "5y"],
index=1
)
risk_free_rate = st.sidebar.number_input(
"Risk-Free Rate (%)",
min_value=0.0,
max_value=10.0,
value=4.5,
step=0.1
) / 100
api_key = st.sidebar.text_input("Gemini API Key", type="password")
# Data fetching with caching
@st.cache_data(ttl=3600)
def fetch_portfolio_data(tickers, period):
data = yf.download(tickers, period=period, progress=False)
if len(tickers) == 1:
return pd.DataFrame(data["Close"], columns=tickers)
return data["Close"]
# Risk calculations
def calculate_risk_metrics(prices, risk_free_rate):
returns = prices.pct_change().dropna()
annual_returns = returns.mean() * 252
annual_volatility = returns.std() * np.sqrt(252)
sharpe_ratio = (annual_returns - risk_free_rate) / annual_volatility
var_95 = returns.quantile(0.05)
correlation = returns.corr()
# Equal-weight portfolio
n = len(prices.columns)
weights = np.array([1 / n] * n)
portfolio_return = np.dot(weights, annual_returns)
portfolio_volatility = np.sqrt(
np.dot(weights.T, np.dot(returns.cov() * 252, weights))
)
portfolio_sharpe = (portfolio_return - risk_free_rate) / portfolio_volatility
return {
"returns": returns,
"annual_returns": annual_returns,
"annual_volatility": annual_volatility,
"sharpe_ratio": sharpe_ratio,
"var_95": var_95,
"correlation": correlation,
"portfolio_return": portfolio_return,
"portfolio_volatility": portfolio_volatility,
"portfolio_sharpe": portfolio_sharpe,
}
# Fetch data
try:
prices = fetch_portfolio_data(tickers, period)
if prices.empty:
st.error("No data found for the given tickers. Please check your input.")
st.stop()
metrics = calculate_risk_metrics(prices, risk_free_rate)
except Exception as e:
st.error(f"Error fetching data: {str(e)}")
st.stop()
# Build metrics DataFrame for display and AI context
metrics_df = pd.DataFrame({
"Annual Return": metrics["annual_returns"],
"Volatility": metrics["annual_volatility"],
"Sharpe Ratio": metrics["sharpe_ratio"],
"VaR (95%, 1-day)": metrics["var_95"],
})
# Create tabs
tab1, tab2 = st.tabs(["Dashboard", "AI Advisor"])
# Dashboard tab
with tab1:
st.subheader("Portfolio Summary (Equal Weight)")
col1, col2, col3 = st.columns(3)
col1.metric("Annual Return", f"{metrics['portfolio_return']:.2%}")
col2.metric("Annual Volatility", f"{metrics['portfolio_volatility']:.2%}")
col3.metric("Sharpe Ratio", f"{metrics['portfolio_sharpe']:.2f}")
st.subheader("Normalized Price History")
normalized = prices / prices.iloc[0] * 100
st.line_chart(normalized)
st.subheader("Individual Stock Metrics")
st.dataframe(metrics_df.style.format("{:.4f}"), use_container_width=True)
st.subheader("Correlation Matrix")
fig, ax = plt.subplots(figsize=(8, 6))
im = ax.imshow(
metrics["correlation"].values, cmap="RdYlGn", vmin=-1, vmax=1
)
ax.set_xticks(range(len(tickers)))
ax.set_yticks(range(len(tickers)))
ax.set_xticklabels(tickers, rotation=45, ha="right")
ax.set_yticklabels(tickers)
for i in range(len(tickers)):
for j in range(len(tickers)):
ax.text(
j, i, f"{metrics['correlation'].iloc[i, j]:.2f}",
ha="center", va="center", fontsize=10
)
plt.colorbar(im, ax=ax)
plt.tight_layout()
st.pyplot(fig)
Your dashboard is alive with data. Metric cards, a price chart, a risk table, and a correlation heatmap give investors everything they need at a glance. Next up, you'll connect an AI advisor that can explain all of these numbers in plain English.
Integrate the AI Chat Advisor
Your dashboard is calculating risk metrics and displaying them visually. Now comes the part that makes this project stand out on a portfolio: an AI-powered chat interface that explains those numbers in plain English.
The key challenge in AI integration is not just calling an API. It is giving the LLM enough context to produce genuinely useful answers. You will build a context-engineered prompt that feeds your live portfolio data directly into the conversation, so the AI advisor can reference your actual Sharpe ratio, volatility, and correlations.
In this step, get ready to:
- Build a chat interface with message history using Streamlit's chat elements.
- Engineer a context prompt that feeds live portfolio data to the LLM.
- Connect to Google Gemini and display AI-generated responses.
Build the chat interface structure
Streamlit provides dedicated chat elements that handle message rendering and input collection. You will use st.chat_message for displaying messages and st.chat_input for collecting user questions.
- In app.py, scroll to the bottom of the file (below the closing of the with tab1: block).
- Add the AI Advisor tab header and API key guard by pasting this code:
# AI Advisor tab
with tab2:
st.subheader("AI Portfolio Advisor")
st.caption(
"Ask questions about your portfolio risk metrics. "
"The AI will explain results in plain English."
)
if not api_key:
st.info("Enter your Gemini API key in the sidebar to enable the AI advisor.")
What does this code do?
This creates the heading and description for the AI Advisor tab. The if not api_key check shows a helpful message when the learner has not yet entered their API key in the sidebar, preventing confusing errors later.
- Save app.py and refresh your browser.
- Click the AI Advisor tab. You should see the subheader "AI Portfolio Advisor" and an info message prompting you to enter your API key.
Don't see the AI Advisor tab content?
Make sure your new code starts with with tab2: and is at the same indentation level as the existing with tab1: block. All code inside the tab must be indented one level beneath with tab2:.
help me debug my Streamlit tab layout
- Now add the chat state management and input handling directly below the st.info line (still inside with tab2:):
if "messages" not in st.session_state:
st.session_state.messages = []
for message in st.session_state.messages:
with st.chat_message(message["role"]):
st.markdown(message["content"])
if prompt := st.chat_input("Ask about your portfolio..."):
if not api_key:
st.error("Please enter your Gemini API key in the sidebar.")
else:
with st.chat_message("user"):
st.markdown(prompt)
st.session_state.messages.append({"role": "user", "content": prompt})
What does this code do?
This manages the full chat lifecycle. st.session_state.messages persists chat history across Streamlit reruns so previous messages do not disappear.
The for loop re-renders every past message each time the app reruns. The := walrus operator assigns the user's input to prompt and checks if it is truthy in one line. When a message arrives, it gets displayed immediately and saved to history.
- Save app.py and refresh your browser.
- In the AI Advisor tab, you should now see a chat input box at the bottom of the page with the placeholder text "Ask about your portfolio...".
Chat input not appearing?
Check that all the code after st.info(...) is indented with exactly 4 spaces (one level inside with tab2:). Also confirm you have no syntax errors by checking the terminal where Streamlit is running.
help me fix my Streamlit chat input
Engineer the context prompt
A raw question like "What is my Sharpe ratio?" gives the LLM zero context about your actual data. Context engineering solves this by injecting your live portfolio metrics directly into the prompt so the AI can give specific, data-grounded answers.
- In app.py, find the line st.session_state.messages.append({"role": "user", "content": prompt}) inside the else: block.
- Add the context prompt directly below that line (still inside the else: block):
context = f"""You are an AI portfolio risk advisor. Explain financial
metrics in plain English for non-technical stakeholders.
Current Portfolio Analysis:
- Tickers: {', '.join(tickers)}
- Time Period: {period}
- Portfolio Annual Return: {metrics['portfolio_return']:.2%}
- Portfolio Volatility: {metrics['portfolio_volatility']:.2%}
- Portfolio Sharpe Ratio: {metrics['portfolio_sharpe']:.2f}
- Risk-Free Rate: {risk_free_rate:.2%}
Individual Stock Metrics:
{metrics_df.to_string()}
Correlation Matrix:
{metrics['correlation'].to_string()}
User Question: {prompt}
Provide a clear, concise answer a non-technical executive could understand.
Use analogies where helpful. Frame any suggestions as educational observations,
not financial advice."""
Why this prompt structure?
This is the difference between a generic chatbot and a useful portfolio advisor. The prompt has three layers:
- A role definition telling the model to explain things in plain English for non-technical stakeholders.
- A data injection section that feeds actual portfolio numbers (tickers, returns, volatility, Sharpe, correlations) so the AI references real values.
- A guardrail instructing the model to frame suggestions as educational observations, not financial advice.
- Save app.py.
You will not see a visible change yet. The next chunk connects this context to the Gemini API.
Connect to Gemini and display responses
With the context prompt ready, you now need to send it to Google Gemini and stream the response back into the chat interface.
- In app.py, add the following code directly below the context variable you just created (still inside the else: block):
try:
client = genai.Client(api_key=api_key)
interaction = client.interactions.create(
model="gemini-2.5-flash",
input=context
)
response = interaction.output_text
except Exception as e:
response = f"Error: {str(e)}. Please check your API key and try again."
with st.chat_message("assistant"):
st.markdown(response)
st.session_state.messages.append(
{"role": "assistant", "content": response}
)
What does this code do?
The genai.Client initializes a connection to Google's Gemini API using the key from the sidebar. The client.interactions.create() call sends your context-rich prompt to the gemini-2.5-flash model and returns the response in interaction.output_text.
The try/except block catches API errors (invalid key, rate limits, network issues) and shows a helpful message instead of crashing the app. Finally, the assistant's response is displayed as a chat bubble and saved to history.
- Save app.py and refresh your browser.
- In the sidebar, paste your Gemini API key into the Gemini API Key field.
- Switch to the AI Advisor tab.
- Type What does my portfolio's Sharpe ratio tell me? in the chat input and press Enter.
- You should see a plain-English explanation that references your actual portfolio data (mentioning specific ticker names and numeric values).
Getting an API error?
If you see an error message in the chat, check that your Gemini API key is correct. You can verify it by going back to aistudio.google.com and confirming the key is active.
If you see a rate limit error, wait 60 seconds and try again. The free tier allows 10 requests per minute.
help me debug my Gemini API connection in Streamlit
Why context engineering instead of fine-tuning?
Fine-tuning a model requires thousands of examples and significant compute cost. Context engineering achieves similar specificity by injecting relevant data at inference time. Every time the user asks a question, the model receives the latest portfolio numbers. This means the AI advisor stays current without retraining.
✔️ Awesome, I've got everything!
Great. Double check you have saved app.py and that the AI advisor is responding to your questions with data-specific answers.
ⓧ I'd like to double check the full code
Here is the complete app.py at the end of this step:
import streamlit as st
import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from google import genai
# Page config
st.set_page_config(page_title="Portfolio Risk Analyzer", layout="wide")
st.title("AI-Powered Portfolio Risk Analyzer")
# Sidebar configuration
st.sidebar.header("Portfolio Settings")
tickers_input = st.sidebar.text_input(
"Enter tickers (comma-separated)",
value="AAPL, MSFT, GOOGL, AMZN"
)
tickers = [t.strip().upper() for t in tickers_input.split(",") if t.strip()]
period = st.sidebar.selectbox(
"Time Period",
options=["6mo", "1y", "2y", "5y"],
index=1
)
risk_free_rate = st.sidebar.number_input(
"Risk-Free Rate (%)",
min_value=0.0,
max_value=10.0,
value=4.5,
step=0.1
) / 100
api_key = st.sidebar.text_input("Gemini API Key", type="password")
# Data fetching with caching
@st.cache_data(ttl=3600)
def fetch_portfolio_data(tickers, period):
data = yf.download(tickers, period=period, progress=False)
if len(tickers) == 1:
return pd.DataFrame(data["Close"], columns=tickers)
return data["Close"]
# Risk calculations
def calculate_risk_metrics(prices, risk_free_rate):
returns = prices.pct_change().dropna()
annual_returns = returns.mean() * 252
annual_volatility = returns.std() * np.sqrt(252)
sharpe_ratio = (annual_returns - risk_free_rate) / annual_volatility
var_95 = returns.quantile(0.05)
correlation = returns.corr()
# Equal-weight portfolio
n = len(prices.columns)
weights = np.array([1 / n] * n)
portfolio_return = np.dot(weights, annual_returns)
portfolio_volatility = np.sqrt(
np.dot(weights.T, np.dot(returns.cov() * 252, weights))
)
portfolio_sharpe = (portfolio_return - risk_free_rate) / portfolio_volatility
return {
"returns": returns,
"annual_returns": annual_returns,
"annual_volatility": annual_volatility,
"sharpe_ratio": sharpe_ratio,
"var_95": var_95,
"correlation": correlation,
"portfolio_return": portfolio_return,
"portfolio_volatility": portfolio_volatility,
"portfolio_sharpe": portfolio_sharpe,
}
# Fetch data
try:
prices = fetch_portfolio_data(tickers, period)
if prices.empty:
st.error("No data found for the given tickers. Please check your input.")
st.stop()
metrics = calculate_risk_metrics(prices, risk_free_rate)
except Exception as e:
st.error(f"Error fetching data: {str(e)}")
st.stop()
# Build metrics DataFrame for display and AI context
metrics_df = pd.DataFrame({
"Annual Return": metrics["annual_returns"],
"Volatility": metrics["annual_volatility"],
"Sharpe Ratio": metrics["sharpe_ratio"],
"VaR (95%, 1-day)": metrics["var_95"],
})
# Create tabs
tab1, tab2 = st.tabs(["Dashboard", "AI Advisor"])
# Dashboard tab
with tab1:
st.subheader("Portfolio Summary (Equal Weight)")
col1, col2, col3 = st.columns(3)
col1.metric("Annual Return", f"{metrics['portfolio_return']:.2%}")
col2.metric("Annual Volatility", f"{metrics['portfolio_volatility']:.2%}")
col3.metric("Sharpe Ratio", f"{metrics['portfolio_sharpe']:.2f}")
st.subheader("Normalized Price History")
normalized = prices / prices.iloc[0] * 100
st.line_chart(normalized)
st.subheader("Individual Stock Metrics")
st.dataframe(metrics_df.style.format("{:.4f}"), use_container_width=True)
st.subheader("Correlation Matrix")
fig, ax = plt.subplots(figsize=(8, 6))
im = ax.imshow(
metrics["correlation"].values, cmap="RdYlGn", vmin=-1, vmax=1
)
ax.set_xticks(range(len(tickers)))
ax.set_yticks(range(len(tickers)))
ax.set_xticklabels(tickers, rotation=45, ha="right")
ax.set_yticklabels(tickers)
for i in range(len(tickers)):
for j in range(len(tickers)):
ax.text(
j, i, f"{metrics['correlation'].iloc[i, j]:.2f}",
ha="center", va="center", fontsize=10
)
plt.colorbar(im, ax=ax)
plt.tight_layout()
st.pyplot(fig)
# AI Advisor tab
with tab2:
st.subheader("AI Portfolio Advisor")
st.caption(
"Ask questions about your portfolio risk metrics. "
"The AI will explain results in plain English."
)
if not api_key:
st.info("Enter your Gemini API key in the sidebar to enable the AI advisor.")
if "messages" not in st.session_state:
st.session_state.messages = []
for message in st.session_state.messages:
with st.chat_message(message["role"]):
st.markdown(message["content"])
if prompt := st.chat_input("Ask about your portfolio..."):
if not api_key:
st.error("Please enter your Gemini API key in the sidebar.")
else:
with st.chat_message("user"):
st.markdown(prompt)
st.session_state.messages.append({"role": "user", "content": prompt})
context = f"""You are an AI portfolio risk advisor. Explain financial
metrics in plain English for non-technical stakeholders.
Current Portfolio Analysis:
- Tickers: {', '.join(tickers)}
- Time Period: {period}
- Portfolio Annual Return: {metrics['portfolio_return']:.2%}
- Portfolio Volatility: {metrics['portfolio_volatility']:.2%}
- Portfolio Sharpe Ratio: {metrics['portfolio_sharpe']:.2f}
- Risk-Free Rate: {risk_free_rate:.2%}
Individual Stock Metrics:
{metrics_df.to_string()}
Correlation Matrix:
{metrics['correlation'].to_string()}
User Question: {prompt}
Provide a clear, concise answer a non-technical executive could understand.
Use analogies where helpful. Frame any suggestions as educational observations,
not financial advice."""
try:
client = genai.Client(api_key=api_key)
interaction = client.interactions.create(
model="gemini-2.5-flash",
input=context
)
response = interaction.output_text
except Exception as e:
response = f"Error: {str(e)}. Please check your API key and try again."
with st.chat_message("assistant"):
st.markdown(response)
st.session_state.messages.append(
{"role": "assistant", "content": response}
)
Secret mission
Add Monte Carlo Value at Risk Simulation
Your historical VaR tells you what happened in the past. But what about the future? Add a Monte Carlo simulation that runs thousands of randomized portfolio scenarios to estimate forward-looking risk, complete with interactive controls and a distribution visualization.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally and uses the Google Gemini free tier, so there are no ongoing costs. Decide whether to keep your resources as-is or delete them entirely.
Resources you used:
- The portfolio-risk-analyzer project folder on your Desktop.
- Python packages installed: streamlit, yfinance, google-genai, matplotlib.
- Your Gemini API key (free, no billing attached).
Keep everything running
No action needed. Choose this if you plan to keep experimenting with different tickers, time periods, or prompt strategies.
- Your project folder stays on your Desktop. Relaunch anytime with streamlit run app.py.
- Your Gemini API key remains active at no cost. The free tier does not expire and has no billing attached.
- The installed Python packages remain available for future projects.
Pause - I'll come back to this later
Stop the Streamlit server to free up your terminal. Everything else stays in place for when you return.
- In your terminal, press Ctrl+C to stop the Streamlit development server.
- Your project folder, installed packages, and API key all remain ready for next time.
Delete - I don't want to use this again
Remove all project files and uninstall the packages to start fresh.
- Stop the Streamlit server by pressing Ctrl+C in your terminal.
- Delete the project folder by running:
macOS
cd ~/Desktop
rm -rf portfolio-risk-analyzer
Windows
cd ~\Desktop
rmdir /s /q portfolio-risk-analyzer
- Uninstall the Python packages by running:
pip uninstall streamlit yfinance google-genai matplotlib -y
- Delete your Gemini API key. Go to the Google Cloud API Credentials page, find the API key you created, click the Actions icon, select Delete API key, and confirm by clicking Delete.
Nice Work!
Nice Work!
Nice work! You just built a full-stack AI-powered portfolio risk dashboard that pulls live market data, calculates institutional-grade risk metrics, and uses an LLM to explain it all in plain English.
You've learned how to:
- Build a Streamlit web application with interactive sidebar controls and tabbed layout that fetches real stock data via yfinance and calculates portfolio risk metrics (Sharpe ratio, Value at Risk, volatility, correlation).
- Create interactive data visualizations including normalized price charts, metrics cards, and a matplotlib correlation heatmap.
- Integrate the Google Gemini API with context-engineered prompts to translate quantitative analysis into plain-English explanations through a conversational chat interface.
- Secret Mission: Added a Monte Carlo simulation for forward-looking VaR estimation with interactive controls and distribution visualization.
Ready to quiz yourself?