Probability Theory Fundamentals › Practical Implementation: Simulating Returns and Risk

Michael BrenndoerferOctober 19, 20258 min read
Quantitative FinanceAll chapters
Part 1 of 15Mathematical Foundations
Chapter 2Probability Theory Fundamentals › Practical Implementation: Simulating Returns and Risk
Reading section 11 of 15 · Practical Implementation: Simulating Returns and Risk · 8 minSection 11 of 15 · 8 min

Book contents

Section 11 of 15 · 8 min read

Probability Theory Fundamentals › Practical Implementation: Simulating Returns and RiskOne section · Sections in this chapter11 / 15 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

This section brings together the concepts from this chapter by simulating stock returns and computing risk measures. This exercise demonstrates how probability distributions translate to financial risk analysis.

Simulating Daily Returns

We'll simulate iid normal daily simple returns as a local approximation and compute key statistics. Because a normal law is unbounded, the model assigns positive probability to returns at or below -100%; the seeded draw is validated before applying log⁡(1+r)\log(1+r).

In[24]:
Code
import numpy as np

np.random.seed(42)

# Parameters for daily returns
annual_return = 0.10  # Annualized arithmetic mean of daily simple returns
annual_volatility = 0.20  # 20% annual volatility

# Convert under iid daily arithmetic-mean and zero-autocovariance scaling
trading_days = 252
daily_return = annual_return / trading_days
daily_volatility = annual_volatility / np.sqrt(trading_days)

# Simulate 5 years of daily returns
n_years = 5
n_days = trading_days * n_years
returns = np.random.normal(daily_return, daily_volatility, n_days)
assert np.all(returns > -1), "simple returns must exceed -100% before log1p"
Out[25]:
Console
Simulation Parameters:
  Annualized arithmetic daily mean: 10.0%
  Annual volatility: 20.0%
  Daily expected return: 0.0397%
  Daily volatility: 1.2599%

Simulated 1260 trading days (5 years)

Computing Sample Statistics

Now we calculate the sample mean, standard deviation, skewness, and kurtosis.

In[26]:
Code
from scipy import stats

# Sample statistics
sample_mean = np.mean(returns)
sample_std = np.std(returns, ddof=1)  # ddof=1 for sample std
sample_skew = stats.skew(returns)
sample_kurtosis = stats.kurtosis(returns)  # Excess kurtosis

# Annualize sample statistics
annualized_mean = sample_mean * trading_days
annualized_std = sample_std * np.sqrt(trading_days)
Out[27]:
Console
Sample Statistics (Daily):
  Mean: 0.000877
  Standard Deviation: 0.012467
  Skewness: 0.0792
  Excess Kurtosis: 0.0151

Annualized:
  Mean Return: 22.10% (target: 10.00%)
  Volatility: 19.79% (target: 20.00%)

For this seed, annualized volatility is about 19.79%, close to the 20% input, while the annualized sample mean is about 22.10%, well above the 10% input. The gap illustrates how noisy a mean estimate can remain with 1,260 daily observations. Near-zero sample skewness and excess kurtosis are consistent with normal draws, but those two statistics alone do not confirm normality; here the generating call establishes the normal model.

Visualizing the Return Distribution

Out[28]:
Visualization
Histogram of returns with overlaid normal distribution curve.
Histogram of simulated daily returns with a fitted normal density. The visual agreement is expected because the sample was generated from a normal model; it is a consistency check, not an independent validation.

Computing Value at Risk

Value at Risk (VaR) is a quantile threshold for loss over a specified horizon. Under a continuous calibrated distribution, a 95% VaR has 5% probability beyond the threshold: 5% is one period in twenty in probability terms, not a fixed schedule of exceedances. With discrete or empirical distributions, ties and the chosen quantile convention can change the exact fraction. VaR is neither the worst possible loss nor the expected loss beyond that threshold.

The code below reports signed lower-tail return cutoffs. If loss is defined as L=−RL=-R, a loss VaR quantile can be positive or negative; it is positive in this example because the return cutoffs are negative. For a continuous return distribution with a strictly increasing CDF, such as the normal model here, the loss quantile at confidence cc equals the negative of the return quantile at 1−c1-c. The empirical calculation uses NumPy's linear-interpolation percentile convention, which also has this reflection property. Other quantile conventions, atoms, or gaps in a distribution's support require checking the endpoints explicitly.

In[29]:
Code
# Signed lower-tail return cutoffs at 95% and 99% confidence levels
var_95 = np.percentile(returns, 5, method="linear")  # loss VaR = -var_95
var_99 = np.percentile(returns, 1, method="linear")  # loss VaR = -var_99

# Parametric VaR using normal distribution
var_95_parametric = sample_mean + stats.norm.ppf(0.05) * sample_std
var_99_parametric = sample_mean + stats.norm.ppf(0.01) * sample_std
Out[30]:
Console
Daily signed return cutoffs (loss VaR is their negative here):

95% confidence (5th-percentile return cutoff):
  Empirical sample percentile: -1.9143%
  Fitted normal:               -1.9629%

99% confidence (1st-percentile return cutoff):
  Empirical sample percentile: -2.6514%
  Fitted normal:               -2.8125%

Estimated 5th-percentile daily return: -1.91%
This is the empirical sample percentile; the fitted-normal threshold is listed separately above.

For this seeded sample, the empirical 5th- and 1st-percentile daily return cutoffs are -1.9143% and -2.6514%. They correspond to positive loss VaR estimates of 1.9143% and 2.6514%. The fitted-normal return cutoffs are -1.9629% and -2.8125%, corresponding to positive loss VaR estimates of 1.9629% and 2.8125%.

In the signed-return presentation used here, historical VaR directly uses a lower-tail sample percentile, while parametric VaR calculates the corresponding percentile from a fitted distribution, normal in this example. Under the stated conventions, negating either cutoff gives the loss VaR, positive for these particular cutoffs. When the model fits and the sample is sufficiently informative, the estimates may be similar. In a finite real-data sample, historical VaR can be higher or lower than normal parametric VaR; its main distinction is that it does not impose a normal shape on the observations in the estimation window.

Out[31]:
Visualization
Histogram of returns with vertical lines marking fitted-normal 5% and 1% lower-tail return cutoffs and shaded tail regions.
Fitted-normal lower-tail return cutoffs. The plotted 5% and 1% cutoffs are -1.9629% and -2.8125%, corresponding to positive loss VaR estimates of 1.9629% and 2.8125%. The empirical sample cutoffs, which are not plotted as lines, are -1.9143% and -2.6514%. Shading shows the visible portion of the fitted-normal tails.

Converting Price Path from Returns

Starting from an initial price, we can construct a price path using cumulative returns.

Out[32]:
Visualization
Line chart showing one simulated stock price path starting at 100 over 5 years of trading days.
Simulated stock price path over 5 years implied by compounding the simulated daily returns.
Out[33]:
Console
Initial price: $100.00
Final price: $273.74
Total return: 173.74%
Annualized return: 22.31%

The simulated price path is one realization under the model. The 10% input is an annualized arithmetic mean of daily simple returns, whereas 22.31% is the geometric CAGR of this realized path, so equality is not a model target. Sampling variation explains why this seed's annualized sample arithmetic mean is 22.10% rather than 10%. The separate move from that sample mean to the 22.31% CAGR comes from geometrically compounding the realized daily sequence; it is not another estimate of the arithmetic-mean input.