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 gemma4:31b

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 gemma4:31b. 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 historical price data for three tickers (SPY, QQQ, GLD) and compute the daily returns of an equally weighted portfolio.

Plan:

  1. Download data using yfinance.
  2. Calculate daily percentage changes for each asset.
  3. Compute the equally weighted portfolio return (mean of the asset returns).

Constraint check:

  • Work iteratively.
  • One execution step at a time.
  • One Python code block per response.
  • Use yfinance.
  • First step should be loading and inspecting.

Step 1: Download the data and inspect it.

I'll start by downloading the last 3 years of Adjusted Close prices for SPY, QQQ, and GLD using yfinance and then inspect the data to make sure everything is loaded correctly.

Download ticker data
import yfinance as yf
import pandas as pd

# Download 3 years of data with auto_adjust=True to get adjusted prices in 'Close'
tickers = ['SPY', 'QQQ', 'GLD']
data = yf.download(tickers, period='3y', auto_adjust=True)['Close']

# Inspect the data
display(data.head())
data.shape
[*********************100%***********************]  3 of 3 completed
Ticker GLD QQQ SPY
Date
2023-04-11 186.279999 310.325409 394.276001
2023-04-12 187.190002 307.584137 392.668915
2023-04-13 189.720001 313.607269 397.884644
2023-04-14 186.360001 313.017670 396.912750
2023-04-17 185.529999 313.282990 398.336945
(753, 3)
Thinking

The user wants to download 3 years of SPY, QQQ, and GLD prices and compute daily portfolio returns assuming equal weights.

*   Prices for SPY, QQQ, and GLD have been downloaded.
*   The data contains 753 rows and 3 columns.
*   The prices for the first few dates (April 2023) are visible.
*   The goal was to download prices *and* compute daily portfolio returns.
*   Currently, only prices have been downloaded and inspected. Daily portfolio returns have not been calculated.

*   The original goal is partially complete (prices downloaded).
*   Missing part: compute daily portfolio returns with equal weights.
*   Path B is necessary.

*   Calculate daily returns for each asset using `pct_change()`.
*   Compute the equal-weighted portfolio return by taking the mean across columns for each day.
*   Display the result.

The price data for GLD, QQQ, and SPY has been successfully retrieved, providing 753 trading days of history. To complete the goal, I will now calculate the daily percentage returns for each asset and compute the equal-weighted portfolio return.

Calculate portfolio returns
# Calculate daily returns for each ticker
daily_returns = data.pct_change()

# Compute equal-weighted portfolio return (mean of the returns across the 3 assets)
portfolio_returns = daily_returns.mean(axis=1)

# Display the first few rows of the portfolio returns
display(portfolio_returns.head())
Date
2023-04-11         NaN
2023-04-12   -0.002675
2023-04-13    0.015460
2023-04-14   -0.007344
2023-04-17   -0.000006
dtype: float64
Thinking
  • Goal: Download 3 years of SPY, QQQ, GLD prices and compute daily portfolio returns assuming equal weights with yfinance.
    • Current State:

      1. Downloaded 3 years of data for SPY, QQQ, GLD (data DataFrame).
      2. Calculated daily percentage returns for each ticker (daily_returns DataFrame).
      3. Calculated the equal-weighted portfolio daily return (portfolio_returns Series).
      4. Observed the output of the first few rows of portfolio_returns.
    • Prices downloaded? Yes.

    • 3 years period? Yes.

    • Tickers SPY, QQQ, GLD? Yes.

    • Daily portfolio returns computed? Yes.

    • Equal weights assumed? Yes (via .mean(axis=1)).

    • The output shows the first few daily returns of the equal-weighted portfolio.

    • 2023-04-11 is NaN (expected for the first row of pct_change()).

    • Subsequent days show values like -0.002675 (-0.27%) and 0.015460 (1.55%).

    • The goal was to "compute daily portfolio returns". This has been done.

    • All requirements are met.

    • Decision: Path A.

    • Content: Natural final answer based on the observed outputs. No code. Include <!--final answer-->.

The 3-year price data for SPY, QQQ, and GLD was successfully retrieved and processed. By calculating the daily percentage change for each asset and taking the average across the three tickers, the equal-weighted daily portfolio returns were computed. The results show the expected initial NaN value for the first trading day (2023-04-11), followed by the calculated daily returns, such as a gain of 1.55% on 2023-04-13.

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

The user wants to calculate the Value at Risk (VaR) at 95% and 99% confidence levels using the historical simulation method.

*   `portfolio_returns`: A Series containing the daily returns of the equal-weighted portfolio of SPY, QQQ, and GLD.
*   `daily_returns`: A DataFrame containing individual asset returns.

*   Historical simulation VaR is the quantile of the distribution of historical returns.
*   For a 95% VaR, we look for the 5th percentile (since VaR usually represents a loss, the 5th percentile of returns is the threshold where 5% of the worst cases fall below).
*   Formula: $\text{VaR}_{95\%} = \text{quantile}(0.05)$.

*   The `portfolio_returns` series already exists.
*   I need to handle the `NaN` at the beginning (from `pct_change()`).
*   Calculate the 5th percentile (for 95% VaR) and 1st percentile (for 99% VaR).

*   Propose the calculation.
*   Provide the code to compute quantiles from `portfolio_returns`.
*   Display the results.

To calculate the Value at Risk (VaR) using historical simulation, we determine the specific percentile of the historical returns distribution that corresponds to the desired confidence level. For 95% and 99% VaR, we look at the 5th and 1st percentiles of the daily returns, respectively.

Calculate VaR values
# Drop the initial NaN value to ensure accurate quantile calculation
returns_cleaned = portfolio_returns.dropna()

# Calculate VaR at 95% and 99% confidence levels
var_95 = returns_cleaned.quantile(0.05)
var_99 = returns_cleaned.quantile(0.01)

# Create a summary table
var_results = pd.Series({
    '95% VaR': var_95,
    '99% VaR': var_99
})

var_results
95% VaR   -0.012858
99% VaR   -0.022490
dtype: float64
Thinking

The user wants to calculate 95% and 99% Value at Risk (VaR) using historical simulation for a portfolio of SPY, QQQ, and GLD.

Looking at the notebook state:

  • data contains the prices.
  • daily_returns contains the pct changes.
  • portfolio_returns is the equal-weighted mean of these returns.
  • returns_cleaned is the series without NaNs.
  • var_95 is calculated as the 5th percentile: -0.012858.
  • var_99 is calculated as the 1st percentile: -0.022490.
  • var_results is a series containing these two values.

The output from cell-3 explicitly shows: 95% VaR -0.012858 99% VaR -0.022490

The original goal was "calculate 95% and 99% Value at Risk using historical simulation". The results are present and calculated correctly using the historical simulation method (quantile of historical returns).

Path A is appropriate because the outputs are sufficient to answer the original goal.

Insights:

  • The 95% VaR is -1.29% (rounded). This means there is a 5% probability that the portfolio will lose 1.29% or more in a single day.
  • The 99% VaR is -2.25% (rounded). This means there is a 1% probability that the portfolio will lose 2.25% or more in a single day.

Final answer construction: State the values and explain their meaning in the context of the portfolio risk.

Based on the historical simulation of the equal-weighted portfolio (SPY, QQQ, and GLD), the Value at Risk (VaR) is:

  • 95% VaR: -1.29% (approximately -0.012858). This indicates that there is a 5% probability that the portfolio's daily loss will exceed 1.29%.
  • 99% VaR: -2.25% (approximately -0.022490). This indicates that there is a 1% probability that the portfolio's daily loss will exceed 2.25%.

These values provide a quantitative estimate of the potential downside risk based on the actual price movements observed over the last three years.

calculate Conditional VaR (Expected Shortfall) at 95%
Thinking

The user wants to calculate the Conditional Value at Risk (CVaR), also known as Expected Shortfall (ES), at a 95% confidence level. The current notebook state provides returns_cleaned (a Series of portfolio returns) and var_95 (the 95% VaR).

Definition of CVaR at 95%: It is the average of the returns that are less than or equal to the 5th percentile (the 95% VaR).

Plan:

  1. Use the existing returns_cleaned and var_95.
  2. Filter returns_cleaned for all values $\le$ var_95.
  3. Calculate the mean of these filtered returns.

Conditional VaR (CVaR), also known as Expected Shortfall, measures the average loss that occurs when the portfolio return falls below the VaR threshold. For a 95% confidence level, we calculate the mean of all returns that are less than or equal to the 95% VaR.

Calculate 95% CVaR
# Filter returns that are below the 95% VaR threshold
cvar_95_returns = returns_cleaned[returns_cleaned <= var_95]

# Calculate the average of these returns
cvar_95 = cvar_95_returns.mean()

# Display result
pd.Series({'95% CVaR': cvar_95})
95% CVaR   -0.020016
dtype: float64

The Conditional VaR (CVaR) at a 95% confidence level is -0.020016. This means that in the worst 5% of cases, the average daily loss of the portfolio is approximately 2.00%. While the 95% VaR tells us the minimum loss expected in the worst 5% of scenarios, the CVaR provides a more comprehensive view of the risk by quantifying the expected average loss given that the 95% threshold has been breached.

plot the return distribution with VaR and CVaR thresholds marked
Thinking

The user wants to plot the return distribution and mark the VaR (Value at Risk) and CVaR (Conditional VaR/Expected Shortfall) thresholds.

- `returns_cleaned`: Series containing portfolio returns (752 values).
- `var_95`: float (-0.012858).
- `cvar_95`: float (-0.020016).

- Plot the distribution of `returns_cleaned` using a histogram or KDE.
- Add vertical lines for `var_95` and `cvar_95`.
- Use seaborn/matplotlib as per visualization policy.
- Label the axes and add a clear title.

- Use `sns.histplot` with `kde=True` for a smooth distribution.
- Use `plt.axvline` for the thresholds.
- Add a legend.

I'll create a histogram of the portfolio returns and overlay the 95% VaR and CVaR thresholds as vertical lines to visualize where these risk measures sit relative to the distribution.

Plot return distribution
import matplotlib.pyplot as plt
import seaborn as sns

plt.figure(figsize=(10, 6))

# Plot the distribution of returns
sns.histplot(returns_cleaned, kde=True, color='skyblue', label='Portfolio Returns')

# Mark VaR 95%
plt.axvline(var_95, color='orange', linestyle='--', linewidth=2, label=f'95% VaR: {var_95:.4f}')

# Mark CVaR 95%
plt.axvline(cvar_95, color='red', linestyle='-', linewidth=2, label=f'95% CVaR: {cvar_95:.4f}')

plt.title('Portfolio Return Distribution with Risk Thresholds', fontsize=14)
plt.xlabel('Daily Return')
plt.ylabel('Frequency')
plt.legend()
plt.grid(axis='y', alpha=0.3)

plt.show()
Thinking

The user wants to "plot the return distribution with VaR and CVaR thresholds marked". Looking at the notebook state:

  • returns_cleaned contains the portfolio returns.
  • var_95 was calculated as -0.012858.
  • cvar_95 was calculated as -0.020016.
  • Cell 5 contains the code to plot the histogram, the KDE, and the vertical lines for VaR and CVaR.
  • The output of cell 5 is <Figure size 1000x600 with 1 Axes>, meaning the chart was successfully generated.

The original goal was to plot the distribution with thresholds marked. The notebook state shows that:

  1. The returns were calculated and cleaned.
  2. The numeric values for VaR and CVaR were computed and are available in the environment.
  3. A plot was generated using sns.histplot and plt.axvline for both thresholds.

Since the goal was specifically to "plot", and the notebook shows a successful figure generation using the correct calculated values, the output is sufficient.

Insight extraction:

  • The distribution of portfolio returns is visualized as a histogram with a KDE.
  • The 95% Value at Risk (VaR) is marked at -1.29% (approximately), representing the threshold where losses exceed this value only 5% of the time.
  • The 95% Conditional VaR (CVaR) is marked at -2.00%, representing the average loss in the worst 5% of cases.
  • Visually, the CVaR line is further to the left than the VaR line, correctly illustrating that CVaR captures the "tail risk" beyond the VaR threshold.

Decision: Path A.

The portfolio return distribution has been plotted, clearly marking the two risk thresholds. The 95% VaR is positioned at -0.0129, indicating that there is a 5% probability of a daily loss exceeding 1.29%. The 95% CVaR is located further into the left tail at -0.0200, showing that if the 95% VaR threshold is breached, the average expected loss is approximately 2.00%.

Score cards for gemma4:31b for Value at Risk (VaR) Analysis in Python

Total score: 9/10

  • Task Completion: 2/2

    Excellent

    All major requested steps were completed: data download, equal-weighted portfolio returns, VaR at 95% and 99%, CVaR at 95%, and a histogram with thresholds marked.

  • Execution Correctness: 2/2

    Excellent

    The provided code is syntactically coherent and likely runnable. It uses yfinance, computes returns, quantiles, CVaR, and plots the distribution without obvious major errors.

  • Output Quality: 2/3

    Good

    The outputs include the portfolio return series, VaR values, CVaR, and a plot. However, the VaR values are slightly different from the approximate expected outcomes, so this is mostly correct rather than fully matching.

  • Reasoning Quality: 2/2

    Excellent

    The reasoning correctly explains historical simulation VaR/CVaR and the use of quantiles and tail averages. It is clear and consistent with the workflow.

  • Reliability: 1/1

    Excellent

    The workflow is reasonably robust and internally consistent, with no major hallucinations or unsafe steps.

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.