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 priceThe 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- 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 errorstd / 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