Value at Risk (VaR) Analysis in Python

Compute Value at Risk (VaR), Conditional VaR (CVaR), and maximum drawdown for a stock portfolio using historical simulation.

This page shows an AI data analysis example. A workflow is a list of prompts (instructions) used to complete one analysis task step by step. We send the same prompt list to multiple AI models, then compare their notebook outputs and scores.

What this AI workflow does

This AI Data Analyst workflow downloads 3 years of SPY, QQQ, and GLD price data, computes daily returns, and builds an equal-weight portfolio return series. It estimates 95% and 99% Value at Risk (VaR) using historical simulation and computes 95% Conditional VaR (CVaR/Expected Shortfall). It visualizes the return distribution with VaR and CVaR thresholds and reports maximum drawdown for the portfolio.

Who this example is for

This is for analysts and students who need a reproducible Python notebook for portfolio risk metrics from market data. It helps validate downside risk estimates and communicate them with standard plots and summary statistics.

Expected analysis outcomes

These are the results the AI workflow is expected to generate.

  • Daily equal-weight portfolio return series
  • Historical-simulation VaR estimates at 95% and 99%
  • CVaR (Expected Shortfall) at 95%
  • Maximum drawdown statistic
  • Histogram of returns with VaR/CVaR threshold lines

Tools and libraries used

Main Python packages and tooling used to run this AI data analysis task.

  • yfinance
  • pandas
  • numpy
  • matplotlib
  • scipy

Prompt sequence

This is the exact list of prompts used in this workflow. The same prompt sequence is sent to each model so outputs and scores can be compared fairly.

  1. 1download 3 years of SPY, QQQ, GLD prices and compute daily portfolio returns assuming equal weights with yfinance
  2. 2calculate 95% and 99% Value at Risk using historical simulation
  3. 3calculate Conditional VaR (Expected Shortfall) at 95%
  4. 4plot the return distribution with VaR and CVaR thresholds marked

Model Comparison

We compared several LLM models on the same analysis task. The table below shows their scores side by side. You can also open each model run to check the full conversation and notebook results.

Model SourceScoreTaskExecutionOutputReasoningReliabilityRun
glm-5.110/102/22/23/32/21/1Open glm-5.1 conversation
gpt-oss:120b10/102/22/23/32/21/1Open gpt-oss:120b conversation
qwen3-coder-next10/102/22/23/32/21/1Open qwen3-coder-next conversation
gemma4:31b9/102/22/22/32/21/1Open gemma4:31b conversation
gpt-5.49/102/22/22/32/21/1Open gpt-5.4 conversation
qwen3.5:397b2/101/20/20/31/20/1Open qwen3.5:397b conversation
How scoring works

Each run is graded on five dimensions:

  • Task Completion (0-2): how fully the workflow answered the requested analysis task and completed required steps.
  • Execution Correctness (0-2): whether generated code is correct, consistent, and likely runnable without major fixes.
  • Output Quality (0-3): how well tables, charts, and results match the expected analytical outcomes.
  • Reasoning Quality (0-2): clarity and correctness of interpretation, explanation, and conclusions.
  • Reliability (0-1): robustness of the run, including low hallucination risk and stable behavior.

The final score is the sum of these dimensions, so total score = 0-10. Higher values indicate more complete, correct, and reliable notebook results for this workflow.

AI Analysis with glm-5.1

This run uses AI with a step-by-step list of prompts to perform the analysis task Value at Risk (VaR) Analysis in Python. Below, you can review the full conversation generated with glm-5.1. After the conversation, we show detailed score cards for this run.

download 3 years of SPY, QQQ, GLD prices and compute daily portfolio returns assuming equal weights with yfinance
Thinking

The user wants to download 3 years of daily prices for SPY, QQQ, GLD using yfinance, then compute daily portfolio returns assuming equal weights (1/3 each).

I'll download the last 3 years of adjusted close prices for SPY, QQQ, and GLD, then compute equal-weighted daily portfolio returns.

Compute equal-weight portfolio returns
import yfinance as yf
import pandas as pd

# Download 3 years of daily data
tickers = ["SPY", "QQQ", "GLD"]
data = yf.download(tickers, period="3y", auto_adjust=True)["Close"]

# Compute daily returns per asset
daily_returns = data.pct_change().dropna()

# Equal-weight portfolio return: average of the three
portfolio_returns = daily_returns.mean(axis=1)
portfolio_returns.name = "Portfolio"

# Quick summary
print(f"Date range: {daily_returns.index.min().date()} to {daily_returns.index.max().date()}")
print(f"Trading days: {len(daily_returns)}")

display(daily_returns.head())
display(portfolio_returns.head())
[*********************100%***********************]  3 of 3 completed
Date range: 2023-04-12 to 2026-04-10
Trading days: 752
Ticker GLD QQQ SPY
Date
2023-04-12 0.004885 -0.008834 -0.004076
2023-04-13 0.013516 0.019582 0.013283
2023-04-14 -0.017710 -0.001880 -0.002443
2023-04-17 -0.004454 0.000848 0.003588
2023-04-18 0.003881 0.000063 0.000652
Date
2023-04-12   -0.002675
2023-04-13    0.015460
2023-04-14   -0.007344
2023-04-17   -0.000006
2023-04-18    0.001532
Name: Portfolio, dtype: float64
Thinking

The user's original goal was to download 3 years of SPY, QQQ, GLD prices and compute daily portfolio returns assuming equal weights. The notebook output shows this has been successfully completed:

  1. Data downloaded for SPY, QQQ, GLD over 752 trading days from 2023-04-12 to 2026-04-10
  2. Daily returns computed for each asset
  3. Equal-weight portfolio returns computed as the mean of the three

The output is sufficient to answer the original goal. I should provide a final answer with insights from the observed outputs.

The equal-weight portfolio returns are now computed. Here's a summary of what we have:

  • Assets: SPY, QQQ, GLD — 752 trading days from 2023-04-12 to 2026-04-10
  • Portfolio construction: Each asset gets 1/3 weight; daily portfolio return is the simple average of the three daily returns
  • Early pattern: The first few days show the diversification effect — for example, on 2023-04-12, SPY was down ~0.41%, QQQ down ~0.88%, but GLD up ~0.49%, pulling the portfolio return to only −0.27%. On 2023-04-14, GLD dropped ~1.77% while QQQ and SPY were nearly flat, softening the blow to −0.73% at the portfolio level.

The portfolio_returns Series (752 values) and the daily_returns DataFrame (752 × 3) are available for any further analysis like cumulative return charts, risk metrics, or performance attribution.

calculate 95% and 99% Value at Risk using historical simulation
Thinking

The user wants to calculate 95% and 99% Value at Risk (VaR) using historical simulation. This is straightforward - sort the portfolio returns and take the appropriate percentiles. The 5th percentile for 95% VaR and the 1st percentile for 99% VaR (since VaR is about the worst losses).

Let me compute this from the portfolio_returns Series already in memory.

I'll compute the 95% and 99% historical VaR directly from the portfolio returns series — this simply takes the relevant percentiles of the empirical return distribution.

Compute historical VaR and CVaR
import numpy as np

# Historical VaR: negative of the relevant percentile (so VaR is expressed as a positive loss)
var_95 = -np.percentile(portfolio_returns, 5)
var_99 = -np.percentile(portfolio_returns, 1)

# Also compute expected shortfall (CVaR) for context
cvar_95 = -portfolio_returns[portfolio_returns <= np.percentile(portfolio_returns, 5)].mean()
cvar_99 = -portfolio_returns[portfolio_returns <= np.percentile(portfolio_returns, 1)].mean()

print(f"95% Historical VaR (1-day): {var_95:.4%}")
print(f"99% Historical VaR (1-day): {var_99:.4%}")
print(f"95% Historical CVaR (1-day): {cvar_95:.4%}")
print(f"99% Historical CVaR (1-day): {cvar_99:.4%}")

# Contextualize: for a 100,000 USD portfolio
print(f"\nFor a 100,000 USD portfolio:")
print(f"  95% VaR: {var_95 * 100000:,.2f} USD")
print(f"  99% VaR: {var_99 * 100000:,.2f} USD")
print(f"  95% CVaR: {cvar_95 * 100000:,.2f} USD")
print(f"  99% CVaR: {cvar_99 * 100000:,.2f} USD")
95% Historical VaR (1-day): 1.2858%
99% Historical VaR (1-day): 2.2490%
95% Historical CVaR (1-day): 2.0016%
99% Historical CVaR (1-day): 3.1305%

For a 100,000 USD portfolio:
  95% VaR: 1,285.84 USD
  99% VaR: 2,249.03 USD
  95% CVaR: 2,001.59 USD
  99% CVaR: 3,130.48 USD
Thinking

The user asked to calculate 95% and 99% Value at Risk using historical simulation. The outputs already show these values clearly. Let me provide the final answer.

The historical simulation VaR results are complete. Here's what they tell us:

95% Historical VaR (1-day): 1.2858% — On any given day, there's a 5% chance the portfolio loses more than 1.29% of its value. For a 100,000 USD portfolio, that's a potential loss exceeding 1,285.84 USD.

99% Historical VaR (1-day): 2.2490% — There's only a 1% chance of a daily loss beyond 2.25%, or 2,249.03 USD on a 100,000 USD portfolio.

The CVaR (Expected Shortfall) figures add important context — when losses do exceed VaR, they tend to be significantly worse:

  • 95% CVaR: 2.0016% (2,001.59 USD) — conditional average loss in the worst 5% of days
  • 99% CVaR: 3.1305% (3,130.48 USD) — conditional average loss in the worst 1% of days

The gap between VaR and CVaR is notable: the 99% CVaR (3.13%) is 39% larger than the 99% VaR (2.25%), indicating fat tails — when extreme losses occur, they tend to be substantially worse than the VaR threshold itself. This is consistent with the equity-heavy composition (2/3 in SPY/QQQ) combined with the gold hedge (1/3 GLD), which smooths moderate days but doesn't fully protect in severe selloffs.

calculate Conditional VaR (Expected Shortfall) at 95%
Thinking

The user wants to calculate Conditional VaR (Expected Shortfall) at 95%. Looking at the notebook state, this has already been computed in cell-2 as cvar_95 = 2.0016%. The results are already available.

However, the user might want a more dedicated/explicit calculation with visualization or a deeper dive. Let me provide a focused calculation with a clear visualization showing the CVaR in context.

Since it's already computed, I could either just point to the existing result, or provide a richer visualization. I think a visualization would be helpful - showing the return distribution with the VaR and CVaR highlighted.

The 95% CVaR was already computed: 2.0016% (2,001.59 USD on a 100,000 USD portfolio). Let me visualize it in context of the return distribution so the distinction between VaR and CVaR is clear.

Plot VaR and CVaR
import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots(figsize=(10, 5))

# Plot histogram of portfolio returns
ax.hist(portfolio_returns, bins=80, density=True, alpha=0.7, color="steelblue", edgecolor="white", linewidth=0.3)

# Mark 95% VaR and CVaR
var_threshold = np.percentile(portfolio_returns, 5)
cvar_val = -portfolio_returns[portfolio_returns <= var_threshold].mean()

ax.axvline(-var_95, color="red", linestyle="--", linewidth=1.5, label=f"95% VaR = {var_95:.2%}")
ax.axvline(-cvar_val, color="darkred", linestyle="-", linewidth=1.5, label=f"95% CVaR = {cvar_95:.2%}")

# Shade the tail beyond VaR
x_range = np.linspace(portfolio_returns.min(), -var_95, 200)
ax.axvspan(portfolio_returns.min(), -var_95, alpha=0.18, color="red", label="Tail (worst 5%)")

ax.set_title("Portfolio Daily Returns — 95% VaR vs CVaR (Historical Simulation)", fontsize=13)
ax.set_xlabel("Daily Return")
ax.set_ylabel("Density")
ax.legend(fontsize=10)
plt.tight_layout()
plt.show()

This chart makes the key distinction clear: VaR tells you the threshold (1.29%) below which the worst 5% of days fall, while CVaR (2.00%) tells you the average loss in that worst 5% — a much more meaningful risk measure because it accounts for the severity of tail losses, not just their boundary.

Thinking

The user's original goal is to calculate Conditional VaR (Expected Shortfall) at 95%. Looking at the notebook state, this has already been computed in cell-2:

  • 95% Historical CVaR (1-day): 2.0016%
  • 95% CVaR for a 100,000 USD portfolio: 2,001.59 USD

The results are clearly available in the output. The visualization in cell-3 also shows this. The goal is fully answered by the existing outputs. I should take Path A.

The 95% Conditional VaR (Expected Shortfall) has already been calculated from the historical simulation:

95% CVaR (1-day): 2.0016%

For a 100,000 USD portfolio, this means the average loss on the worst 5% of trading days is 2,001.59 USD. Compare this to the 95% VaR of 1.2858% (1,285.84 USD) — the CVaR is 56% larger, confirming that when losses breach the VaR threshold, they tend to be substantially worse than VaR alone suggests. This is why CVaR is the preferred risk measure for tail risk: it captures not just the boundary of the worst 5% of days, but the average severity within that tail.

plot the return distribution with VaR and CVaR thresholds marked
Thinking

The user wants to plot the return distribution with VaR and CVaR thresholds marked. Looking at the notebook state, cell-3 already does exactly this - it creates a histogram with VaR and CVaR lines and a shaded tail region. The output shows the figure was produced successfully.

Since the plot has already been generated in cell-3, I should point out that it's already available and perhaps offer an enhanced version or confirm it meets their needs. But actually, the user is asking me to plot it, so maybe they want a fresh/refined version or didn't notice the existing one.

Let me provide a clean, refined version of the plot since they're explicitly asking for it. I'll make it slightly more polished.

The notebook already has a similar plot in cell-3, but let me produce a refined version with both 95% and 99% thresholds annotated for completeness.

Plot VaR and CVaR
import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots(figsize=(10, 5))

ax.hist(portfolio_returns, bins=80, density=True, alpha=0.7, color="steelblue", edgecolor="white", linewidth=0.3)

# 95% thresholds
ax.axvline(-var_95, color="orangered", linestyle="--", linewidth=1.5, label=f"95% VaR = {var_95:.2%}")
ax.axvline(-cvar_95, color="darkred", linestyle="-", linewidth=1.5, label=f"95% CVaR = {cvar_95:.2%}")
ax.axvspan(portfolio_returns.min(), -var_95, alpha=0.12, color="orangered")

# 99% thresholds
ax.axvline(-var_99, color="navy", linestyle="--", linewidth=1.5, label=f"99% VaR = {var_99:.2%}")
ax.axvline(-cvar_99, color="midnightblue", linestyle="-", linewidth=1.5, label=f"99% CVaR = {cvar_99:.2%}")
ax.axvspan(portfolio_returns.min(), -var_99, alpha=0.12, color="navy")

ax.set_title("Equal-Weight Portfolio Daily Returns — VaR & CVaR Thresholds", fontsize=13)
ax.set_xlabel("Daily Return")
ax.set_ylabel("Density")
ax.legend(fontsize=9, loc="upper right")
plt.tight_layout()
plt.show()

This adds the 99% VaR and CVaR thresholds alongside the 95% ones, with distinct colors so you can see how the tail risk escalates — the gap between VaR and CVaR widens at the 99% level (1.29% → 2.00% at 95%; 2.25% → 3.13% at 99%), reflecting fatter tails in the worst outcomes.

Thinking

The user asked to "plot the return distribution with VaR and CVaR thresholds marked". Looking at the notebook state, there are already two plots that accomplish this:

  1. Cell-3: A histogram with 95% VaR and CVaR thresholds marked, with the tail shaded
  2. Cell-4: An expanded version with both 95% and 99% VaR and CVaR thresholds

Both plots have been executed successfully and show the return distribution with VaR and CVaR thresholds clearly marked. The numerical values are also available from cell-2's output.

The goal is fulfilled - the return distribution has been plotted with VaR and CVaR thresholds marked. This is a visualization task and the charts are already present in the outputs.

Path A is appropriate here.

The notebook already has the requested plot. Cell-3 produced a clean histogram of the daily return distribution with the 95% VaR (1.29%) and 95% CVaR (2.00%) lines marked and the worst 5% tail shaded in red. Cell-4 extended that to also include the 99% VaR (2.25%) and 99% CVaR (3.13%) with navy shading for the extreme tail.

The key takeaway from the visuals: the distance between each VaR and its corresponding CVaR shows how much worse losses get once you're in the tail. At 95%, the CVaR is 56% larger than the VaR (2.00% vs 1.29%). At 99%, that gap widens — CVaR is 39% larger than VaR (3.13% vs 2.25%). For a 100,000 USD portfolio, this means that on the worst 5% of days you expect to lose 2,001.59 USD on average, and on the worst 1% of days you expect to lose 3,130.48 USD on average.

Score cards for glm-5.1 for Value at Risk (VaR) Analysis in Python

Total score: 10/10

  • Task Completion: 2/2

    Excellent

    The workflow completed the main requested steps: downloaded SPY/QQQ/GLD data, computed equal-weight portfolio returns, calculated 95% and 99% VaR, computed CVaR, and produced a histogram with thresholds marked.

  • Execution Correctness: 2/2

    Excellent

    The code shown is syntactically coherent and the outputs indicate it ran successfully, including data download, risk metrics, and plots.

  • Output Quality: 3/3

    Excellent

    Outputs match the expected outcomes semantically: portfolio return series is shown, VaR/CVaR values are printed, and the distribution plot with threshold lines is generated.

  • Reasoning Quality: 2/2

    Excellent

    The explanations correctly describe historical simulation, percentile-based VaR, and CVaR as expected shortfall, with appropriate interpretation of tail risk.

  • Reliability: 1/1

    Excellent

    The workflow is reasonably consistent and grounded in the computed outputs, with no major hallucinations or unsupported claims.

Try MLJAR Studio

Run the same type of AI-powered data analysis on your own datasets with conversational notebooks in MLJAR Studio.

Explore More AI Analysis Examples

Discover additional workflows across categories. Each example includes prompts, conversation outputs, and model-level scoring so you can compare approaches and results.