Fourier-based Option Pricing

Documentation:

This is the documentation for fftoptionlib [https://github.com/arraystream/fftoptionlib]

Assume the price of the asset has the following representation: $$ S_t = S_0 \exp{[(r-q+w)t+X_t]} $$ where $X_t$ is a stochastic process and r,q,w are interest rate, dividend rate and martingale correction item

We can get the log price $$ \log(S_t)=\log(S_0)+(r-q+w)t+X_t $$

If the charateristic function of $X_t$ is known, we can derive the characteristic function of $\log(S_t)$

We provide the following characteristic functions for $\log(S_t)$:

  • BlackScholes
  • MertonJump
  • KouJump
  • Poisson
  • VarianceGamma
  • NIG
  • Heston
  • CGMY

If you want to add more, just write the characteristic function for $X_t$, use general_ln_st_chf function can produce the corresponding characteristic function for $\log(S_t)$

Each Process can be priced by the following three methods

  • FFTEngine
  • FractionFFTEngine
  • CosineEngine

In [1]:
import numpy as np
import simpleplotly as spt
from fftoptionlib import *
import plotly
import plotly.offline as py
%matplotlib inline
plotly.offline.init_notebook_mode() # run at the start of every notebook


FFT Experiments

Black-Scholes analytical Call price


In [2]:
from scipy.stats import norm

def phi(x):
    return np.exp(-x*x/2.)/np.sqrt(2*pi)
   
def BlackScholesCore(CallPutFlag,DF,F,X,T,v):
    vsqrt=v*np.sqrt(T)
    d1 = (np.log(F/X)+(vsqrt*vsqrt/2.))/vsqrt
    d2 = d1-vsqrt
    if CallPutFlag:
        return DF*(F*norm.cdf(d1)-X*norm.cdf(d2))
    else:
        return DF*(X*norm.cdf(-d2)-F*norm.cdf(-d1))

def BlackScholesAnalytical(CallPutFlag,S,X,T,r,d,v):
    return BlackScholesCore(CallPutFlag,np.exp(-r*T),np.exp((r-d)*T)*S,X,T,v)

Set Up


In [3]:
underlying_price = 100
maturity_date = '2016-03-17'
risk_free_rate = 0.05
dividend_rate = 0.01
evaluation_date = '2016-01-17'
vanilla_option = BasicOption()
(vanilla_option.set_underlying_close_price(underlying_price)
 .set_dividend(dividend_rate)
 .set_maturity_date(maturity_date)
 .set_evaluation_date(evaluation_date)
 .set_zero_rate(risk_free_rate))

ft_pricer = FourierPricer(vanilla_option)

In [4]:
import fftoptionlib

In [5]:
fftoptionlib.BasicOption


Out[5]:
fftoptionlib.option_class.BasicOption

In [6]:
import pandas as pd

Black-Scholes

FFT Engine


In [7]:
N=2**15
d_u = 0.01
alpha = 1
sigma = 0.3
(ft_pricer.set_log_st_process(BlackScholes(sigma))
          .set_pricing_engine(FFTEngine(N, d_u, alpha, spline_order=2)))


Out[7]:
<fftoptionlib.pricing_class.FourierPricer at 0x7f12eaf77dd8>

In [8]:
strike_arr = np.arange(50,120,5)
put_call=np.array(['call']*len(strike_arr))
theory_value = BlackScholesAnalytical(True,underlying_price,strike_arr,60/365,risk_free_rate,dividend_rate,sigma)

In [9]:
%time
ffn_value = ft_pricer.calc_price(strike_arr,put_call)


CPU times: user 2 µs, sys: 3 µs, total: 5 µs
Wall time: 9.78 µs

In [10]:
fig=spt.FigureBuilder()
fig.add(spt.Scatter(x=strike_arr,y=theory_value,name='Analytical BS'))
fig.add(spt.Scatter(x=strike_arr,y=ffn_value,name='FFT BS'))
fig.add(spt.XAxis('strike'))
fig.add(spt.YAxis('call price'))
fig.update_layout(title='Call Option Prices').plot()


Fractional FFT Engine


In [11]:
N=2**12
d_u = 0.01
d_k = 0.01
alpha = 1
ft_pricer.set_pricing_engine(FractionFFTEngine(N, d_u, d_k,alpha, spline_order=2))


Out[11]:
<fftoptionlib.pricing_class.FourierPricer at 0x7f12eaf77dd8>

In [12]:
%time
ffn_value = ft_pricer.calc_price(strike_arr,put_call)


CPU times: user 3 µs, sys: 2 µs, total: 5 µs
Wall time: 9.3 µs

In [13]:
fig=spt.FigureBuilder()
fig.add(spt.Scatter(x=strike_arr,y=theory_value,name='Analytical BS'))
fig.add(spt.Scatter(x=strike_arr,y=ffn_value,name='FractionalFFT BS'))
fig.add(spt.XAxis('strike'))
fig.add(spt.YAxis('call price'))
fig.update_layout(title='Call Option Prices').plot()


Cosine Engine


In [14]:
N=1000
L=10
ft_pricer.set_pricing_engine(CosineEngine(N,L))


Out[14]:
<fftoptionlib.pricing_class.FourierPricer at 0x7f12eaf77dd8>

In [15]:
%time
ffn_value = ft_pricer.calc_price(strike_arr,put_call)


CPU times: user 3 µs, sys: 2 µs, total: 5 µs
Wall time: 8.58 µs

In [16]:
fig=spt.FigureBuilder()
fig.add(spt.Scatter(x=strike_arr,y=theory_value,name='Analytical BS'))
fig.add(spt.Scatter(x=strike_arr,y=ffn_value,name='Cosine BS'))
fig.add(spt.XAxis('strike'))
fig.add(spt.YAxis('call price'))
fig.update_layout(title='Call Option Prices').plot()


Heston


In [17]:
ft_pricer.set_log_st_process(Heston(
    V0=0.04,
    theta=0.04,
    k=2,
    sigma=0.3,
    rho=-0.7))


Out[17]:
<fftoptionlib.pricing_class.FourierPricer at 0x7f12eaf77dd8>

FFT Engine


In [18]:
ft_pricer.set_pricing_engine(FFTEngine(N=2**15, d_u=0.01, alpha=1, spline_order=2))
%time
fft_price = ft_pricer.calc_price(strike_arr,put_call)


CPU times: user 2 µs, sys: 2 µs, total: 4 µs
Wall time: 8.58 µs

Fractional FFT Engine


In [19]:
ft_pricer.set_pricing_engine(FractionFFTEngine(N=2**15, d_u=0.01, d_k=0.01,alpha=1, spline_order=2))
%time
fraction_price = ft_pricer.calc_price(strike_arr,put_call)


CPU times: user 4 µs, sys: 0 ns, total: 4 µs
Wall time: 9.06 µs

Cosine Engine


In [20]:
ft_pricer.set_pricing_engine(CosineEngine(N=2000,L=10))
%time
cos_price = ft_pricer.calc_price(strike_arr,put_call)


CPU times: user 2 µs, sys: 2 µs, total: 4 µs
Wall time: 9.06 µs

In [21]:
fig=spt.FigureBuilder()
fig.add(spt.Scatter(x=strike_arr,y=theory_value,name='Analytical BS'))
fig.add(spt.Scatter(x=strike_arr,y=fft_price,name='FFT Heston'))
fig.add(spt.Scatter(x=strike_arr,y=fraction_price,name='FractionalFFT Hestion'))
fig.add(spt.Scatter(x=strike_arr,y=cos_price,name='Cosine Hestion'))
fig.add(spt.XAxis('strike'))
fig.add(spt.YAxis('call price'))
fig.update_layout(title='Call Option Prices').plot()


Variance Gamma


In [22]:
ft_pricer.set_log_st_process(VarianceGamma(
    theta=-0.14,
    v=0.2,
    sigma=0.3))


Out[22]:
<fftoptionlib.pricing_class.FourierPricer at 0x7f12eaf77dd8>

FFT Engine


In [23]:
ft_pricer.set_pricing_engine(FFTEngine(N=2**15, d_u=0.01, alpha=1, spline_order=2))
%time
fft_price = ft_pricer.calc_price(strike_arr,put_call)


CPU times: user 5 µs, sys: 3 µs, total: 8 µs
Wall time: 15.5 µs

Fractional FFT Engine


In [24]:
ft_pricer.set_pricing_engine(FractionFFTEngine(N=2**15, d_u=0.01, d_k=0.01,alpha=1, spline_order=2))
%time
fraction_price = ft_pricer.calc_price(strike_arr,put_call)


CPU times: user 6 µs, sys: 0 ns, total: 6 µs
Wall time: 11.9 µs

Cosine Engine


In [25]:
ft_pricer.set_pricing_engine(CosineEngine(N=2000,L=10))
%time
cos_price = ft_pricer.calc_price(strike_arr,put_call)


CPU times: user 3 µs, sys: 2 µs, total: 5 µs
Wall time: 11.4 µs

In [26]:
fig=spt.FigureBuilder()
fig.add(spt.Scatter(x=strike_arr,y=theory_value,name='Analytical BS'))
fig.add(spt.Scatter(x=strike_arr,y=fft_price,name='FFT Heston'))
fig.add(spt.Scatter(x=strike_arr,y=fraction_price,name='FractionalFFT Hestion'))
fig.add(spt.Scatter(x=strike_arr,y=cos_price,name='Cosine Hestion'))
fig.add(spt.XAxis('strike'))
fig.add(spt.YAxis('call price'))
fig.update_layout(title='Call Option Prices').plot()


Conclusion

The above example explore BlackShole, Heston, VaranceGamma with three Engines, FFT, Fractional FFT and Cosine. You can change to other processes easily by calling set_log_st_process.