Forem

Cover image for Monte Carlo Simulation for Stock Price Forecasting: Historical vs Implied Volatility in Python
Ayrat Murtazin
Ayrat Murtazin

Posted on • Originally published at Medium

Monte Carlo Simulation for Stock Price Forecasting: Historical vs Implied Volatility in Python

1. Introduction

Forecasting financial markets is a sophisticated fusion of quantitative precision and global economic nuance. In this quest, the Monte Carlo simulation stands out as a premier statistical instrument, guiding our understanding of future stock prices.

Named after the famous Monte Carlo Casino in Monaco, this method doesn't bank on luck but is rooted in rigorous probabilistic modelling. Imagine orchestrating thousands of experiments in a controlled environment, with each one unfolding a different story of stock price movement. That's the power of the Monte Carlo simulation.

In this article, our approach is twofold:

  1. We delve into historical and implied volatilities β€” their distinctions, implications, and relevance
  2. We employ the Monte Carlo simulation using both volatility measures to extrapolate potential future paths

Central to our analysis will be the derivation of probabilities. By observing the simulation's multitude of potential outcomes, we'll compute the likelihood of the stock price landing within specific ranges.

πŸ“¬ Get the full Google Colab notebook free at AlgoEdge Insights


2. Volatility Analysis

2.1 Historical Volatility

Historical volatility quantifies the variability of a stock's returns over a past period. It gives us an idea of how much a stock's price deviated from its average value during that timeframe.

To annualise: HV_annual = HV_daily Γ— √252

This formula is key when you want to estimate volatility over a given horizon.

2.2 Implied Volatility

The implied volatility can be extracted from the Black-Scholes option pricing model:

Where:

  • C = call option price
  • Sβ‚€ = current stock price
  • X = strike price
  • t = time to expiration
  • r = risk-free rate
  • q = dividend yield
  • N() = CDF of the standard normal distribution

To extract implied volatility (Οƒ), one would reverse engineer the model using the observed market option price. Many traders prefer sourcing implied volatility directly from platforms like Yahoo Finance or their brokers.

2.3 Historical vs. Implied Volatility

Historical Implied
Perspective Backward-looking Forward-looking
Application Risk assessment from past behavior Options pricing, market sentiment
Determination Statistical β€” from past price data Extracted from market option prices

3. Monte Carlo Simulation for Price Forecasting

The Monte Carlo simulation leverages probability theory to model the evolution of stock prices based on two key components:

3.1 Drift and Volatility

Drift (deterministic component):

drift = (ΞΌ - σ²/2) Γ— Ξ”t
Enter fullscreen mode Exit fullscreen mode

Volatility term (stochastic component):

vol_term = Οƒ Γ— Ξ΅ Γ— βˆšΞ”t
Enter fullscreen mode Exit fullscreen mode

Where Ξ΅ is a random value drawn from a standard normal distribution.

3.2 Stock Price Path Simulation

Starting from Sβ‚€, we compute successive prices using:

S(t) = S(t-1) Γ— exp(drift + vol_term)
Enter fullscreen mode Exit fullscreen mode

Running thousands of simulations gives a spectrum of possible outcomes β€” a probability distribution for future stock prices.


4. Python Implementation

4.1 Setup

import numpy as np
import pandas as pd
import yfinance as yf
import matplotlib.pyplot as plt
from datetime import datetime, timedelta
from scipy.stats import norm
Enter fullscreen mode Exit fullscreen mode

4.2 Download Historical Data

symbol = "ASML.AS"
start_date = "2020-01-01"
end_date = "2023-12-30"
df = yf.download(symbol, start=start_date, end=end_date)
Enter fullscreen mode Exit fullscreen mode

4.3 Compute Daily Returns

log_returns = np.log(df['Close'] / df['Close'].shift(1))
Enter fullscreen mode Exit fullscreen mode

πŸ“¬ Get the full Google Colab notebook free at AlgoEdge Insights


4.4 Simulation Parameters

days_to_forecast = 20
num_simulations = 10000
dt = 1  # 1 trading day

# Price thresholds
threshold1 = 650  # Upper threshold
threshold2 = 540  # Lower threshold

# BSM implied volatility
volatility_bsm = 0.29  # 29% annualized
print("BSM Implied Volatility:", volatility_bsm)
Enter fullscreen mode Exit fullscreen mode

4.5 Run the Simulations

def run_simulation(volatility, dt, annualized=False):
    simulated_prices = np.zeros((days_to_forecast, num_simulations))
    simulated_prices[0] = df['Close'][-1]

    if annualized:
        volatility = volatility / np.sqrt(252)

    for t in range(1, days_to_forecast):
        random_walk = np.random.normal(
            loc=log_returns.mean() * dt,
            scale=volatility * np.sqrt(dt),
            size=num_simulations
        )
        simulated_prices[t] = simulated_prices[t - 1] * np.exp(random_walk)

    return simulated_prices

simulated_prices_historical = run_simulation(log_returns.std(), dt)
simulated_prices_bsm = run_simulation(volatility_bsm, dt, annualized=True)
print("Historical Volatility:", log_returns.std())
Enter fullscreen mode Exit fullscreen mode

4.6 Visualization β€” Confidence Cones

fig, axs = plt.subplots(1, 2, figsize=(30, 7))

for simulated_prices, ax, title in zip(
    [simulated_prices_historical, simulated_prices_bsm],
    axs,
    ['Historical Volatility', 'BSM Volatility']
):
    mean_price_path   = np.mean(simulated_prices, axis=1)
    median_price_path = np.median(simulated_prices, axis=1)
    lower_bound_68    = np.percentile(simulated_prices, 16, axis=1)
    upper_bound_68    = np.percentile(simulated_prices, 84, axis=1)
    lower_bound_95    = np.percentile(simulated_prices, 2.5, axis=1)
    upper_bound_95    = np.percentile(simulated_prices, 97.5, axis=1)

    ax.plot(simulated_prices, color='lightgray', alpha=0.1)
    ax.plot(mean_price_path,   color='black', label='Mean Price Path')
    ax.plot(median_price_path, color='blue',  label='Median Price Path')
    ax.plot(lower_bound_68, color='green', linestyle='--', label='68% CI (Lower)')
    ax.plot(upper_bound_68, color='green', linestyle='--', label='68% CI (Upper)')
    ax.plot(lower_bound_95, color='blue',  linestyle='--', label='95% CI (Lower)')
    ax.plot(upper_bound_95, color='red',   linestyle='--', label='95% CI (Upper)')

    ax.axhline(y=threshold1, color='purple', linestyle='--', alpha=0.25, label='Threshold 1')
    ax.axhline(y=threshold2, color='orange', linestyle='--', alpha=0.25, label='Threshold 2')

    # Probability annotations
    above_prob   = (simulated_prices[-1] > threshold1).sum() / num_simulations
    below_prob   = (simulated_prices[-1] < threshold2).sum() / num_simulations
    between_prob = 1 - above_prob - below_prob

    props = dict(boxstyle='round', facecolor='wheat', alpha=0.5)
    ax.text(0.02, 0.05,
        f'P(>{threshold1:.2f}): {above_prob:.2%}\n'
        f'P(<{threshold2:.2f}): {below_prob:.2%}\n'
        f'P({threshold2:.2f}-{threshold1:.2f}): {between_prob:.2%}',
        transform=ax.transAxes, fontsize=15,
        verticalalignment='bottom', bbox=props)

    ax.set_xlabel('Days')
    ax.set_ylabel('Stock Price')
    ax.set_title(f'Monte Carlo Confidence Cone for {symbol} using {title}', fontsize=25)
    ax.legend(fontsize=15)

plt.show()
Enter fullscreen mode Exit fullscreen mode

4.7 Derived Probabilities

From the simulation results, we calculate three key probabilities at the end of the forecast period:

above_threshold1_prob = (simulated_prices[-1] > threshold1).sum() / num_simulations
below_threshold2_prob = (simulated_prices[-1] < threshold2).sum() / num_simulations
between_thresholds_prob = 1 - above_threshold1_prob - below_threshold2_prob
Enter fullscreen mode Exit fullscreen mode

These probabilities answer:

  • P(> threshold1) β€” likelihood the stock exceeds a favorable level
  • P(< threshold2) β€” risk of dropping below a concerning level
  • P(between) β€” probability of staying within the defined range

Monte Carlo Simulations of ASML.AS Stock Trajectories: An Examination of Historical vs. BSM Implied Volatilities and Their Respective Probabilistic Outcomes Over a 20-Day Forecast Horizon.

5. Results: Historical vs. Implied Volatility

Our simulation produced two sets of potential future price paths:

Historical Volatility: Derived from past price movements β€” a projection rooted in historical behavior. Reliable when market conditions remain consistent.

BSM Implied Volatility: Represents market expectations β€” more relevant when future events or market regime changes are anticipated.

Interpreting the comparison:

  • Distributions align closely β†’ consensus between past behavior and market expectations
  • Distributions diverge significantly β†’ external factors or events not captured by historical data

6. Concluding Remarks

Monte Carlo simulation empowers investors to peer into a range of potential tomorrows. Key takeaways:

Appreciate uncertainty β€” the wide range of simulated paths captures the inherent unpredictability of stock prices, emphasizing the need for risk management.

Informed decision-making β€” use the probability zones to set stop-loss orders, determine entry/exit points, or adjust portfolio risk.

Use both volatility measures β€” historical vol shows what happened; implied vol shows what the market expects. Together, they give a more complete picture.

This is a step away from mere speculation and a leap towards informed, data-driven decision-making.


Get the Full Notebook

Every week I publish one Python quant finance tool like this β€” fully coded, backtested, and ready to run in Google Colab for free.

Subscribe free: AlgoEdge Insights

Top comments (1)

Collapse
 
vicchen profile image
Vic Chen

Clean implementation. The comparison between historical and BSM implied vol is really useful for anyone building quantitative tools.

One thing I have been experimenting with in my own work (tracking institutional 13F filings) is using regime-switching models to dynamically blend historical and implied vol based on market conditions. During earnings seasons or major macro events, implied vol tends to diverge significantly from historical, and a static choice between the two can be misleading.

Have you tried extending this to portfolio-level simulations with correlated assets? The correlation structure between holdings is where things get really interesting -- especially when you want to model how institutional portfolios (the kind visible in 13F filings) behave under stress scenarios.