Skip to content
All library documents

Checking Short-Horizon Volatility in Lognormal Monte Carlo Paths

Article Quant Q&A · Author: Bartosz Wozniak

Summary

The document examines why simulated returns from a geometric Brownian motion setup may not match the input annualized volatility when paths are short. Its code generates daily lognormal price changes using a drift adjustment and volatility scaled by the square root of the time step, then compares compounded path returns and volatility estimates with the model parameters. The response explains that terminal price variance follows the lognormal distribution formula, which depends on both drift and volatility, and demonstrates comparing theoretical and simulated terminal-price means and variances. This reframes the check around the distribution of terminal prices rather than treating the average of per-path sample standard deviations as an exact match to the annual input. The example reports close agreement using a large simulation, but does not fully resolve every issue in the original volatility calculation or quantify finite-sample error. A second reply points to a possible mismatch in the function arguments, underscoring that parameter wiring should also be checked.

Key ideas

  • Lognormal price paths have a terminal variance determined by both drift and volatility.
  • Annualized model volatility should not be expected to equal every short path's sample volatility exactly.
  • Compare simulated terminal-price moments with the corresponding theoretical moments to check a Monte Carlo model.
  • Verify that function arguments pass the intended drift and volatility values.

Tags

Full text
# Volatility in simulated paths different to monte carlo parameters


# Volatility in simulated paths different to monte carlo parameters












I am trying to convince myself that I have set up my monte carlo simulation correctly by looking at the results and trying to get them to agree with the model parameters. Please help me understand what am I doing wrong.

I use the following code to generate each simulated price path.

```
def create_paths_changes_normal(mu, sigma, path_len, sample_size):
    dt = 1 / BUSINESS_DAYS_IN_YEAR

    r_daily = (1 + mu) ** dt - 1
    iv_daily = sigma * np.sqrt(dt)

    drift = (r_daily - 0.5 * iv_daily ** 2)
    diffusion = iv_daily

    # Generate random daily returns
    return np.exp(np.random.normal(loc=drift, scale=diffusion, size=[sample_size, path_len]))
```

Afterwards, I run the following code

```
def check_statistics(mu, sigma, days, sample_size=100000):
    path_changes = create_paths_changes_normal(mu, sigma, days, sample_size)

    returns = np.prod(path_changes, axis=1)
    annal_returns = returns.mean() ** (BUSINESS_DAYS_IN_YEAR / days) - 1
    avg_return = annal_returns.mean()

    daily_returns = path_changes - 1.0
    vol = daily_returns.std(axis=1).mean() * np.sqrt(BUSINESS_DAYS_IN_YEAR)

    # assert mu == avg_return  # This is ok
    # assert sigma == vol  # This fails for short paths e.g. days=5.
```

For paths of length 5 (days) the average returns agree, but the vol does not. For longer paths both agree.

Please help me understand if I have made a wrong assumption somewhere and whether I am doing this correctly.

## Answer by THATS MY QUANT MY QUANTITATIVE (score 1)

https://quant.stackexchange.com/a/79943

The issue is with how you are calculating your mean and volatility. According to Wikipedia, $$ \begin{align} \mathbb{E}(S_t) &= S_0 e^{\mu t} \\ \text{Var}(S_t) &= S_0^2 e^{2\mu t} \left( e^{\sigma^2 t} - 1 \right) \end{align} $$

Then adapting your code:

```
import numpy as np

def create_paths_changes_normal(mu, sigma, BUSINESS_DAYS_IN_YEAR, path_len, sample_size):
    dt = 1 / BUSINESS_DAYS_IN_YEAR

    r_daily = (1 + mu) ** dt - 1
    iv_daily = sigma * np.sqrt(dt)

    drift = (r_daily - 0.5 * iv_daily ** 2)
    diffusion = iv_daily

    # Generate random daily returns
    return np.exp(np.random.normal(loc=drift, scale=diffusion, size=[sample_size, path_len]))

mu = 0.05
sigma = 0.2
days = 5
sample_size = 10000000
BUSINESS_DAYS_IN_YEAR = 252.

path_changes = create_paths_changes_normal(mu, sigma, BUSINESS_DAYS_IN_YEAR, days, sample_size)
returns = np.prod(path_changes, axis=1)

S_0 = 100. 
t = days / BUSINESS_DAYS_IN_YEAR

S_mc = S_0 * returns 

EV_S_theo = S_0 *np.exp(mu * t)   # 100.09925557498198
EV_S_MC = S_mc.mean() # ~100.09840608411352

VAR_S_theo = (S_0**2) * (np.exp(2 * mu * t)) * (np.exp(t * sigma**2) - 1) # 7.955427106400656
VAR_S_MC = S_mc.var() # ~7.955427106400656
```

## Answer by Mahavir Bhattacharya (score 0)

https://quant.stackexchange.com/a/79840

In the second code snippet, in this line: path_changes = create_paths_changes_normal(mu, sigma, days, sample_size)

you are changing the arguments from what you had declared in the previous code:

create_paths_changes_normal(risk_free_rate_difference, implied_vol, path_len, sample_size)

Essentially, the first argument is getting changed.

Shown in full with attribution under the source's licence. Licence: CC BY-SA 4.0 (Stack Exchange)

This summary was written by Stratmill's research agent from the original; it is not a copy of the source.