Financial Engineering and Risk Management tutorials: Option Pricing

This tutorial is a guide for pricing European put option using either numerical integration or FFT. The second approach is useful since one only needs to specify the model’s characteristic function, instead of the density which may note be readily available.

C’est un support des travaux dirigés pour le cours de M2 “Financial Engineering and Risk Management” que j’ai enseigné aux étudiants de l’Université Paris-Sud en 2017 et 2018 (devenue désormais Paris-Saclay).

Part one

Consider a speculator who believes that the stock of Amazon will increase in value in the next 6 months. The current stock price is $2000.

Suppose that the speculator buys 100 stocks today.

Consider the following scenarios for the portfolio’s P&L (profit and loss):

Scenario Today price Future price (6m) P&L
1 $2,000 $1,900  
2 $2,000 $1,950  
3 $2,000 $2,000  
4 $2,000 $2,050  
5 $2,000 $2,100  

Q1. What is the P&L under Scenario 2? (select with mouse to reveal the answer) -5000

Q2. What is the P&L under Scenario 5? (select with mouse to reveal the answer) 10000

Instead of buying stocks, suppose that the speculator decides to buy 100 6-month call options on the Amazon stock with strike K=$2025. Assume that the option price is $10.

Consider the following scenarios for the portfolio’s P&L (profit and loss):

Scenario Today price Future price (6m) Option payoff P&L
1 $2,000 $1,900    
2 $2,000 $1,950    
3 $2,000 $2,000    
4 $2,000 $2,050    
5 $2,000 $2,100    

Q3. What is the P&L under Scenario 2? Answer: -1000

Q4. What is the P&L under Scenario 5? Answer: 6500

Suppose that your investment portfolio consists of 100 stocks of Amazon and assume that you do not buy any options to hedge your position.

Consider the following scenarios for the portfolio’s value in 6 months:

Scenario Today price Future price (6m) Future portfolio value (6m)
1 $2,000 $1,600  
2 $2,000 $1,800  
3 $2,000 $2,000  
4 $2,000 $2,200  
5 $2,000 $2,400  

Q5. What will be your future portfolio value under Scenario 1? Answer: 160000

Q6. What will be your future portfolio value under Scenario 4? Answer: 220000

Instead, suppose that you decide to buy 100 6-month put options on the Amazon stock at strike K=1900 (you still own the stocks). Assume that the put price is $12.

Consider the following scenarios for the portfolio’s value in 6 months:

Scenario Today price Future price (6m) Option payoff Future portfolio value (6m)
1 $2,000 $1,600    
2 $2,000 $1,800    
3 $2,000 $2,000    
4 $2,000 $2,200    
5 $2,000 $2,400    

Q7. What will be your future portfolio value under Scenario 1? Note: please take the cost of options into account when calculating portfolio value. Answer: 188800

Q8. What will be your future portfolio value under Scenario 4? Note: take the cost of options into account when calculating portfolio value. Answer: 218800

To answer the following questions, use a Jupyter notebook.

Assume that the stock price follows a lognormal distribution. Using the numerical integration method given during the lecture, estimate the price of a European put with the following parameters: spot price \(S_0​=100\), strike \(K=90\), risk-free rate \(r=4%\), dividend rate \(q=2%\), volatility \(σ=25%\), and maturity \(T=1\) year.

Do the numerical integration with the number of grid points equal to \(N=2n, n=1,2,...,15\). For each \(N\) your code should report \(η\) and \(P_0\) ​the price of the put:

\(N\) \(η\) \(P_0\)​
21    
22    
215    

Q9. What is the value of \(η\) when \(n=3\)? Answer: 11.25

Q10 What is the value of \(η\) when \(n=10\)? Answer: 0.088

Q11. What is the value of \(P_0​\) when \(n=4\)? Answer: 4.482

Q12. What is the value of \(P_0\)​ when \(n=15\)? Answer: 4.524

Prepare the code to price put options using FFT for different models.

For the following questions, assume that the spot price is \(S_0​=100\), the strike is \(K=80\), the risk-free rate is \(r=5%\), the dividend rate is \(1%\), and the maturity is \(T=1\).

First, we will price put options using the BMS (Black-Merton-Scholes) model. For this model, assume that the parameters are \(Θ={σ}={0.3}\). Compute the put prices according to the following table:

\(α\) \(η=0.1\)
\(N=26\)
\(η=0.1\)
\(N=210\)
\(η=0.25\)
\(N=26\)
\(η=0.25\)
\(N=210\)
-1.01        
-1.25        
-1.5        
-1.75        
-2        
-5        

Q13. Given your numerical results what is the value of the put option under the BMS model? Answer: 2.708

Next, we will price put options using the Heston model. For this model, assume that the parameters are \(Θ={κ,θ,λ,ρ,v_0​}={2,0.05,0.3,-0.7,0.04}\). Compute the put prices according to the following table:

\(α\) \(η=0.1\)
\(N=26\)
\(η=0.1\)
\(N=210\)
\(η=0.25\)
\(N=26\)
\(η=0.25\)
\(N=210\)
-1.01        
-1.25        
-1.5        
-1.75        
-2        
-5        

Q14. Given your numerical results what is the value of the put option under the Heston model? Answer: 1.341

Finally, we will price put options using the VG (Variance Gamma) model. For this model, assume that the parameters are \(Θ={σ,ν,θ}={0.3,0.5,-0.4}\). Compute the put prices according to the following table:

\(α\) \(η=0.1\)
\(N=26\)
\(η=0.1\)
\(N=210\)
\(η=0.25\)
\(N=26\)
\(η=0.25\)
\(N=210\)
-1.01        
-1.25        
-1.5        
-1.75        
-2        
-5        

Q15. Given your numerical results what is the value of the put option under the VG model? Answer: 5.3137

Solutions

A1: \(P\&L = 100 · (S_6-S_0)\)
A2: \(P\&L = 100 · (S_6-S_0)\)
A3: \(P\&L = 100 · ((S_6-K)^{+}-C)\)
A4: \(P\&L = 100 · ((S_6-K)^{+}-C)\)
A5: \(V=100 · S_6\)
A6: \(V=100 · (S_6 + (K-S_6)^{+}-P)\)
A7: \(V=100 · (S_6 + (K-S_6)^{+}-P)\)
A8: \(\eta = K/N = K/2^n\) for puts.
A9: \(\eta = K/N = K/2^n\) for puts.

Modeling:

import numpy as np # for fast vector computations
from time import time # to obtain the running time of some functions

We assume that the stock price follows a lognormal distribution. The following is a function that computes the lognormal density.

def logNormal(S, r, q, sig, S0, T):
    ''' Computes the log normal density function for the stock price. '''
    f = np.exp(-0.5*((np.log(S/S0)-(r-q-sig**2/2)*T)/(sig*np.sqrt(T)))**2) / (sig*S*np.sqrt(2*np.pi*T))
    return f

Warning: be careful when evaluating this function at \(S=0.0\). The theoretical value should be \(f=0.0\), but the function above will give you a warning and a NaN value. A quick fix is to evaluate it at \(S=\epsilon\), where \(\epsilon\) is a small positive number such as 1e-8.

We want to estimate the price of a European put with the following parameters:

# Parameters for put
S0 = 100
K = 90
r = 0.04
q = 0.02
sig = 0.25
T = 1.0

Now, we will use the numerical integration method to price this put. For this, we create the following function that takes as arguments the above parameters and a positive integer \(N\) which is the number of grid points.

def numerical_integral_put(r, q, S0, K, sig, T, N):
    ''' Numerical integration for puts. '''
    
    # temporary value for output
    eta = 0.0 # spacing of the grid
    priceP = 0.0 # price of the put
    
    
    #discount factor
    df = np.exp(-r * T)
    # step size
    eta = 1. * K / N
    # vector of stock prices
    S = np.arange(0, N) * eta
    # avoid numerical issues
    S[0] = 1e-8
    # vector of weights
    w = np.ones(N) * eta
    w[0] = eta / 2
    # lognormal densities
    logN = np.zeros(N)
    logN = logNormal(S, r, q, sig, S0, T)
    # numerical integral
    sumP = np.sum((K - S) * logN * w)
    # price put
    priceP = df * sumP

    return eta, priceP

We use the previous function to price the put for different values of \(N=2^n\), \(n=1,2,\ldots,15\).

# vector with all values of n
n_min = 1
n_max = 15
n_vec = np.arange(n_min, n_max + 1, dtype=int)
# compute the numerical integration for each value of n
start_time = time()
# will store the results in vectors
eta_vec = np.zeros(n_max)
put_vec = np.zeros(n_max)
for i in range(n_max):
    N = 2** n_vec[i]
    
    [eta_vec[i], put_vec[i]] = numerical_integral_put(r, q, S0, K, sig, T, N)
    
end_time = time()
print('Elapsed time (seconds): ' + str(end_time - start_time))
Elapsed time (seconds): 0.8806734085083008

We print a table with the results.

# print a table with the numerical integration for each value of n
print('N\teta\tP_0')
for i in range(n_max):
    print('2^%i\t%.3f\t%.4f' % (n_vec[i], eta_vec[i], put_vec[i]))
N	eta	P_0
2^1	45.000	0.4847
2^2	22.500	3.8251
2^3	11.250	4.3546
2^4	5.625	4.4822
2^5	2.812	4.5137
2^6	1.406	4.5216
2^7	0.703	4.5236
2^8	0.352	4.5241
2^9	0.176	4.5242
2^10	0.088	4.5242
2^11	0.044	4.5242
2^12	0.022	4.5242
2^13	0.011	4.5242
2^14	0.005	4.5242
2^15	0.003	4.5242

A9:

# eta, when n = 3
n_question = 3
# remember that Python indices start at 0
print(eta_vec[n_question-1])
11.25

A10:

# eta, when n = 10
n_question = 10
# remember that Python indices start at 0
print(eta_vec[n_question-1])
0.087890625

A11:

# put, when n = 4
n_question = 4
# remember that Python indices start at 0
print(put_vec[n_question-1])
4.48223458094208

A12:

# put, when n = 15
n_question = 15
# remember that Python indices start at 0
print(put_vec[n_question-1])
4.524213707706823

During the lectures, we saw how to price call options using FFT (the extension to put options is straighforward).

We first need a function that computes the characteristic function of the log-stock price for 3 different models: BMS, Heston, and VG (each one with different sets of parameters). The function below will do the job! The part of Heston and VG is already complete, you just need to implement the BMS part.

def generic_CF(u, params, S0, r, q, T, model):
    ''' Computes the characteristic function for different models (BMS, Heston, VG). '''   
    
    if (model == 'BMS'):
        # unpack parameters
        sig = params[0]

        mu = np.log(S0) + (r-q-sig**2/2)*T
        a = sig*np.sqrt(T)
        phi = np.exp(1j*mu*u-(a*u)**2/2)

    elif(model == 'Heston'):  
        # unpack parameters
        kappa  = params[0]
        theta  = params[1]
        sigma  = params[2]
        rho    = params[3]
        v0     = params[4]
        # cf
        tmp = (kappa-1j*rho*sigma*u)
        g = np.sqrt((sigma**2)*(u**2+1j*u)+tmp**2)        
        pow1 = 2*kappa*theta/(sigma**2)
        numer1 = (kappa*theta*T*tmp)/(sigma**2) + 1j*u*T*r + 1j*u*np.log(S0)
        log_denum1 = pow1 * np.log(np.cosh(g*T/2)+(tmp/g)*np.sinh(g*T/2))
        tmp2 = ((u*u+1j*u)*v0)/(g/np.tanh(g*T/2)+tmp)
        log_phi = numer1 - log_denum1 - tmp2
        phi = np.exp(log_phi)

    elif (model == 'VG'):
        # unpack parameters
        sigma  = params[0];
        nu     = params[1];
        theta  = params[2];
        # cf
        if (nu == 0):
            mu = np.log(S0) + (r-q - theta -0.5*sigma**2)*T
            phi  = np.exp(1j*u*mu) * np.exp((1j*theta*u-0.5*sigma**2*u**2)*T)
        else:
            mu  = np.log(S0) + (r-q + np.log(1-theta*nu-0.5*sigma**2*nu)/nu)*T
            phi = np.exp(1j*u*mu)*((1-1j*nu*theta*u+0.5*nu*sigma**2*u**2)**(-T/nu))

    return phi

We now provide you with a function that uses FFT to price European option for any of the 3 models we have discussed. The same function works for both calls and puts (you should ask yourself: how and why?). The function is complete, you don’t need to add any code. It return two vectors, one with the log-strikes and one with option prices.

def genericFFT(params, S0, K, r, q, T, alpha, eta, n, model):
    ''' Option pricing using FFT (model-free). '''
    
    N = 2**n
    
    # step-size in log strike space
    lda = (2 * np.pi / N) / eta
    
    # choice of beta
    #beta = np.log(S0)-N*lda/2 # the log strike we want is in the middle of the array
    beta = np.log(K) # the log strike we want is the first element of the array
    
    # forming vector x and strikes km for m=1,...,N
    km = np.zeros(N)
    xX = np.zeros(N)
    
    # discount factor
    df = np.exp(-r*T)
    
    nuJ = np.arange(N) * eta
    psi_nuJ = generic_CF(nuJ - (alpha + 1) * 1j, params, S0, r, q, T, model) / ((alpha + 1j*nuJ)*(alpha+1+1j*nuJ))
    
    km = beta + lda * np.arange(N)
    w = eta * np.ones(N)
    w[0] = eta / 2
    xX = np.exp(-1j * beta * nuJ) * df * psi_nuJ * w
     
    yY = np.fft.fft(xX)
    cT_km = np.zeros(N) 
    multiplier = np.exp(-alpha * km) / np.pi
    cT_km = multiplier * np.real(yY)
    
    return km, cT_km

We want to estimate a European put with the following parameters:

# parameters
S0 = 100
K = 80
r = 0.05
q = 0.01
T = 1.0

For the 3 models, we consider the same values of \(\alpha\), \(\eta\), and \(n\).

# parameters for fft
eta_vec = np.array([0.1, 0.25])
n_vec = np.array([6, 10])
alpha_vec = np.array([-1.01, -1.25, -1.5, -1.75, -2., -5.])
num_prices = len(eta_vec) * len(n_vec) * len(alpha_vec)

We write a function that given a model and the above parameters will compute the put price.

def price_all_puts(params, S0, K, r, q, T, model, alpha_vec, eta_vec, n_vec):
    num_prices = len(eta_vec) * len(n_vec) * len(alpha_vec)
    # output is a matrix, the columns correspond to eta, n, alpha, and put price
    put_matrix = np.zeros([num_prices, 4])
    i = 0
    for eta in eta_vec:
        for n in n_vec:
            for alpha in alpha_vec:
                # pricing via fft
                put = 0 # store here the put price
                ###############################################################
                ###############################################################
                # your code starts here
                ###############################################################
                k_vec, option_vec = genericFFT(params, S0, K, r, q, T, alpha, eta, n, model)
                put = option_vec[0] # only interested in one strike
                ###############################################################
                # your code ends here
                ###############################################################
                ###############################################################
                put_matrix[i] = np.array([eta, n, alpha, put])
                i += 1
    return put_matrix

BMS

These are the parameters for the model.

# model-specific parameters
mod = 'BMS'
sig = 0.3
params = [sig]
start_time = time()
bms_matrix = price_all_puts(params, S0, K, r, q, T, mod, alpha_vec, eta_vec, n_vec)
end_time = time()
print('Elapsed time (seconds): ' + str(end_time - start_time))
Elapsed time (seconds): 0.11588907241821289
# print results in table form
print('Model = ' + mod)
print('eta\tN\talpha\tput')
for i in range(num_prices):
    print('%.2f\t2^%i\t%.2f\t%.4f' % (bms_matrix[i,0], bms_matrix[i,1], bms_matrix[i,2], bms_matrix[i,3]))
Model = BMS
eta	N	alpha	put
0.10	2^6	-1.01	89.7431
0.10	2^6	-1.25	2.7396
0.10	2^6	-1.50	2.7569
0.10	2^6	-1.75	2.7701
0.10	2^6	-2.00	2.7789
0.10	2^6	-5.00	2.6727
0.10	2^10	-1.01	89.7316
0.10	2^10	-1.25	2.7080
0.10	2^10	-1.50	2.7080
0.10	2^10	-1.75	2.7080
0.10	2^10	-2.00	2.7080
0.10	2^10	-5.00	2.7080
0.25	2^6	-1.01	269.0367
0.25	2^6	-1.25	2.8504
0.25	2^6	-1.50	2.7083
0.25	2^6	-1.75	2.7080
0.25	2^6	-2.00	2.7080
0.25	2^6	-5.00	2.7080
0.25	2^10	-1.01	269.0367
0.25	2^10	-1.25	2.8504
0.25	2^10	-1.50	2.7083
0.25	2^10	-1.75	2.7080
0.25	2^10	-2.00	2.7080
0.25	2^10	-5.00	2.7080

A13: 2.7080

Heston

# model-specific parameters
mod = 'Heston'
kappa = 2.
theta = 0.05
lda = 0.3
rho = -0.7
v0 = 0.04
params = [kappa, theta, lda, rho, v0]
start_time = time()
heston_matrix = price_all_puts(params, S0, K, r, q, T, mod, alpha_vec, eta_vec, n_vec)
end_time = time()
print('Elapsed time (seconds): ' + str(end_time - start_time))
Elapsed time (seconds): 0.04112815856933594
# print results in table form
print('Model = ' + mod)
print('eta\tN\talpha\tput')
for i in range(num_prices):
    print('%.2f\t2^%i\t%.2f\t%.4f' % (heston_matrix[i,0], heston_matrix[i,1], heston_matrix[i,2], heston_matrix[i,3]))
Model = Heston
eta	N	alpha	put
0.10	2^6	-1.01	88.0744
0.10	2^6	-1.25	1.1102
0.10	2^6	-1.50	1.1666
0.10	2^6	-1.75	1.2167
0.10	2^6	-2.00	1.2605
0.10	2^6	-5.00	1.3979
0.10	2^10	-1.01	88.3648
0.10	2^10	-1.25	1.3412
0.10	2^10	-1.50	1.3412
0.10	2^10	-1.75	1.3412
0.10	2^10	-2.00	1.3412
0.10	2^10	-5.00	1.3412
0.25	2^6	-1.01	267.6738
0.25	2^6	-1.25	1.4873
0.25	2^6	-1.50	1.3450
0.25	2^6	-1.75	1.3445
0.25	2^6	-2.00	1.3442
0.25	2^6	-5.00	1.3415
0.25	2^10	-1.01	267.6698
0.25	2^10	-1.25	1.4835
0.25	2^10	-1.50	1.3414
0.25	2^10	-1.75	1.3412
0.25	2^10	-2.00	1.3412
0.25	2^10	-5.00	1.3412

A14: 1.3412

VG

# model-specific parameters
mod = 'VG'
sigma = 0.3
nu = 0.5
theta = -0.4
params = [sigma, nu, theta]
start_time = time()
vg_matrix = price_all_puts(params, S0, K, r, q, T, mod, alpha_vec, eta_vec, n_vec)
end_time = time()
print('Elapsed time (seconds): ' + str(end_time - start_time))
Elapsed time (seconds): 0.020387887954711914
# print results in table form
print('Model = ' + mod)
print('eta\tN\talpha\tput')
for i in range(num_prices):
    print('%.2f\t2^%i\t%.2f\t%.4f' % (vg_matrix[i,0], vg_matrix[i,1], vg_matrix[i,2], vg_matrix[i,3]))
Model = VG
eta	N	alpha	put
0.10	2^6	-1.01	92.2103
0.10	2^6	-1.25	5.2047
0.10	2^6	-1.50	5.2230
0.10	2^6	-1.75	5.2401
0.10	2^6	-2.00	5.2556
0.10	2^6	-5.00	0.0030
0.10	2^10	-1.01	92.3373
0.10	2^10	-1.25	5.3137
0.10	2^10	-1.50	5.3137
0.10	2^10	-1.75	5.3137
0.10	2^10	-2.00	5.3137
0.10	2^10	-5.00	0.0000
0.25	2^6	-1.01	271.6399
0.25	2^6	-1.25	5.4539
0.25	2^6	-1.50	5.3121
0.25	2^6	-1.75	5.3122
0.25	2^6	-2.00	5.3124
0.25	2^6	-5.00	0.0019
0.25	2^10	-1.01	271.6423
0.25	2^10	-1.25	5.4560
0.25	2^10	-1.50	5.3139
0.25	2^10	-1.75	5.3137
0.25	2^10	-2.00	5.3137
0.25	2^10	-5.00	0.0020

A15: 5.3137