σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
uantCraft AcademySkills
Ctrl K
AO
+1.2k Learners
LoginJoin ↗
Skills
AO
+1.2k Learners
Login
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Python skill›Module 8 · Stochastic models & machine learning›Topic 8.4·~55 minAdvanced

Asian options

Skill positionModule 8 of 11 · Topic 4 of 10
8.4 Asian options
—Lab score— / 100Total XP—Streak—

Paying on the average

A standard call depends on the price on one day, the last. An Asian option depends on the average price over a set of dates: an Asian call pays max(average - K, 0). Companies use them to hedge a cost they incur steadily, for example a monthly oil purchase, and they are hard to manipulate at expiry. They are *path-dependent*, so there is no simple Black–Scholes formula for the arithmetic average, and the usual tool is Monte Carlo simulation.

import numpy as np

# Two simulated paths: the starting price, then monthly prices; the last column is expiry
paths = np.array([[100.0, 90.0, 110.0, 130.0],
                  [100.0, 70.0,  80.0,  90.0]])
average = paths[:, 1:].mean(axis=1)                      # the monitoring dates only, not the starting price
print(average)                                           # [110.  80.]
print(np.maximum(average - 100.0, 0.0))                  # Asian call payoffs: [10.  0.]
print(np.maximum(paths[:, -1] - 100.0, 0.0))             # European call payoffs on the same paths: [30.  0.]

Simulating the paths

The paths come from geometric Brownian motion, with the risk-neutral drift: the rate r replaces mu. Draw a matrix of standard normals with one row per path and one column per monitoring date, turn it into log price moves, and take a cumulative sum along each row. One array then holds all the paths, and every later step (averages, payoffs) is a single vectorised operation along the rows, with no loop over paths.

import numpy as np

def simulate_paths(s0, rate, sigma, t, steps, n_paths, rng):
    dt = t / steps
    z = rng.standard_normal((n_paths, steps))
    log_moves = (rate - 0.5 * sigma ** 2) * dt + sigma * np.sqrt(dt) * z
    return s0 * np.exp(np.hstack([np.zeros((n_paths, 1)), np.cumsum(log_moves, axis=1)]))

rng = np.random.default_rng(1)
paths = simulate_paths(100.0, 0.05, 0.2, 1.0, 12, 50000, rng)       # monthly monitoring for a year
print(paths.shape)                                                   # (50000, 13)
print(round(float(paths[:, -1].mean()), 2), round(100 * np.exp(0.05), 2))      # the risk-neutral mean of the final price

The price and its error bar

The option's price is the discounted average payoff: exp(-r * T) * mean(payoffs). A Monte Carlo price is an estimate, so always report its standard error, std(payoffs) / sqrt(number of paths) (discounted), which tells how far from the truth the number may be: four times as many paths halve it. An Asian call is cheaper than a European call on the same stock, because averaging smooths the price and lowers the effective volatility.

import numpy as np

def simulate_paths(s0, rate, sigma, t, steps, n_paths, rng):
    dt = t / steps
    z = rng.standard_normal((n_paths, steps))
    log_moves = (rate - 0.5 * sigma ** 2) * dt + sigma * np.sqrt(dt) * z
    return s0 * np.exp(np.hstack([np.zeros((n_paths, 1)), np.cumsum(log_moves, axis=1)]))

def asian_price(paths, strike, rate, t):
    payoff = np.maximum(paths[:, 1:].mean(axis=1) - strike, 0.0)
    discount = np.exp(-rate * t)
    return discount * payoff.mean(), discount * payoff.std(ddof=1) / np.sqrt(len(payoff))

rng = np.random.default_rng(1)
paths = simulate_paths(100.0, 0.05, 0.2, 1.0, 12, 100000, rng)
price, error = asian_price(paths, 100.0, 0.05, 1.0)
european = np.exp(-0.05) * np.maximum(paths[:, -1] - 100.0, 0.0).mean()
print(round(float(price), 3), "+/-", round(float(error), 3))
print(round(float(european), 3), "European call on the same paths")
# Geometric and arithmetic averages of the same monitoring prices
import numpy as np

prices = np.array([90.0, 110.0, 130.0])
arithmetic = prices.mean()
geometric = np.exp(np.log(prices).mean())
print(round(float(arithmetic), 3), round(float(geometric), 3))        # 110.0 108.774: the geometric average is never larger
The geometric average has a formulaA product of lognormals is lognormal, so an Asian option on the geometric average can be priced by a Black–Scholes-style formula. That makes it valuable as a check, and even more as a control variate for the arithmetic one: the two payoffs are highly correlated, so the known exact price of the geometric option can cancel much of the Monte Carlo noise. A later topic, control variates, uses exactly this idea.
  • An Asian option pays on the average of the monitoring dates: max(average - K, 0); path-dependent, so no simple formula and Monte Carlo is the tool.
  • Simulate all paths in one array (rows = paths, columns = dates) with the risk-neutral drift; averages and payoffs are then vectorised along the rows.
  • Price = exp(-r * T) * mean(payoffs) with a standard error std / sqrt(paths); averaging makes it cheaper than a European call, and the geometric version is a control variate.

Next up: barrier options, which are born or die when the price touches a level.

Lab · what's graded

  • Average along each path with average_price25 pts
  • Try the geometric average with geometric_average25 pts
  • Pay on the average with asian_call_payoff25 pts
  • Price it, with an error bar, with asian_price25 pts
Explorer · lab-105-1
▾ lab-105-1
…
bash · in-browser
learner@quantcraft:~/lab-105-1$
Python not loaded yetPythonLn 1, Col 1Spaces: 4UTF-8

Reset this topic?

Everything in lab-105-1 goes back to the starter files and the terminal is cleared. Your current solution will be lost — this can't be undone.

Will be reset
  • main.py → starter code
  • Terminal history and test output
  • Unsubmitted changes
Will be kept
  • Best lab score
  • Your XP and streak
  • Lecture progress and notes

Log in to run your code.

Every lecture is free to read. Create an account or log in to run code, run tests and submit this lab.

  • In-browser IDE with a bash terminal — nothing to install
  • Instant grading, XP and your place on the skill
Create an accountLog in
OR
Continue with GitHub