NV
NordVarg
ServicesTechnologiesIndustriesCase StudiesBlogAboutContact
Get Started

Footer

NV
NordVarg

Software Development & Consulting

GitHubLinkedInTwitter

Services

  • Product Development
  • Quantitative Finance
  • Financial Systems
  • ML & AI

Technologies

  • C++
  • Python
  • Rust
  • OCaml
  • TypeScript
  • React

Company

  • About
  • Case Studies
  • Blog
  • Contact

© 2025 NordVarg. All rights reserved.

November 25, 2025
•
NordVarg Team
•

Jump-Diffusion Models for Equity and Crypto Markets

Quantitative Financejump-diffusionMerton-modelKou-modelPoisson-processvariance-gammaoption-pricingcrypto
10 min read
Share:

TL;DR – Jump-diffusion models capture sudden price movements that pure diffusion models miss. This guide covers Merton and Kou models, Poisson processes, calibration to market data, and variance gamma processes with production implementations.

1. Why Jump-Diffusion Models?#

Standard geometric Brownian motion (GBM) assumes continuous price paths. But real markets exhibit:

  • Sudden crashes – Black Monday 1987, COVID-19 crash
  • Earnings surprises – Gap ups/downs on announcements
  • Crypto volatility – Flash crashes, exchange hacks
  • Fat tails – More extreme moves than normal distribution predicts

Key insight: Jump-diffusion models add discrete jumps to continuous diffusion, better capturing market reality.

2. Poisson Processes: Modeling Jump Arrivals#

2.1 Definition#

A Poisson process NtN_tNt​ counts random events over time:

  • Jump arrivals are independent
  • Average rate λ\lambdaλ (jumps per year)
  • Probability of kkk jumps in time ttt: P(Nt=k)=(λt)ke−λtk!P(N_t = k) = \frac{(\lambda t)^k e^{-\lambda t}}{k!}P(Nt​=k)=k!(λt)ke−λt​

2.2 Simulation#

python
1import numpy as np
2import matplotlib.pyplot as plt
3
4def simulate_poisson_process(lambda_rate, T, n_paths=1):
5    """
6    Simulate Poisson process paths.
7    
8    Args:
9        lambda_rate: Average jump rate per unit time
10        T: Time horizon
11        n_paths: Number of paths
12        
13    Returns:
14        List of jump times for each path
15    """
16    paths = []
17    for _ in range(n_paths):
18        t = 0
19        jump_times = []
20        while t < T:
21            # Time to next jump ~ Exponential(lambda)
22            dt = np.random.exponential(1/lambda_rate)
23            t += dt
24            if t < T:
25                jump_times.append(t)
26        paths.append(np.array(jump_times))
27    return paths
28
29# Simulate
30lambda_rate = 5  # 5 jumps per year on average
31T = 1.0
32paths = simulate_poisson_process(lambda_rate, T, n_paths=10)
33
34# Plot
35plt.figure(figsize=(12, 6))
36for i, jump_times in enumerate(paths):
37    counts = np.arange(len(jump_times) + 1)
38    times = np.concatenate([[0], jump_times, [T]])
39    counts_extended = np.repeat(counts, 2)[1:-1]
40    times_extended = np.repeat(times, 2)[:-2]
41    plt.plot(times_extended, counts_extended, alpha=0.6, label='Path ' + str(i+1) if i < 3 else '')
42
43plt.xlabel('Time')
44plt.ylabel('Number of Jumps')
45plt.title('Poisson Process: Jump Arrivals (lambda = 5)')
46plt.legend()
47plt.grid(True, alpha=0.3)
48plt.show()
49

3. Merton Jump-Diffusion Model (1976)#

3.1 Model Dynamics#

Stock price follows: dSt=μStdt+σStdWt+StdJtdS_t = \mu S_t dt + \sigma S_t dW_t + S_t dJ_tdSt​=μSt​dt+σSt​dWt​+St​dJt​

Where:

  • μ\muμ = drift
  • σ\sigmaσ = diffusion volatility
  • dJtdJ_tdJt​ = jump component
  • Jumps occur with rate λ\lambdaλ
  • Jump sizes YYY are log-normal: log⁡(1+Y)∼N(mJ,σJ2)\log(1+Y) \sim N(m_J, \sigma_J^2)log(1+Y)∼N(mJ​,σJ2​)

Analytical solution: St=S0exp⁡((μ−σ22)t+σWt)∏i=1Nt(1+Yi)S_t = S_0 \exp\left((\mu - \frac{\sigma^2}{2})t + \sigma W_t\right) \prod_{i=1}^{N_t} (1 + Y_i)St​=S0​exp((μ−2σ2​)t+σWt​)∏i=1Nt​​(1+Yi​)

3.2 Implementation#

python
1class MertonJumpDiffusion:
2    """Merton jump-diffusion model."""
3    
4    def __init__(self, mu, sigma, lambda_jump, m_jump, sigma_jump, S0):
5        self.mu = mu
6        self.sigma = sigma
7        self.lambda_jump = lambda_jump
8        self.m_jump = m_jump
9        self.sigma_jump = sigma_jump
10        self.S0 = S0
11    
12    def simulate(self, T, n_steps, n_paths=1):
13        """
14        Simulate price paths.
15        
16        Returns:
17            Array of shape (n_paths, n_steps + 1)
18        """
19        dt = T / n_steps
20        sqrt_dt = np.sqrt(dt)
21        
22        S = np.zeros((n_paths, n_steps + 1))
23        S[:, 0] = self.S0
24        
25        for i in range(n_steps):
26            # Diffusion component
27            dW = np.random.normal(0, sqrt_dt, n_paths)
28            diffusion = (self.mu - 0.5 * self.sigma**2) * dt + self.sigma * dW
29            
30            # Jump component
31            n_jumps = np.random.poisson(self.lambda_jump * dt, n_paths)
32            jump_sizes = np.zeros(n_paths)
33            
34            for j in range(n_paths):
35                if n_jumps[j] > 0:
36                    # Sum of log-normal jump sizes
37                    log_jumps = np.random.normal(
38                        self.m_jump, self.sigma_jump, n_jumps[j]
39                    )
40                    jump_sizes[j] = np.sum(log_jumps)
41            
42            # Update price
43            S[:, i+1] = S[:, i] * np.exp(diffusion + jump_sizes)
44        
45        return S
46    
47    def option_price_fft(self, K, T, r, option_type='call'):
48        """
49        Price European option using FFT (Carr-Madan).
50        
51        Faster than Monte Carlo for single option.
52        """
53        # This is a simplified version
54        # Full implementation would use characteristic function
55        pass
56    
57    def calibrate(self, market_prices, strikes, maturities):
58        """
59        Calibrate model to market option prices.
60        
61        Minimize sum of squared pricing errors.
62        """
63        from scipy.optimize import minimize
64        
65        def objective(params):
66            mu, sigma, lambda_j, m_j, sigma_j = params
67            model = MertonJumpDiffusion(mu, sigma, lambda_j, m_j, sigma_j, self.S0)
68            
69            errors = []
70            for i, (K, T, market_price) in enumerate(zip(strikes, maturities, market_prices)):
71                # Price option via Monte Carlo
72                paths = model.simulate(T, 252, n_paths=10000)
73                payoffs = np.maximum(paths[:, -1] - K, 0)
74                model_price = np.exp(-mu * T) * np.mean(payoffs)
75                errors.append((model_price - market_price)**2)
76            
77            return np.sum(errors)
78        
79        # Initial guess
80        x0 = [0.1, 0.2, 2.0, -0.1, 0.3]
81        bounds = [(0, 0.5), (0.01, 1.0), (0, 10), (-1, 0), (0.01, 1.0)]
82        
83        result = minimize(objective, x0, bounds=bounds, method='L-BFGS-B')
84        
85        return {
86            'mu': result.x[0],
87            'sigma': result.x[1],
88            'lambda': result.x[2],
89            'm_jump': result.x[3],
90            'sigma_jump': result.x[4],
91            'rmse': np.sqrt(result.fun / len(market_prices))
92        }
93
94# Example usage
95model = MertonJumpDiffusion(
96    mu=0.05,
97    sigma=0.2,
98    lambda_jump=2.0,  # 2 jumps per year
99    m_jump=-0.1,      # Average jump is -10%
100    sigma_jump=0.15,  # Jump volatility
101    S0=100
102)
103
104# Simulate paths
105paths = model.simulate(T=1.0, n_steps=252, n_paths=100)
106
107# Plot
108t = np.linspace(0, 1, 253)
109plt.figure(figsize=(14, 7))
110for i in range(10):
111    plt.plot(t, paths[i], alpha=0.6)
112plt.xlabel('Time (years)')
113plt.ylabel('Stock Price')
114plt.title('Merton Jump-Diffusion: Sample Paths')
115plt.grid(True, alpha=0.3)
116plt.show()
117

Performance: Simulating 10,000 paths over 252 steps takes ~200ms.

4. Kou Double Exponential Jump-Diffusion (2002)#

4.1 Model Specification#

Improvement over Merton: asymmetric jumps (different up/down distributions).

Jump sizes YYY follow double exponential: fY(y)=p⋅η1e−η1y1y≥0+(1−p)⋅η2eη2y1y<0f_Y(y) = p \cdot \eta_1 e^{-\eta_1 y} \mathbb{1}_{y \geq 0} + (1-p) \cdot \eta_2 e^{\eta_2 y} \mathbb{1}_{y < 0}fY​(y)=p⋅η1​e−η1​y1y≥0​+(1−p)⋅η2​eη2​y1y<0​

Where:

  • ppp = probability of upward jump
  • η1\eta_1η1​ = decay rate for upward jumps
  • η2\eta_2η2​ = decay rate for downward jumps

Key feature: Captures asymmetric tail behavior (crashes vs rallies).

4.2 Implementation#

python
1class KouJumpDiffusion:
2    """Kou double exponential jump-diffusion model."""
3    
4    def __init__(self, mu, sigma, lambda_jump, p, eta1, eta2, S0):
5        self.mu = mu
6        self.sigma = sigma
7        self.lambda_jump = lambda_jump
8        self.p = p  # Prob of upward jump
9        self.eta1 = eta1  # Upward jump decay
10        self.eta2 = eta2  # Downward jump decay
11        self.S0 = S0
12    
13    def generate_jump_sizes(self, n_jumps):
14        """Generate double exponential jump sizes."""
15        # Decide direction
16        directions = np.random.binomial(1, self.p, n_jumps)
17        
18        jumps = np.zeros(n_jumps)
19        # Upward jumps
20        n_up = np.sum(directions)
21        if n_up > 0:
22            jumps[directions == 1] = np.random.exponential(1/self.eta1, n_up)
23        
24        # Downward jumps
25        n_down = n_jumps - n_up
26        if n_down > 0:
27            jumps[directions == 0] = -np.random.exponential(1/self.eta2, n_down)
28        
29        return jumps
30    
31    def simulate(self, T, n_steps, n_paths=1):
32        """Simulate price paths."""
33        dt = T / n_steps
34        sqrt_dt = np.sqrt(dt)
35        
36        S = np.zeros((n_paths, n_steps + 1))
37        S[:, 0] = self.S0
38        
39        for i in range(n_steps):
40            # Diffusion
41            dW = np.random.normal(0, sqrt_dt, n_paths)
42            diffusion = (self.mu - 0.5 * self.sigma**2) * dt + self.sigma * dW
43            
44            # Jumps
45            total_jumps = np.zeros(n_paths)
46            for j in range(n_paths):
47                n_jumps = np.random.poisson(self.lambda_jump * dt)
48                if n_jumps > 0:
49                    jump_sizes = self.generate_jump_sizes(n_jumps)
50                    total_jumps[j] = np.sum(jump_sizes)
51            
52            # Update
53            S[:, i+1] = S[:, i] * np.exp(diffusion + total_jumps)
54        
55        return S
56
57# Example: Asymmetric jumps (crashes larger than rallies)
58kou = KouJumpDiffusion(
59    mu=0.05,
60    sigma=0.15,
61    lambda_jump=3.0,
62    p=0.4,        # 40% upward jumps
63    eta1=10.0,    # Small upward jumps
64    eta2=5.0,     # Larger downward jumps
65    S0=100
66)
67
68paths_kou = kou.simulate(T=1.0, n_steps=252, n_paths=100)
69
70# Compare distributions
71plt.figure(figsize=(14, 6))
72plt.subplot(1, 2, 1)
73for i in range(10):
74    plt.plot(t, paths_kou[i], alpha=0.6)
75plt.xlabel('Time')
76plt.ylabel('Price')
77plt.title('Kou Model: Asymmetric Jumps')
78plt.grid(True, alpha=0.3)
79
80plt.subplot(1, 2, 2)
81returns_kou = np.log(paths_kou[:, -1] / paths_kou[:, 0])
82plt.hist(returns_kou, bins=50, alpha=0.7, density=True, label='Kou')
83plt.xlabel('Log Return')
84plt.ylabel('Density')
85plt.title('Return Distribution (Fat Tails)')
86plt.legend()
87plt.grid(True, alpha=0.3)
88plt.tight_layout()
89plt.show()
90

5. Variance Gamma Process#

5.1 Model Description#

Pure jump process (no diffusion component). Time is randomized by gamma process.

St=S0exp⁡(Xt)S_t = S_0 \exp(X_t)St​=S0​exp(Xt​)

where XtX_tXt​ is a variance gamma process with parameters (σ,ν,θ)(\sigma, \nu, \theta)(σ,ν,θ).

Properties:

  • Infinite activity (infinitely many small jumps)
  • Captures skewness and kurtosis
  • Popular for crypto markets

5.2 Implementation#

python
1class VarianceGamma:
2    """Variance Gamma process."""
3    
4    def __init__(self, sigma, nu, theta, S0):
5        self.sigma = sigma  # Volatility
6        self.nu = nu        # Variance rate
7        self.theta = theta  # Drift
8        self.S0 = S0
9    
10    def simulate(self, T, n_steps, n_paths=1):
11        """Simulate via gamma time change."""
12        dt = T / n_steps
13        
14        S = np.zeros((n_paths, n_steps + 1))
15        S[:, 0] = self.S0
16        
17        for i in range(n_steps):
18            # Gamma time change
19            gamma_times = np.random.gamma(dt / self.nu, self.nu, n_paths)
20            
21            # Brownian motion with drift
22            X = self.theta * gamma_times + \
23                self.sigma * np.sqrt(gamma_times) * np.random.normal(0, 1, n_paths)
24            
25            S[:, i+1] = S[:, i] * np.exp(X)
26        
27        return S
28
29# Example
30vg = VarianceGamma(sigma=0.2, nu=0.5, theta=-0.1, S0=100)
31paths_vg = vg.simulate(T=1.0, n_steps=252, n_paths=1000)
32
33# Analyze tail behavior
34returns_vg = np.log(paths_vg[:, -1] / paths_vg[:, 0])
35print("Skewness: {:.3f}".format(np.mean((returns_vg - np.mean(returns_vg))**3) / np.std(returns_vg)**3))
36print("Kurtosis: {:.3f}".format(np.mean((returns_vg - np.mean(returns_vg))**4) / np.std(returns_vg)**4))
37

6. Option Pricing with Jumps#

6.1 Monte Carlo Pricing#

python
1def price_european_option_jump_diffusion(
2    model,
3    K,
4    T,
5    r,
6    n_sims=100000,
7    option_type='call'
8):
9    """
10    Price European option under jump-diffusion.
11    
12    Args:
13        model: Jump-diffusion model instance
14        K: Strike price
15        T: Maturity
16        r: Risk-free rate
17        n_sims: Number of simulations
18        option_type: 'call' or 'put'
19    """
20    # Simulate terminal prices
21    paths = model.simulate(T, n_steps=252, n_paths=n_sims)
22    ST = paths[:, -1]
23    
24    # Payoffs
25    if option_type == 'call':
26        payoffs = np.maximum(ST - K, 0)
27    else:
28        payoffs = np.maximum(K - ST, 0)
29    
30    # Discount
31    price = np.exp(-r * T) * np.mean(payoffs)
32    std_error = np.exp(-r * T) * np.std(payoffs) / np.sqrt(n_sims)
33    
34    return price, std_error
35
36# Example
37price, se = price_european_option_jump_diffusion(model, K=100, T=1.0, r=0.05)
38print("Option price: {:.4f} ± {:.4f}".format(price, se))
39

6.2 Implied Volatility Smile#

Jump-diffusion models naturally produce volatility smiles:

python
1def compute_implied_vol_smile(model, strikes, T, r):
2    """Compute implied volatility smile."""
3    from scipy.optimize import brentq
4    from scipy.stats import norm
5    
6    def black_scholes_price(S0, K, r, sigma, T):
7        d1 = (np.log(S0/K) + (r + 0.5*sigma**2)*T) / (sigma*np.sqrt(T))
8        d2 = d1 - sigma*np.sqrt(T)
9        return S0 * norm.cdf(d1) - K * np.exp(-r*T) * norm.cdf(d2)
10    
11    def implied_vol(market_price, S0, K, r, T):
12        def objective(sigma):
13            return black_scholes_price(S0, K, r, sigma, T) - market_price
14        try:
15            return brentq(objective, 0.01, 2.0)
16        except:
17            return np.nan
18    
19    implied_vols = []
20    for K in strikes:
21        price, _ = price_european_option_jump_diffusion(model, K, T, r, n_sims=50000)
22        iv = implied_vol(price, model.S0, K, r, T)
23        implied_vols.append(iv)
24    
25    return np.array(implied_vols)
26
27# Generate smile
28strikes = np.linspace(80, 120, 9)
29ivs = compute_implied_vol_smile(model, strikes, T=1.0, r=0.05)
30
31plt.figure(figsize=(10, 6))
32plt.plot(strikes, ivs * 100, 'o-', linewidth=2)
33plt.xlabel('Strike Price')
34plt.ylabel('Implied Volatility (%)')
35plt.title('Volatility Smile from Jump-Diffusion Model')
36plt.grid(True, alpha=0.3)
37plt.show()
38

7. Calibration to Market Data#

7.1 Objective Function#

Minimize squared errors between model and market prices:

python
1def calibrate_to_market(
2    S0,
3    market_data,  # List of (K, T, market_price)
4    r,
5    model_type='merton'
6):
7    """
8    Calibrate jump-diffusion model to market option prices.
9    
10    Returns:
11        Calibrated parameters
12    """
13    from scipy.optimize import differential_evolution
14    
15    def objective(params):
16        if model_type == 'merton':
17            mu, sigma, lambda_j, m_j, sigma_j = params
18            model = MertonJumpDiffusion(mu, sigma, lambda_j, m_j, sigma_j, S0)
19        elif model_type == 'kou':
20            mu, sigma, lambda_j, p, eta1, eta2 = params
21            model = KouJumpDiffusion(mu, sigma, lambda_j, p, eta1, eta2, S0)
22        
23        total_error = 0
24        for K, T, market_price in market_data:
25            model_price, _ = price_european_option_jump_diffusion(
26                model, K, T, r, n_sims=10000
27            )
28            total_error += (model_price - market_price)**2
29        
30        return total_error
31    
32    # Bounds
33    if model_type == 'merton':
34        bounds = [(0, 0.3), (0.05, 0.5), (0, 10), (-0.5, 0), (0.01, 0.5)]
35    else:
36        bounds = [(0, 0.3), (0.05, 0.5), (0, 10), (0.1, 0.9), (1, 50), (1, 50)]
37    
38    result = differential_evolution(objective, bounds, maxiter=100, seed=42)
39    
40    return result.x, np.sqrt(result.fun / len(market_data))
41

8. Production Checklist#

  • Validate jump parameters – Ensure λ>0\lambda > 0λ>0, σJ>0\sigma_J > 0σJ​>0
  • Use variance reduction – Antithetic variates for Monte Carlo
  • Implement FFT pricing – Faster than MC for single options
  • Cache calibration results – Expensive to recalibrate
  • Monitor jump detection – Real-time jump identification
  • Handle edge cases – Very high/low jump rates
  • Benchmark against Black-Scholes – Verify smile generation
  • Test on historical crashes – 1987, 2008, 2020
  • Implement Greeks – Delta, gamma, vega under jumps
  • Document assumptions – Jump size distributions

References#

  1. Merton, R. (1976). Option pricing when underlying stock returns are discontinuous. Journal of Financial Economics.
  2. Kou, S. (2002). A jump-diffusion model for option pricing. Management Science.
  3. Cont, R. & Tankov, P. (2004). Financial Modelling with Jump Processes. Chapman & Hall.
  4. Madan, D. & Seneta, E. (1990). The Variance Gamma (V.G.) Model for Share Market Returns. Journal of Business.

Jump-diffusion models are essential for pricing options in volatile markets. Use Merton for simplicity, Kou for asymmetry, and variance gamma for crypto. Always calibrate to market data and validate with historical crashes.

NT

NordVarg Team

Technical Writer

NordVarg Team is a software engineer at NordVarg specializing in high-performance financial systems and type-safe programming.

jump-diffusionMerton-modelKou-modelPoisson-processvariance-gamma

Join 1,000+ Engineers

Get weekly insights on building high-performance financial systems, latest industry trends, and expert tips delivered straight to your inbox.

✓Weekly articles
✓Industry insights
✓No spam, ever

Related Posts

Nov 24, 2025•15 min read
Stochastic Calculus for Quantitative Finance
Quantitative Financestochastic-calculusito-lemma
Jan 21, 2025•17 min read
GPU Computing for Quantitative Finance: CUDA vs OpenCL vs Vulkan Compute
Quantitative Financegpucuda
Nov 26, 2025•11 min read
GPU-Accelerated Portfolio Optimization: When 10 Hours Becomes 10 Seconds
Quantitative Financeportfolio-optimizationGPU

Interested in working together?