Build a Portfolio Risk Analytics Engine

Compute VaR and CVaR using three methods with an interactive dashboard.

Introduction

30 Second Summary

Every investor faces a simple question: how much could I lose tomorrow? Banks, hedge funds, and pension funds answer this question every single day using mathematical models that estimate their worst-case losses.

In this project, you will build a portfolio risk analytics engine in Python that downloads real stock market data and calculates risk using three different industry-standard methods. You will then visualize the results in an interactive dashboard you can explore in your browser.

What You'll Build

A Python risk engine powered by a Dash web dashboard that computes Value-at-Risk and Conditional Value-at-Risk using three methodologies on live market data, with interactive charts and a styled risk report table.

By the end of this project, you'll have:

  • A risk_engine.py script that fetches live market data and computes VaR using Historical Simulation, Variance-Covariance, and Monte Carlo methods.
  • An interactive Dash dashboard with a histogram overlaying VaR estimates as vertical lines and a grouped bar chart comparing risk metrics across all three methods.
  • A styled HTML risk report table rendered live in the browser, summarizing portfolio risk in a format similar to what a professional risk desk would produce.
  • Secret Mission: Build a stress testing framework that shocks your portfolio with historical crisis scenarios (2008 GFC, 2020 COVID crash) and adds a stress test visualization to the dashboard.

Are there any prerequisites?

You should be comfortable with basic Python (variables, functions, loops) and have a general familiarity with financial concepts like portfolio returns and standard deviation. No prior experience with risk modeling or Dash is required.

Before We Start

Before you can build a risk analytics engine, you need a clean environment with the right tools installed.

In this step, you will create a dedicated project folder, set up a Python virtual environment, and install the packages that power your risk calculations and dashboard.

In this step, get ready to:
  • Create a project folder and virtual environment.
  • Install the required Python packages.
  • Create the main project file.
Create a project folder and virtual environment

A virtual environment gives your project its own isolated set of packages. This prevents version conflicts with other Python projects on your machine.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir risk-engine
cd risk-engine
  • Create a virtual environment by running:
python -m venv venv

What does this command do?

python -m venv venv tells Python to create a virtual environment in a folder called venv inside your project directory.

This virtual environment stores packages separately from your system Python. When you activate it, pip install puts packages here instead of globally.

  • Activate the virtual environment:

macOS

  • Run this command to activate:
source venv/bin/activate

Windows

  • Run this command to activate:
venv\Scripts\activate

Your terminal prompt now shows (venv) at the beginning. This confirms the virtual environment is active.

Don't see (venv) in your prompt?

Make sure you ran the activate command from inside the risk-engine folder. If you see a "file not found" error, double-check you created the virtual environment first with python -m venv venv.

On Windows PowerShell, you may need to run Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser first if you see a security error.

help me activate my Python virtual environment

  • Open your project in VS Code by running:
code .

What does code . do?

The code . command opens VS Code with the current folder as your workspace. The dot (.) means "this directory."

Command not found?

If code is not recognized, open VS Code manually. Press Cmd+Space (macOS) or the Windows key (Windows), type VS Code, and press Enter. Then click File > Open Folder and select the risk-engine folder on your Desktop.

help me open my project folder in VS Code

Install the required packages

Your risk engine depends on five packages. Each handles a different part of the pipeline.

  • Install all five packages by running this command in your terminal:
pip install numpy pandas scipy dash yfinance

What are these packages?

  • numpy handles numerical arrays and statistical functions like percentiles.
  • pandas provides DataFrames for organizing stock price data.
  • scipy gives you the normal distribution functions needed for parametric VaR.
  • dash builds interactive web dashboards with Plotly charts.
  • yfinance downloads free historical stock price data from Yahoo Finance.
  • Verify all packages installed correctly by running:
python -c "import numpy, pandas, scipy, dash, yfinance; print('All packages installed successfully')"

You should see All packages installed successfully printed in your terminal.

Seeing an import error?

Make sure your virtual environment is active. Your terminal prompt should show (venv) at the beginning. If it does not, re-run the activate command from the previous substep.

If a specific package failed to install, try running pip install again with just that package name.

help me fix my package installation error

Create the project file

All of your risk engine code will live in a single Python file. This keeps the project focused and easy to run.

  • In VS Code, click File in the top menu bar.
  • Click New File.
  • Type risk_engine.py as the file name and press Enter.
  • Save the empty file with Cmd+S (macOS) or Ctrl+S (Windows).
  • Confirm the file runs without errors by switching to your terminal and running:
python risk_engine.py

You should see no output and no errors. The script runs and exits cleanly because the file is empty.

Seeing a 'file not found' error?

Make sure your terminal is in the risk-engine folder. Run ls (macOS) or dir (Windows) and confirm risk_engine.py appears in the listing.

help me find my risk_engine.py file

Your environment is ready. Next up, you will download real market data and calculate your first portfolio returns.

Fetch and Prepare Portfolio Data

Your environment is set up and ready to go. Now you need the raw material for risk analysis: real market data.

In this step, you will download five years of daily stock prices for a diversified portfolio using yfinance, then transform those prices into daily log returns. These returns are the foundation for every risk calculation in the project.

In this step, get ready to:
  • Download 5 years of daily stock data for a 5-stock portfolio using yfinance.
  • Calculate daily log returns and weighted portfolio returns.
  • Print summary statistics to validate data quality.
Add the imports

The risk engine relies on several libraries working together. Each handles a different responsibility in the pipeline.

  • In risk_engine.py, add the following imports at the top of the file:
import numpy as np
import pandas as pd
from scipy import stats
import plotly.graph_objects as go
from dash import Dash, html, dcc
import yfinance as yf

What do these imports do?

  • numpy handles numerical computations like percentiles and random number generation.
  • pandas manages time-series data as DataFrames (price tables with dates as rows).
  • scipy.stats provides statistical distribution functions for the parametric VaR method.
  • plotly.graph_objects builds interactive charts (histograms and bar charts).
  • dash serves the interactive dashboard in your browser.
  • yfinance downloads free stock market data from Yahoo Finance.
  • Save risk_engine.py.
  • Run the script in your terminal to confirm no import errors:
python risk_engine.py

The script should exit silently with no output. If you see an error, one of your packages did not install correctly in Step 1.

Seeing a ModuleNotFoundError?

Make sure your virtual environment is active. Your terminal prompt should show (venv) at the beginning.

If it does and you still see the error, reinstall the missing package with pip install <package-name>.

Ask for help: Why am I getting ModuleNotFoundError even though I installed the packages?

Download and prepare market data

A portfolio is a collection of assets held in specific proportions. You will define a 5-stock equal-weight portfolio and fetch its historical price data.

  • Add the following code below your imports in risk_engine.py:
# =============================================================================
# SECTION 1: Data Download and Preparation
# =============================================================================

# Define portfolio
tickers = ["AAPL", "MSFT", "JPM", "JNJ", "XOM"]
weights = np.array([0.2, 0.2, 0.2, 0.2, 0.2])

# Download 5 years of daily data
print("Downloading market data...")
data = yf.download(tickers, period="5y", interval="1d", auto_adjust=True, progress=False)

# Extract close prices
prices = data["Close"]

What does this code do?

tickers lists the five stocks: Apple, Microsoft, JPMorgan, Johnson & Johnson, and ExxonMobil. These span tech, finance, healthcare, and energy for diversification.

weights assigns 20% of the portfolio to each stock (equal-weight). In practice, a risk desk might use market-cap or optimized weights.

yf.download() fetches 5 years of daily adjusted close prices. The auto_adjust=True parameter applies stock split and dividend adjustments automatically. The progress=False flag suppresses download progress bars for cleaner output.

data["Close"] extracts just the closing prices into a clean DataFrame with one column per stock.

  • Save risk_engine.py and run the script:
python risk_engine.py

You should see Downloading market data... printed in your terminal. The download may take a few seconds depending on your internet connection.

Getting a connection error?

yfinance needs an internet connection to reach Yahoo Finance servers. Check that you are connected to the internet and try running the script again.

Ask for help: Why is yfinance failing to download data?

Calculate returns and print summary statistics

Raw prices are not useful for risk calculations because a $100 stock moving $2 is very different from a $10 stock moving $2. Log returns normalize price changes into percentage moves, making stocks comparable regardless of price level.

  • Add the following code below the prices = data["Close"] line in risk_engine.py:
# Calculate log returns
log_returns = np.log(prices / prices.shift(1)).dropna()

# Calculate weighted portfolio returns
portfolio_returns = log_returns.dot(weights)

# Summary statistics
print(f"\n{'='*60}")
print("PORTFOLIO DATA SUMMARY")
print(f"{'='*60}")
print(f"Date range: {portfolio_returns.index[0].strftime('%Y-%m-%d')} to {portfolio_returns.index[-1].strftime('%Y-%m-%d')}")
print(f"Trading days: {len(portfolio_returns)}")
print(f"Mean daily return: {portfolio_returns.mean():.6f}")
print(f"Daily std deviation: {portfolio_returns.std():.6f}")
print(f"Annualized volatility: {portfolio_returns.std() * np.sqrt(252):.4f}")
print(f"Min daily return: {portfolio_returns.min():.6f}")
print(f"Max daily return: {portfolio_returns.max():.6f}")

What does this code do?

np.log(prices / prices.shift(1)) computes the natural logarithm of today's price divided by yesterday's price for each stock. This gives daily log returns. The .dropna() removes the first row (which has no "yesterday" to compare against).

log_returns.dot(weights) multiplies each stock's daily return by its portfolio weight (20%) and sums them. The result is a single time series of daily portfolio returns.

The summary statistics block prints key metrics. Annualized volatility multiplies the daily standard deviation by the square root of 252 (trading days per year) to express risk on a yearly basis.

  • Save risk_engine.py and run the script:
python risk_engine.py

You should see a formatted PORTFOLIO DATA SUMMARY block showing the date range, number of trading days, mean return, standard deviation, annualized volatility, and min/max returns.

Seeing NaN or empty output?

If your summary shows NaN values, the data download may have returned an empty DataFrame. Confirm your internet connection is working and that the tickers are spelled exactly as shown: AAPL, MSFT, JPM, JNJ, XOM.

Ask for help: Why is my portfolio data showing NaN values?

Why log returns instead of simple returns?

Log returns are additive across time. If Monday's log return is 0.01 and Tuesday's is -0.02, the two-day log return is simply 0.01 + (-0.02) = -0.01. This mathematical property makes multi-period analysis straightforward.

Log returns are the standard in quantitative finance for this reason. For small daily moves, they closely approximate simple percentage returns.

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the complete risk_engine.py so far:

import numpy as np
import pandas as pd
from scipy import stats
import plotly.graph_objects as go
from dash import Dash, html, dcc
import yfinance as yf

# =============================================================================
# SECTION 1: Data Download and Preparation
# =============================================================================

# Define portfolio
tickers = ["AAPL", "MSFT", "JPM", "JNJ", "XOM"]
weights = np.array([0.2, 0.2, 0.2, 0.2, 0.2])

# Download 5 years of daily data
print("Downloading market data...")
data = yf.download(tickers, period="5y", interval="1d", auto_adjust=True, progress=False)

# Extract close prices
prices = data["Close"]

# Calculate log returns
log_returns = np.log(prices / prices.shift(1)).dropna()

# Calculate weighted portfolio returns
portfolio_returns = log_returns.dot(weights)

# Summary statistics
print(f"\n{'='*60}")
print("PORTFOLIO DATA SUMMARY")
print(f"{'='*60}")
print(f"Date range: {portfolio_returns.index[0].strftime('%Y-%m-%d')} to {portfolio_returns.index[-1].strftime('%Y-%m-%d')}")
print(f"Trading days: {len(portfolio_returns)}")
print(f"Mean daily return: {portfolio_returns.mean():.6f}")
print(f"Daily std deviation: {portfolio_returns.std():.6f}")
print(f"Annualized volatility: {portfolio_returns.std() * np.sqrt(252):.4f}")
print(f"Min daily return: {portfolio_returns.min():.6f}")
print(f"Max daily return: {portfolio_returns.max():.6f}")

Your data pipeline is working. You have real market data transformed into portfolio returns and validated with summary statistics. Next up, you will use these returns to calculate Value-at-Risk using the Historical Simulation method.

Calculate Historical Simulation VaR and CVaR

Your portfolio data is downloaded, cleaned, and ready to analyze. Now it's time to answer the central question: how much could this portfolio lose on a bad day?

Historical Simulation is the simplest VaR methodology. It makes no assumptions about the shape of return distributions. Instead, it looks directly at what actually happened in the past and uses those real outcomes to estimate future risk.

In this step, get ready to:
  • Calculate Value-at-Risk at 95% and 99% confidence using historical percentiles.
  • Calculate Conditional Value-at-Risk (Expected Shortfall) at both confidence levels.
  • Print the results with an interpretation of what the numbers mean.
Implement the Historical Simulation method

The Historical Simulation approach to VaR is straightforward. You sort all observed portfolio returns from worst to best, then pick the return at a specific percentile. The 5th percentile gives you the 95% VaR. The 1st percentile gives you the 99% VaR.

  • In risk_engine.py, add the following code at the bottom of your file, below the summary statistics print statements:
# =============================================================================
# SECTION 2: Historical Simulation VaR and CVaR
# =============================================================================

print(f"\n{'='*60}")
print("METHOD 1: HISTORICAL SIMULATION")
print(f"{'='*60}")

# VaR at 95% and 99% confidence
hist_var_95 = np.percentile(portfolio_returns, 5)
hist_var_99 = np.percentile(portfolio_returns, 1)

What does this code do?

numpy.percentile(portfolio_returns, 5) finds the return value that only 5% of all observed days fell below. This is the 95% VaR: you can say with 95% confidence that the portfolio won't lose more than this amount on any given day.

The 99% VaR uses the 1st percentile for a stricter threshold. No distribution assumptions are needed here. You're using the actual returns that happened in the market.

Why is the 95% VaR the 5th percentile?

VaR asks "what's my worst-case loss at X% confidence?" The 95% confidence level means you want the threshold where only 5% of outcomes are worse. That's the 5th percentile of the return distribution. Similarly, 99% confidence corresponds to the 1st percentile.

Calculate CVaR and print results

VaR tells you the threshold, but not how bad things get beyond it. CVaR (also called Expected Shortfall) answers that by computing the average of all losses that exceed the VaR threshold.

  • Add the CVaR calculations and print statements directly below the VaR code you just wrote:
# CVaR (Expected Shortfall)
hist_cvar_95 = portfolio_returns[portfolio_returns <= hist_var_95].mean()
hist_cvar_99 = portfolio_returns[portfolio_returns <= hist_var_99].mean()

print(f"95% VaR: {hist_var_95:.6f} ({hist_var_95*100:.4f}%)")
print(f"99% VaR: {hist_var_99:.6f} ({hist_var_99*100:.4f}%)")
print(f"95% CVaR: {hist_cvar_95:.6f} ({hist_cvar_95*100:.4f}%)")
print(f"99% CVaR: {hist_cvar_99:.6f} ({hist_cvar_99*100:.4f}%)")
print(f"\nInterpretation: On the worst 5% of days, the portfolio loses at least {abs(hist_var_95)*100:.2f}%")

What does this code do?

portfolio_returns[portfolio_returns <= hist_var_95] filters the returns series to only the days that were at or below the VaR threshold. Taking the .mean() of those worst days gives you the CVaR: the average loss on the tail days.

CVaR is always a larger loss than VaR because it averages the worst outcomes beyond the threshold. Risk desks use both metrics together. VaR shows the boundary. CVaR shows how painful it gets past the boundary.

  • Save risk_engine.py.
  • Run the script in your terminal by running:
python risk_engine.py
  • You should see a METHOD 1: HISTORICAL SIMULATION section printed in your terminal with VaR and CVaR values at 95% and 99% confidence levels, followed by an interpretation line.

Seeing a NameError or empty output?

  • If you see NameError: name 'portfolio_returns' is not defined, make sure your new code is below the existing Section 1 code, not replacing it.
  • If the VaR values look like zero or NaN, check that your data download completed successfully. The portfolio data summary section above should show a positive number of trading days.
  • help me debug my Historical Simulation VaR output

Why store results in separate variables?

The four variables (hist_var_95, hist_var_99, hist_cvar_95, hist_cvar_99) are named to match the pattern you'll use for all three methods. Later steps will produce vc_var_95 and mc_var_95 for the other two approaches. Consistent naming makes the final dashboard comparison straightforward.

✔️ 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's the complete risk_engine.py so far:

import numpy as np
import pandas as pd
from scipy import stats
import plotly.graph_objects as go
from dash import Dash, html, dcc
import yfinance as yf

# =============================================================================
# SECTION 1: Data Download and Preparation
# =============================================================================

# Define portfolio
tickers = ["AAPL", "MSFT", "JPM", "JNJ", "XOM"]
weights = np.array([0.2, 0.2, 0.2, 0.2, 0.2])

# Download 5 years of daily data
print("Downloading market data...")
data = yf.download(tickers, period="5y", interval="1d", auto_adjust=True, progress=False)

# Extract close prices
prices = data["Close"]

# Calculate log returns
log_returns = np.log(prices / prices.shift(1)).dropna()

# Calculate weighted portfolio returns
portfolio_returns = log_returns.dot(weights)

# Summary statistics
print(f"\n{'='*60}")
print("PORTFOLIO DATA SUMMARY")
print(f"{'='*60}")
print(f"Date range: {portfolio_returns.index[0].strftime('%Y-%m-%d')} to {portfolio_returns.index[-1].strftime('%Y-%m-%d')}")
print(f"Trading days: {len(portfolio_returns)}")
print(f"Mean daily return: {portfolio_returns.mean():.6f}")
print(f"Daily std deviation: {portfolio_returns.std():.6f}")
print(f"Annualized volatility: {portfolio_returns.std() * np.sqrt(252):.4f}")
print(f"Min daily return: {portfolio_returns.min():.6f}")
print(f"Max daily return: {portfolio_returns.max():.6f}")

# =============================================================================
# SECTION 2: Historical Simulation VaR and CVaR
# =============================================================================

print(f"\n{'='*60}")
print("METHOD 1: HISTORICAL SIMULATION")
print(f"{'='*60}")

# VaR at 95% and 99% confidence
hist_var_95 = np.percentile(portfolio_returns, 5)
hist_var_99 = np.percentile(portfolio_returns, 1)

# CVaR (Expected Shortfall)
hist_cvar_95 = portfolio_returns[portfolio_returns <= hist_var_95].mean()
hist_cvar_99 = portfolio_returns[portfolio_returns <= hist_var_99].mean()

print(f"95% VaR: {hist_var_95:.6f} ({hist_var_95*100:.4f}%)")
print(f"99% VaR: {hist_var_99:.6f} ({hist_var_99*100:.4f}%)")
print(f"95% CVaR: {hist_cvar_95:.6f} ({hist_cvar_95*100:.4f}%)")
print(f"99% CVaR: {hist_cvar_99:.6f} ({hist_cvar_99*100:.4f}%)")
print(f"\nInterpretation: On the worst 5% of days, the portfolio loses at least {abs(hist_var_95)*100:.2f}%")

Your first risk method is complete. Historical Simulation works well because it uses real data with no assumptions. Next, you'll implement the Variance-Covariance method, which trades that assumption-free approach for speed by assuming returns follow a normal distribution.

Calculate Variance-Covariance VaR and CVaR

Your Historical Simulation method used raw observed returns to estimate risk. That approach makes no assumptions about how returns are distributed, which is its strength.

Now you will implement the Variance-Covariance method, which takes a fundamentally different approach. It assumes portfolio returns follow a normal distribution and uses the portfolio's mean and standard deviation to calculate VaR analytically. This is faster to compute, but it comes with a well-known tradeoff: real markets have fatter tails than the normal distribution predicts.

In this step, get ready to:
  • Calculate parametric VaR using normal distribution z-scores at 95% and 99% confidence.
  • Calculate parametric CVaR using the conditional expectation formula.
  • Print results and note the normality assumption that makes this method faster but less accurate in the tails.
Implement the Variance-Covariance VaR calculation

The Variance-Covariance method calculates VaR using a formula: VaR = mean + z_score * standard_deviation. The z-score comes from the inverse of the normal distribution's cumulative density function, which tells you how many standard deviations away a given confidence level sits.

  • In risk_engine.py, scroll to the bottom of the file (below the Historical Simulation print statements).
  • Add the Variance-Covariance section header and VaR calculation by copying this code:
# =============================================================================
# SECTION 3: Variance-Covariance (Parametric) VaR and CVaR
# =============================================================================

print(f"\n{'='*60}")
print("METHOD 2: VARIANCE-COVARIANCE (PARAMETRIC)")
print(f"{'='*60}")

mu = portfolio_returns.mean()
sigma = portfolio_returns.std()

# VaR using normal distribution quantiles
vc_var_95 = mu + stats.norm.ppf(0.05) * sigma
vc_var_99 = mu + stats.norm.ppf(0.01) * sigma

What does this code do?

mu and sigma capture the portfolio's average daily return and daily standard deviation. These two numbers fully describe the assumed normal distribution.

stats.norm.ppf(0.05) returns the z-score for the 5th percentile of the normal distribution (approximately -1.645). Multiplying by sigma and adding mu gives the return threshold that the portfolio will only fall below 5% of the time, assuming normality.

  • Save risk_engine.py.

Why this formula instead of sorting raw returns?

Historical Simulation sorted actual returns and picked a percentile. The Variance-Covariance method skips the sorting entirely. It uses a mathematical formula that maps confidence levels directly to loss thresholds via the normal distribution.

This makes it extremely fast to compute (no need to store or sort thousands of returns), which is why many risk desks use it for real-time monitoring. The cost is accuracy in the tails.

Calculate parametric CVaR and print results

Parametric CVaR uses the conditional expectation formula for the normal distribution. Instead of averaging observed losses below VaR (like Historical Simulation did), it calculates the expected loss in the tail analytically using the probability density function.

  • Add the CVaR calculation and print statements directly below the VaR lines you just added:
# Parametric CVaR using conditional expectation formula
vc_cvar_95 = mu - sigma * (stats.norm.pdf(stats.norm.ppf(0.05)) / 0.05)
vc_cvar_99 = mu - sigma * (stats.norm.pdf(stats.norm.ppf(0.01)) / 0.01)

print(f"95% VaR: {vc_var_95:.6f} ({vc_var_95*100:.4f}%)")
print(f"99% VaR: {vc_var_99:.6f} ({vc_var_99*100:.4f}%)")
print(f"95% CVaR: {vc_cvar_95:.6f} ({vc_cvar_95*100:.4f}%)")
print(f"99% CVaR: {vc_cvar_99:.6f} ({vc_cvar_99*100:.4f}%)")
print(f"\nNote: Assumes normally distributed returns. Underestimates tail risk.")

What does the CVaR formula do?

The formula mu - sigma * (norm.pdf(norm.ppf(alpha)) / alpha) calculates the average loss you'd expect in the worst alpha-percent of days. norm.pdf() gives the height of the bell curve at the VaR point, and dividing by alpha scales it to the conditional tail mean.

The final print statement explicitly notes the normality assumption. On a real risk desk, this caveat would accompany any parametric VaR report because markets consistently exhibit fatter tails than the normal distribution predicts.

  • Save risk_engine.py.
  • Run the script in your terminal by running:
python risk_engine.py
  • You should see a new METHOD 2: VARIANCE-COVARIANCE (PARAMETRIC) section in the terminal output with VaR and CVaR values at both 95% and 99% confidence, followed by the normality assumption note.

Seeing a NameError for stats?

Make sure your imports at the top of the file include from scipy import stats. This import was added in the previous step but double check it is still there.

Also confirm that mu and sigma are defined before the VaR lines. If either is missing, the VaR formula will fail.

Help me debug my Variance-Covariance VaR calculation. I'm getting a NameError.

✔️ Awesome, I've got everything!

Great. Double check you have saved risk_engine.py and your terminal shows both METHOD 1 and METHOD 2 results.

ⓧ I'd like to double check the full code

Here is the full risk_engine.py file as it should look at the end of this step:

import numpy as np
import pandas as pd
from scipy import stats
import plotly.graph_objects as go
from dash import Dash, html, dcc
import yfinance as yf

# =============================================================================
# SECTION 1: Data Download and Preparation
# =============================================================================

# Define portfolio
tickers = ["AAPL", "MSFT", "JPM", "JNJ", "XOM"]
weights = np.array([0.2, 0.2, 0.2, 0.2, 0.2])

# Download 5 years of daily data
print("Downloading market data...")
data = yf.download(tickers, period="5y", interval="1d", auto_adjust=True, progress=False)

# Extract close prices
prices = data["Close"]

# Calculate log returns
log_returns = np.log(prices / prices.shift(1)).dropna()

# Calculate weighted portfolio returns
portfolio_returns = log_returns.dot(weights)

# Summary statistics
print(f"\n{'='*60}")
print("PORTFOLIO DATA SUMMARY")
print(f"{'='*60}")
print(f"Date range: {portfolio_returns.index[0].strftime('%Y-%m-%d')} to {portfolio_returns.index[-1].strftime('%Y-%m-%d')}")
print(f"Trading days: {len(portfolio_returns)}")
print(f"Mean daily return: {portfolio_returns.mean():.6f}")
print(f"Daily std deviation: {portfolio_returns.std():.6f}")
print(f"Annualized volatility: {portfolio_returns.std() * np.sqrt(252):.4f}")
print(f"Min daily return: {portfolio_returns.min():.6f}")
print(f"Max daily return: {portfolio_returns.max():.6f}")

# =============================================================================
# SECTION 2: Historical Simulation VaR and CVaR
# =============================================================================

print(f"\n{'='*60}")
print("METHOD 1: HISTORICAL SIMULATION")
print(f"{'='*60}")

# VaR at 95% and 99% confidence
hist_var_95 = np.percentile(portfolio_returns, 5)
hist_var_99 = np.percentile(portfolio_returns, 1)

# CVaR (Expected Shortfall)
hist_cvar_95 = portfolio_returns[portfolio_returns <= hist_var_95].mean()
hist_cvar_99 = portfolio_returns[portfolio_returns <= hist_var_99].mean()

print(f"95% VaR: {hist_var_95:.6f} ({hist_var_95*100:.4f}%)")
print(f"99% VaR: {hist_var_99:.6f} ({hist_var_99*100:.4f}%)")
print(f"95% CVaR: {hist_cvar_95:.6f} ({hist_cvar_95*100:.4f}%)")
print(f"99% CVaR: {hist_cvar_99:.6f} ({hist_cvar_99*100:.4f}%)")
print(f"\nInterpretation: On the worst 5% of days, the portfolio loses at least {abs(hist_var_95)*100:.2f}%")

# =============================================================================
# SECTION 3: Variance-Covariance (Parametric) VaR and CVaR
# =============================================================================

print(f"\n{'='*60}")
print("METHOD 2: VARIANCE-COVARIANCE (PARAMETRIC)")
print(f"{'='*60}")

mu = portfolio_returns.mean()
sigma = portfolio_returns.std()

# VaR using normal distribution quantiles
vc_var_95 = mu + stats.norm.ppf(0.05) * sigma
vc_var_99 = mu + stats.norm.ppf(0.01) * sigma

# Parametric CVaR using conditional expectation formula
vc_cvar_95 = mu - sigma * (stats.norm.pdf(stats.norm.ppf(0.05)) / 0.05)
vc_cvar_99 = mu - sigma * (stats.norm.pdf(stats.norm.ppf(0.01)) / 0.01)

print(f"95% VaR: {vc_var_95:.6f} ({vc_var_95*100:.4f}%)")
print(f"99% VaR: {vc_var_99:.6f} ({vc_var_99*100:.4f}%)")
print(f"95% CVaR: {vc_cvar_95:.6f} ({vc_cvar_95*100:.4f}%)")
print(f"99% CVaR: {vc_cvar_99:.6f} ({vc_cvar_99*100:.4f}%)")
print(f"\nNote: Assumes normally distributed returns. Underestimates tail risk.")

You now have two methods producing different VaR estimates from the same portfolio data. Next up, you will implement Monte Carlo simulation as a third approach, then build an interactive Dash dashboard that visualizes all three side by side.

Run Monte Carlo Simulation and Build the Dash Dashboard

You now have two VaR methods producing different risk estimates from the same data. The final methodology, Monte Carlo simulation, takes a different approach: instead of relying on historical data or a formula, it generates thousands of synthetic scenarios to estimate the loss distribution.

In this step, you will implement Monte Carlo VaR and then bring all three methods together in an interactive Dash dashboard. The dashboard will let you visually compare how each method estimates risk from the same portfolio.

In this step, get ready to:
  • Simulate 10,000 portfolio return scenarios using Monte Carlo.
  • Build interactive Plotly charts comparing all three VaR methods.
  • Launch a Dash dashboard with a risk report table at http://127.0.0.1:8050.
Implement Monte Carlo simulation

Monte Carlo simulation generates a large number of random scenarios based on the portfolio's observed statistics. Instead of looking at what actually happened (Historical) or assuming a formula (Variance-Covariance), it asks: what could happen?

  • In risk_engine.py, add the Monte Carlo section below the Variance-Covariance code by copying this code at the bottom of your file:
# =============================================================================
# SECTION 4: Monte Carlo Simulation VaR and CVaR
# =============================================================================

print(f"\n{'='*60}")
print("METHOD 3: MONTE CARLO SIMULATION")
print(f"{'='*60}")

np.random.seed(42)
n_simulations = 10000

# Simulate returns using observed mean and std
simulated_returns = np.random.normal(mu, sigma, n_simulations)

# VaR and CVaR from simulated distribution
mc_var_95 = np.percentile(simulated_returns, 5)
mc_var_99 = np.percentile(simulated_returns, 1)
mc_cvar_95 = simulated_returns[simulated_returns <= mc_var_95].mean()
mc_cvar_99 = simulated_returns[simulated_returns <= mc_var_99].mean()

print(f"95% VaR: {mc_var_95:.6f} ({mc_var_95*100:.4f}%)")
print(f"99% VaR: {mc_var_99:.6f} ({mc_var_99*100:.4f}%)")
print(f"95% CVaR: {mc_cvar_95:.6f} ({mc_cvar_95*100:.4f}%)")
print(f"99% CVaR: {mc_cvar_99:.6f} ({mc_cvar_99*100:.4f}%)")
print(f"\nSimulations run: {n_simulations:,}")

What does this code do?

The random seed (np.random.seed(42)) ensures the simulation produces identical results every time you run it. This makes your analysis reproducible.

The simulation draws 10,000 random returns from a normal distribution using the portfolio's actual mean (mu) and standard deviation (sigma). VaR and CVaR are then calculated from this synthetic distribution using the same percentile approach as Historical Simulation.

Why does Monte Carlo converge to Variance-Covariance?

Because you are feeding it normal inputs (numpy.random.normal), the simulated distribution converges to the same normal curve the parametric method assumes. The real power of Monte Carlo emerges when you model non-normal distributions, fat tails, or complex multi-asset dependencies.

  • Save risk_engine.py.
  • Run the script in your terminal to confirm the Monte Carlo results print:
python risk_engine.py
  • You should see the METHOD 3: MONTE CARLO SIMULATION header followed by VaR and CVaR values at 95% and 99% confidence, plus a confirmation that 10,000 simulations ran.

Not seeing Monte Carlo output?

Check that your Monte Carlo section appears after the Variance-Covariance section. The code uses mu and sigma which are defined in the Variance-Covariance section above.

If you see a NameError, make sure there is no gap or typo between the sections.

help me debug my Monte Carlo simulation code

Build the interactive Plotly charts

Now that all three methods have produced their results, you will visualize them together. The first chart overlays all three VaR estimates as vertical lines on the portfolio's actual return distribution. The second chart uses grouped bars to compare every metric side by side.

  • Add the histogram chart with VaR overlay lines below the Monte Carlo section in risk_engine.py:
# =============================================================================
# SECTION 5: Interactive Dash Dashboard
# =============================================================================

# --- Figure 1: VaR Comparison on Return Distribution ---
fig_distribution = go.Figure()

fig_distribution.add_trace(go.Histogram(
    x=portfolio_returns,
    nbinsx=100,
    histnorm="probability density",
    marker_color="rgba(135, 206, 235, 0.7)",
    name="Portfolio Returns",
))

fig_distribution.add_vline(x=hist_var_95, line_width=2, line_color="blue", line_dash="solid",
                           annotation_text=f"Historical: {hist_var_95*100:.2f}%", annotation_position="top left")
fig_distribution.add_vline(x=vc_var_95, line_width=2, line_color="red", line_dash="dash",
                           annotation_text=f"Var-Covar: {vc_var_95*100:.2f}%", annotation_position="top left")
fig_distribution.add_vline(x=mc_var_95, line_width=2, line_color="green", line_dash="dot",
                           annotation_text=f"Monte Carlo: {mc_var_95*100:.2f}%", annotation_position="top left")

fig_distribution.update_layout(
    title="Portfolio VaR Comparison: 95% Confidence Level",
    xaxis_title="Daily Portfolio Return",
    yaxis_title="Density",
    template="plotly_white",
    height=500,
)

What does this chart show?

The histogram plots your portfolio's actual daily returns as a density curve. Each vertical line marks where a different VaR method places the 95% loss threshold. If the lines cluster together, the methods agree. If they spread apart, you are seeing methodological differences in action.

Blue (Historical) uses raw data. Red (Variance-Covariance) assumes a bell curve. Green (Monte Carlo) simulates from observed parameters.

  • Add the grouped bar chart below the histogram code:
# --- Figure 2: Summary Bar Chart ---
methods = ["Historical Simulation", "Variance-Covariance", "Monte Carlo"]

fig_bars = go.Figure()

fig_bars.add_trace(go.Bar(
    name="VaR 95%",
    x=methods,
    y=[abs(hist_var_95)*100, abs(vc_var_95)*100, abs(mc_var_95)*100],
    marker_color="steelblue",
))
fig_bars.add_trace(go.Bar(
    name="CVaR 95%",
    x=methods,
    y=[abs(hist_cvar_95)*100, abs(vc_cvar_95)*100, abs(mc_cvar_95)*100],
    marker_color="coral",
))
fig_bars.add_trace(go.Bar(
    name="VaR 99%",
    x=methods,
    y=[abs(hist_var_99)*100, abs(vc_var_99)*100, abs(mc_var_99)*100],
    marker_color="darkblue",
))
fig_bars.add_trace(go.Bar(
    name="CVaR 99%",
    x=methods,
    y=[abs(hist_cvar_99)*100, abs(vc_cvar_99)*100, abs(mc_cvar_99)*100],
    marker_color="darkred",
))

fig_bars.update_layout(
    title="VaR and CVaR Comparison Across Methods",
    yaxis_title="Loss (%)",
    barmode="group",
    template="plotly_white",
    height=500,
)

What does this chart show?

The grouped bar chart places all four metrics (VaR 95%, CVaR 95%, VaR 99%, CVaR 99%) side by side for each method. This makes it easy to spot which method is most conservative (tallest bars) and which underestimates tail risk.

The barmode="group" setting clusters the bars by method so you can compare visually.

  • Save risk_engine.py. You will not run the script yet because the figures are not displayed without the Dash app.

Seeing an IndentationError or SyntaxError?

Make sure the multi-line function calls (like go.Histogram(...) and go.Bar(...)) have matching parentheses. Each opening ( needs a closing ) and each trailing comma after keyword arguments is valid Python.

help me fix a syntax error in my Plotly chart code

Create the Dash app and launch the dashboard

The Dash app serves your Plotly figures in a browser and adds a styled HTML risk report table underneath. This is the final section that ties everything together into a live, interactive dashboard.

  • Add the Dash app code below your figure definitions in risk_engine.py:
# --- Dash App ---
app = Dash(__name__)

app.layout = html.Div(style={"fontFamily": "Arial, sans-serif", "maxWidth": "1400px", "margin": "0 auto", "padding": "20px"}, children=[
    html.H1("Portfolio Risk Analytics Engine", style={"textAlign": "center", "color": "#2c3e50"}),
    html.P("Equal-weight portfolio: AAPL, MSFT, JPM, JNJ, XOM | 5 years daily data",
           style={"textAlign": "center", "color": "#7f8c8d", "fontSize": "16px"}),

    # Charts side by side
    html.Div(style={"display": "flex", "gap": "20px", "flexWrap": "wrap"}, children=[
        html.Div(style={"flex": "1", "minWidth": "600px"}, children=[
            dcc.Graph(figure=fig_distribution),
        ]),
        html.Div(style={"flex": "1", "minWidth": "600px"}, children=[
            dcc.Graph(figure=fig_bars),
        ]),
    ]),

    # Risk Report Table
    html.H2("Risk Report", style={"marginTop": "40px", "color": "#2c3e50"}),
    html.Table(style={"width": "100%", "borderCollapse": "collapse", "marginTop": "10px"}, children=[
        html.Thead(children=[
            html.Tr(children=[
                html.Th("Method", style={"border": "1px solid #ddd", "padding": "12px", "backgroundColor": "#2c3e50", "color": "white", "textAlign": "left"}),
                html.Th("VaR 95%", style={"border": "1px solid #ddd", "padding": "12px", "backgroundColor": "#2c3e50", "color": "white", "textAlign": "right"}),
                html.Th("CVaR 95%", style={"border": "1px solid #ddd", "padding": "12px", "backgroundColor": "#2c3e50", "color": "white", "textAlign": "right"}),
                html.Th("VaR 99%", style={"border": "1px solid #ddd", "padding": "12px", "backgroundColor": "#2c3e50", "color": "white", "textAlign": "right"}),
                html.Th("CVaR 99%", style={"border": "1px solid #ddd", "padding": "12px", "backgroundColor": "#2c3e50", "color": "white", "textAlign": "right"}),
            ])
        ]),
        html.Tbody(children=[
            html.Tr(children=[
                html.Td("Historical Simulation", style={"border": "1px solid #ddd", "padding": "10px"}),
                html.Td(f"{hist_var_95*100:.4f}%", style={"border": "1px solid #ddd", "padding": "10px", "textAlign": "right"}),
                html.Td(f"{hist_cvar_95*100:.4f}%", style={"border": "1px solid #ddd", "padding": "10px", "textAlign": "right"}),
                html.Td(f"{hist_var_99*100:.4f}%", style={"border": "1px solid #ddd", "padding": "10px", "textAlign": "right"}),
                html.Td(f"{hist_cvar_99*100:.4f}%", style={"border": "1px solid #ddd", "padding": "10px", "textAlign": "right"}),
            ]),
            html.Tr(children=[
                html.Td("Variance-Covariance", style={"border": "1px solid #ddd", "padding": "10px"}),
                html.Td(f"{vc_var_95*100:.4f}%", style={"border": "1px solid #ddd", "padding": "10px", "textAlign": "right"}),
                html.Td(f"{vc_cvar_95*100:.4f}%", style={"border": "1px solid #ddd", "padding": "10px", "textAlign": "right"}),
                html.Td(f"{vc_var_99*100:.4f}%", style={"border": "1px solid #ddd", "padding": "10px", "textAlign": "right"}),
                html.Td(f"{vc_cvar_99*100:.4f}%", style={"border": "1px solid #ddd", "padding": "10px", "textAlign": "right"}),
            ]),
            html.Tr(children=[
                html.Td("Monte Carlo", style={"border": "1px solid #ddd", "padding": "10px"}),
                html.Td(f"{mc_var_95*100:.4f}%", style={"border": "1px solid #ddd", "padding": "10px", "textAlign": "right"}),
                html.Td(f"{mc_cvar_95*100:.4f}%", style={"border": "1px solid #ddd", "padding": "10px", "textAlign": "right"}),
                html.Td(f"{mc_var_99*100:.4f}%", style={"border": "1px solid #ddd", "padding": "10px", "textAlign": "right"}),
                html.Td(f"{mc_cvar_99*100:.4f}%", style={"border": "1px solid #ddd", "padding": "10px", "textAlign": "right"}),
            ]),
        ]),
    ]),

    # Key insight
    html.Div(style={"marginTop": "20px", "padding": "15px", "backgroundColor": "#f8f9fa", "borderLeft": "4px solid #3498db", "borderRadius": "4px"}, children=[
        html.P("Key Insight: Historical Simulation captures actual tail behavior from observed data. "
               "Variance-Covariance underestimates risk due to the normality assumption. "
               "Monte Carlo with normal inputs converges to Variance-Covariance as simulations increase, "
               "but its real power emerges when modeling non-normal or complex distributions.",
               style={"margin": "0", "color": "#2c3e50"}),
    ]),
])

if __name__ == "__main__":
    print(f"\n{'='*60}")
    print("DASHBOARD LIVE at http://127.0.0.1:8050")
    print(f"{'='*60}")
    app.run(debug=True)

What does this layout do?

The app.layout defines the entire page structure. The two dcc.Graph components render your Plotly figures as interactive charts. The html.Table constructs a styled risk report showing exact values for all methods and confidence levels.

The Key Insight div at the bottom summarizes what the data reveals about each methodology's strengths and limitations. The if __name__ == "__main__" block ensures the server only starts when you run the script directly.

  • Save risk_engine.py.
  • Launch the dashboard by running:
python risk_engine.py
  • Open your browser and navigate to http://127.0.0.1:8050.
  • You should see the Portfolio Risk Analytics Engine dashboard with two interactive charts side by side, a Risk Report table below them, and a Key Insight summary at the bottom.
  • Hover over the histogram bars and bar chart to see interactive tooltips with exact values.

Dashboard not loading?

Make sure your terminal shows the message DASHBOARD LIVE at http://127.0.0.1:8050. If the script exits immediately with an error, check for unmatched parentheses or brackets in the app.layout section. Nested lists and dictionaries make bracket-matching tricky.

If you see a port-in-use error, stop any previous running instance with Ctrl+C in the terminal, then run the script again.

help me debug my Dash dashboard

✔️ Awesome, I've got everything!

Great. Double check you have saved your file and your dashboard is running in the browser.

ⓧ I'd like to double check the full code

Here is the complete risk_engine.py file at this point:

import numpy as np
import pandas as pd
from scipy import stats
import plotly.graph_objects as go
from dash import Dash, html, dcc
import yfinance as yf

# =============================================================================
# SECTION 1: Data Download and Preparation
# =============================================================================

# Define portfolio
tickers = ["AAPL", "MSFT", "JPM", "JNJ", "XOM"]
weights = np.array([0.2, 0.2, 0.2, 0.2, 0.2])

# Download 5 years of daily data
print("Downloading market data...")
data = yf.download(tickers, period="5y", interval="1d", auto_adjust=True, progress=False)

# Extract close prices
prices = data["Close"]

# Calculate log returns
log_returns = np.log(prices / prices.shift(1)).dropna()

# Calculate weighted portfolio returns
portfolio_returns = log_returns.dot(weights)

# Summary statistics
print(f"\n{'='*60}")
print("PORTFOLIO DATA SUMMARY")
print(f"{'='*60}")
print(f"Date range: {portfolio_returns.index[0].strftime('%Y-%m-%d')} to {portfolio_returns.index[-1].strftime('%Y-%m-%d')}")
print(f"Trading days: {len(portfolio_returns)}")
print(f"Mean daily return: {portfolio_returns.mean():.6f}")
print(f"Daily std deviation: {portfolio_returns.std():.6f}")
print(f"Annualized volatility: {portfolio_returns.std() * np.sqrt(252):.4f}")
print(f"Min daily return: {portfolio_returns.min():.6f}")
print(f"Max daily return: {portfolio_returns.max():.6f}")

# =============================================================================
# SECTION 2: Historical Simulation VaR and CVaR
# =============================================================================

print(f"\n{'='*60}")
print("METHOD 1: HISTORICAL SIMULATION")
print(f"{'='*60}")

# VaR at 95% and 99% confidence
hist_var_95 = np.percentile(portfolio_returns, 5)
hist_var_99 = np.percentile(portfolio_returns, 1)

# CVaR (Expected Shortfall)
hist_cvar_95 = portfolio_returns[portfolio_returns <= hist_var_95].mean()
hist_cvar_99 = portfolio_returns[portfolio_returns <= hist_var_99].mean()

print(f"95% VaR: {hist_var_95:.6f} ({hist_var_95*100:.4f}%)")
print(f"99% VaR: {hist_var_99:.6f} ({hist_var_99*100:.4f}%)")
print(f"95% CVaR: {hist_cvar_95:.6f} ({hist_cvar_95*100:.4f}%)")
print(f"99% CVaR: {hist_cvar_99:.6f} ({hist_cvar_99*100:.4f}%)")
print(f"\nInterpretation: On the worst 5% of days, the portfolio loses at least {abs(hist_var_95)*100:.2f}%")

# =============================================================================
# SECTION 3: Variance-Covariance (Parametric) VaR and CVaR
# =============================================================================

print(f"\n{'='*60}")
print("METHOD 2: VARIANCE-COVARIANCE (PARAMETRIC)")
print(f"{'='*60}")

mu = portfolio_returns.mean()
sigma = portfolio_returns.std()

# VaR using normal distribution quantiles
vc_var_95 = mu + stats.norm.ppf(0.05) * sigma
vc_var_99 = mu + stats.norm.ppf(0.01) * sigma

# Parametric CVaR using conditional expectation formula
vc_cvar_95 = mu - sigma * (stats.norm.pdf(stats.norm.ppf(0.05)) / 0.05)
vc_cvar_99 = mu - sigma * (stats.norm.pdf(stats.norm.ppf(0.01)) / 0.01)

print(f"95% VaR: {vc_var_95:.6f} ({vc_var_95*100:.4f}%)")
print(f"99% VaR: {vc_var_99:.6f} ({vc_var_99*100:.4f}%)")
print(f"95% CVaR: {vc_cvar_95:.6f} ({vc_cvar_95*100:.4f}%)")
print(f"99% CVaR: {vc_cvar_99:.6f} ({vc_cvar_99*100:.4f}%)")
print(f"\nNote: Assumes normally distributed returns. Underestimates tail risk.")

# =============================================================================
# SECTION 4: Monte Carlo Simulation VaR and CVaR
# =============================================================================

print(f"\n{'='*60}")
print("METHOD 3: MONTE CARLO SIMULATION")
print(f"{'='*60}")

np.random.seed(42)
n_simulations = 10000

# Simulate returns using observed mean and std
simulated_returns = np.random.normal(mu, sigma, n_simulations)

# VaR and CVaR from simulated distribution
mc_var_95 = np.percentile(simulated_returns, 5)
mc_var_99 = np.percentile(simulated_returns, 1)
mc_cvar_95 = simulated_returns[simulated_returns <= mc_var_95].mean()
mc_cvar_99 = simulated_returns[simulated_returns <= mc_var_99].mean()

print(f"95% VaR: {mc_var_95:.6f} ({mc_var_95*100:.4f}%)")
print(f"99% VaR: {mc_var_99:.6f} ({mc_var_99*100:.4f}%)")
print(f"95% CVaR: {mc_cvar_95:.6f} ({mc_cvar_95*100:.4f}%)")
print(f"99% CVaR: {mc_cvar_99:.6f} ({mc_cvar_99*100:.4f}%)")
print(f"\nSimulations run: {n_simulations:,}")

# =============================================================================
# SECTION 5: Interactive Dash Dashboard
# =============================================================================

# --- Figure 1: VaR Comparison on Return Distribution ---
fig_distribution = go.Figure()

fig_distribution.add_trace(go.Histogram(
    x=portfolio_returns,
    nbinsx=100,
    histnorm="probability density",
    marker_color="rgba(135, 206, 235, 0.7)",
    name="Portfolio Returns",
))

fig_distribution.add_vline(x=hist_var_95, line_width=2, line_color="blue", line_dash="solid",
                           annotation_text=f"Historical: {hist_var_95*100:.2f}%", annotation_position="top left")
fig_distribution.add_vline(x=vc_var_95, line_width=2, line_color="red", line_dash="dash",
                           annotation_text=f"Var-Covar: {vc_var_95*100:.2f}%", annotation_position="top left")
fig_distribution.add_vline(x=mc_var_95, line_width=2, line_color="green", line_dash="dot",
                           annotation_text=f"Monte Carlo: {mc_var_95*100:.2f}%", annotation_position="top left")

fig_distribution.update_layout(
    title="Portfolio VaR Comparison: 95% Confidence Level",
    xaxis_title="Daily Portfolio Return",
    yaxis_title="Density",
    template="plotly_white",
    height=500,
)

# --- Figure 2: Summary Bar Chart ---
methods = ["Historical Simulation", "Variance-Covariance", "Monte Carlo"]

fig_bars = go.Figure()

fig_bars.add_trace(go.Bar(
    name="VaR 95%",
    x=methods,
    y=[abs(hist_var_95)*100, abs(vc_var_95)*100, abs(mc_var_95)*100],
    marker_color="steelblue",
))
fig_bars.add_trace(go.Bar(
    name="CVaR 95%",
    x=methods,
    y=[abs(hist_cvar_95)*100, abs(vc_cvar_95)*100, abs(mc_cvar_95)*100],
    marker_color="coral",
))
fig_bars.add_trace(go.Bar(
    name="VaR 99%",
    x=methods,
    y=[abs(hist_var_99)*100, abs(vc_var_99)*100, abs(mc_var_99)*100],
    marker_color="darkblue",
))
fig_bars.add_trace(go.Bar(
    name="CVaR 99%",
    x=methods,
    y=[abs(hist_cvar_99)*100, abs(vc_cvar_99)*100, abs(mc_cvar_99)*100],
    marker_color="darkred",
))

fig_bars.update_layout(
    title="VaR and CVaR Comparison Across Methods",
    yaxis_title="Loss (%)",
    barmode="group",
    template="plotly_white",
    height=500,
)

# --- Dash App ---
app = Dash(__name__)

app.layout = html.Div(style={"fontFamily": "Arial, sans-serif", "maxWidth": "1400px", "margin": "0 auto", "padding": "20px"}, children=[
    html.H1("Portfolio Risk Analytics Engine", style={"textAlign": "center", "color": "#2c3e50"}),
    html.P("Equal-weight portfolio: AAPL, MSFT, JPM, JNJ, XOM | 5 years daily data",
           style={"textAlign": "center", "color": "#7f8c8d", "fontSize": "16px"}),

    # Charts side by side
    html.Div(style={"display": "flex", "gap": "20px", "flexWrap": "wrap"}, children=[
        html.Div(style={"flex": "1", "minWidth": "600px"}, children=[
            dcc.Graph(figure=fig_distribution),
        ]),
        html.Div(style={"flex": "1", "minWidth": "600px"}, children=[
            dcc.Graph(figure=fig_bars),
        ]),
    ]),

    # Risk Report Table
    html.H2("Risk Report", style={"marginTop": "40px", "color": "#2c3e50"}),
    html.Table(style={"width": "100%", "borderCollapse": "collapse", "marginTop": "10px"}, children=[
        html.Thead(children=[
            html.Tr(children=[
                html.Th("Method", style={"border": "1px solid #ddd", "padding": "12px", "backgroundColor": "#2c3e50", "color": "white", "textAlign": "left"}),
                html.Th("VaR 95%", style={"border": "1px solid #ddd", "padding": "12px", "backgroundColor": "#2c3e50", "color": "white", "textAlign": "right"}),
                html.Th("CVaR 95%", style={"border": "1px solid #ddd", "padding": "12px", "backgroundColor": "#2c3e50", "color": "white", "textAlign": "right"}),
                html.Th("VaR 99%", style={"border": "1px solid #ddd", "padding": "12px", "backgroundColor": "#2c3e50", "color": "white", "textAlign": "right"}),
                html.Th("CVaR 99%", style={"border": "1px solid #ddd", "padding": "12px", "backgroundColor": "#2c3e50", "color": "white", "textAlign": "right"}),
            ])
        ]),
        html.Tbody(children=[
            html.Tr(children=[
                html.Td("Historical Simulation", style={"border": "1px solid #ddd", "padding": "10px"}),
                html.Td(f"{hist_var_95*100:.4f}%", style={"border": "1px solid #ddd", "padding": "10px", "textAlign": "right"}),
                html.Td(f"{hist_cvar_95*100:.4f}%", style={"border": "1px solid #ddd", "padding": "10px", "textAlign": "right"}),
                html.Td(f"{hist_var_99*100:.4f}%", style={"border": "1px solid #ddd", "padding": "10px", "textAlign": "right"}),
                html.Td(f"{hist_cvar_99*100:.4f}%", style={"border": "1px solid #ddd", "padding": "10px", "textAlign": "right"}),
            ]),
            html.Tr(children=[
                html.Td("Variance-Covariance", style={"border": "1px solid #ddd", "padding": "10px"}),
                html.Td(f"{vc_var_95*100:.4f}%", style={"border": "1px solid #ddd", "padding": "10px", "textAlign": "right"}),
                html.Td(f"{vc_cvar_95*100:.4f}%", style={"border": "1px solid #ddd", "padding": "10px", "textAlign": "right"}),
                html.Td(f"{vc_var_99*100:.4f}%", style={"border": "1px solid #ddd", "padding": "10px", "textAlign": "right"}),
                html.Td(f"{vc_cvar_99*100:.4f}%", style={"border": "1px solid #ddd", "padding": "10px", "textAlign": "right"}),
            ]),
            html.Tr(children=[
                html.Td("Monte Carlo", style={"border": "1px solid #ddd", "padding": "10px"}),
                html.Td(f"{mc_var_95*100:.4f}%", style={"border": "1px solid #ddd", "padding": "10px", "textAlign": "right"}),
                html.Td(f"{mc_cvar_95*100:.4f}%", style={"border": "1px solid #ddd", "padding": "10px", "textAlign": "right"}),
                html.Td(f"{mc_var_99*100:.4f}%", style={"border": "1px solid #ddd", "padding": "10px", "textAlign": "right"}),
                html.Td(f"{mc_cvar_99*100:.4f}%", style={"border": "1px solid #ddd", "padding": "10px", "textAlign": "right"}),
            ]),
        ]),
    ]),

    # Key insight
    html.Div(style={"marginTop": "20px", "padding": "15px", "backgroundColor": "#f8f9fa", "borderLeft": "4px solid #3498db", "borderRadius": "4px"}, children=[
        html.P("Key Insight: Historical Simulation captures actual tail behavior from observed data. "
               "Variance-Covariance underestimates risk due to the normality assumption. "
               "Monte Carlo with normal inputs converges to Variance-Covariance as simulations increase, "
               "but its real power emerges when modeling non-normal or complex distributions.",
               style={"margin": "0", "color": "#2c3e50"}),
    ]),
])

if __name__ == "__main__":
    print(f"\n{'='*60}")
    print("DASHBOARD LIVE at http://127.0.0.1:8050")
    print(f"{'='*60}")
    app.run(debug=True)

Secret mission

Stress Testing with Historical Crises

Your VaR engine works great under normal market conditions. But what happens when markets crash? In this extension, you'll build a stress testing framework that shocks your portfolio with real historical crises and proves how standard VaR breaks down when it matters most.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally on your machine, so there are no ongoing costs or cloud resources to worry about.

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • The risk-engine project folder on your Desktop (contains risk_engine.py, the venv virtual environment, and all installed packages).
  • The running Dash development server at http://127.0.0.1:8050.

Keep everything running

No action needed. Choose this if you are still actively building or experimenting with the dashboard.

  • Your Dash server continues running at http://127.0.0.1:8050 as long as the terminal process is active.
  • Your virtual environment stays activated in the terminal session.
  • All project files remain in the risk-engine folder on your Desktop.

Pause - I'll come back to this later

Shut down the running server to free up your terminal. Your files and virtual environment stay on disk for next time.

  • In the terminal where the Dash server is running, press Ctrl+C to stop the server.
  • Deactivate the virtual environment by running:
deactivate

When you come back, reactivate the virtual environment and run the script again to resume.

Delete - I don't want to use this again

Remove the entire project folder and all its contents. This deletes the script, virtual environment, and all installed packages permanently.

  • In the terminal where the Dash server is running, press Ctrl+C to stop the server.
  • Deactivate the virtual environment by running:
deactivate
  • Navigate to your Desktop and delete the project folder by running:
cd ~/Desktop
rm -rf risk-engine
  • On Windows, use this command instead:
cd ~\Desktop
rmdir /s /q risk-engine
  • Confirm the folder is gone by running ls on your Desktop. You should no longer see risk-engine in the output.

That's a Wrap!

That's a Wrap!

Nice work! You just built a portfolio risk analytics engine that downloads real market data, computes Value-at-Risk and CVaR using three industry-standard methodologies, and presents everything in an interactive Dash dashboard.

You've learned how to:

  • Built a portfolio risk engine that fetches real market data with yfinance and computes VaR/CVaR using Historical Simulation, Variance-Covariance, and Monte Carlo methodologies.
  • Created an interactive Dash dashboard with side-by-side Plotly visualizations proving how each method produces meaningfully different risk estimates from identical portfolio data.
  • Generated a styled risk report table comparing all three methods at 95% and 99% confidence levels, rendered live in the browser.
  • Secret Mission: Built a stress testing framework demonstrating how standard VaR underestimates tail risk during historical crises (2008 GFC, 2020 COVID), with interactive visualizations added to the dashboard.

Ready to quiz yourself?