Probability Theory Fundamentals › Practical Implementation: Simulating Returns and Risk
Quantitative FinanceAll chapters 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 .
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"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.
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)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

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 , 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 equals the negative of the return quantile at . 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.
# 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_stdDaily 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.

Converting Price Path from Returns
Starting from an initial price, we can construct a price path using cumulative returns.

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.