Skip to content

Commit

Permalink
Update
Browse files Browse the repository at this point in the history
I fixed many problems.
jaehyukchoi committed Apr 6, 2021
1 parent 21ccba8 commit 2d5d92c
Showing 8 changed files with 51 additions and 36 deletions.
2 changes: 1 addition & 1 deletion pyfeng/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from .norm import Norm # the order is sensitive because of `price_barrier` method. Put it before .bsm
from .bsm import Bsm, BsmDisp
from .cev import Cev
from .gamma import Invgam
from .gamma import InvGam
from .sabr import SabrHagan2002, SabrNorm, SabrLorig2017, SabrChoiWu2021H, SabrChoiWu2021P
from .sabr_int import SabrUncorrChoiWu2021
from .nsvh import Nsvh1
8 changes: 3 additions & 5 deletions pyfeng/gamma.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import scipy.stats as spst
import scipy.special as spsp
import numpy as np
from . import opt_abc as opt
from . import opt_smile_abc as smile


class Invgam(smile.OptSmileABC, opt.OptABC):
class InvGam(smile.OptSmileABC):
"""
Option pricing model with the inverse gamma (reciprocal gamma) distribution.
@@ -17,9 +15,9 @@ class Invgam(smile.OptSmileABC, opt.OptABC):
Examples:
>>> import numpy as np
>>> import pyfeng as pf
>>> m = pf.Invgam(sigma=0.2, intr=0.05, divr=0.1)
>>> m = pf.InvGam(sigma=0.2, intr=0.05, divr=0.1)
>>> m.price(np.arange(80, 121, 10), 100, 1.2)
array([21.34327542, 13.99490086, 8.60288219, 5.02287171, 2.82349989])
array([15.49803779, 9.53595458, 5.49889751, 3.02086661, 1.60505654])
"""
sigma = None

2 changes: 1 addition & 1 deletion pyfeng/multiasset.py
Original file line number Diff line number Diff line change
@@ -212,7 +212,7 @@ def price(self, strike, spot, texp, cp=1):
alpha = 1/(m2/m1**2-1) + 2
beta = (alpha-1)*m1

price = gamma.Invgam.price_formula(strike, m1, texp, alpha, beta, cp=cp, is_fwd=True)
price = gamma.InvGam.price_formula(strike, m1, texp, alpha, beta, cp=cp, is_fwd=True)
return df * price


31 changes: 20 additions & 11 deletions pyfeng/multiasset_mc.py
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@ class BsmNdMc(opt.OptMaABC):
>>> payoff = lambda x: np.fmax(np.mean(x,axis=1) - strike, 0) # Basket option
>>> strikes = np.arange(80, 121, 10)
>>> m = pf.BsmNdMc(sigma, cor=0.5, rn_seed=1234)
>>> m.simulate(tobs=[texp], n_path=20000)
>>> m.simulate(n_path=20000, tobs=[texp])
>>> p = []
>>> for strike in strikes:
>>> p.append(m.price_european(spot, texp, payoff))
@@ -26,13 +26,13 @@ class BsmNdMc(opt.OptMaABC):
sigma = np.ones(2)*0.1

# MC params
n_path = 100
n_path = 0
rn_seed = None
rng = None
antithetic = True

# path
path, tobs = None, None
path, tobs = np.array([]), None

def __init__(self, sigma, cor=None, intr=0.0, divr=0.0, rn_seed=None):
self.rn_seed = rn_seed
@@ -74,31 +74,40 @@ def _bm_incr(self, tobs, n_path=None):

return bm_incr

def simulate(self, tobs, n_path=None, store=True):
def simulate(self, n_path=None, tobs=None, store=1):
"""
Simulate the price paths and store in the class.
The initial prices are normalized to 1.
Args:
tobs: array of observation times
n_path: number of paths. If None (default), use the stored one.
store: if True (default), save the result to self.path_stored
store: if 0, do not store, if 1 (default), overwrite the path to self.path, if 2 append to self.path
Returns:
price path (time, path, asset)
"""
# (n_t, n_path, n_asset) * (n_asset, n_asset)
path = self._bm_incr(tobs, n_path)
if store == 1 or (store == 2 and self.n_path == 0):
self.tobs = np.atleast_1d(tobs)
elif store == 2 and tobs is not None:
# make sure that tobs == self.tobs
if not np.all(np.isclose(self.tobs, tobs)):
raise ValueError('tobs is different from the saved value.')

path = self._bm_incr(self.tobs, n_path)
# Add drift and convexity
dt = np.diff(np.atleast_1d(tobs), prepend=0)
dt = np.diff(self.tobs, prepend=0)
path += (self.intr - self.divr - 0.5*self.sigma**2)*dt[:, None, None]
np.cumsum(path, axis=0, out=path)
np.exp(path, out=path)

if store:
if store == 1 or (store == 2 and self.n_path == 0):
self.n_path = n_path
self.path = path
self.tobs = tobs
elif store == 2:
self.n_path += n_path
self.path = np.concatenate((self.path, path), axis=1)

return path

@@ -114,13 +123,13 @@ def price_european(self, spot, texp, payoff):
Returns:
The MC price of the payoff
"""
if self.path is None:
if self.n_path == 0:
raise ValueError('Simulated paths are not available. Run simulate() first.')

# check if texp is in tobs
ind, *_ = np.where(np.isclose(self.tobs, texp))
if len(ind) == 0:
raise ValueError(f'Stored path does not contain t = {texp}')
raise ValueError(f'Stored tobs does not contain t={texp}')

path = self.path[ind[0], ] * spot
price = np.exp(-self.intr * texp) * np.mean(payoff(path), axis=0)
8 changes: 5 additions & 3 deletions pyfeng/nsvh.py
Original file line number Diff line number Diff line change
@@ -21,8 +21,9 @@ class Nsvh1(sabr.SabrABC):
"""

beta = 0.0 # beta is already defined in the parent class, but the default value set as 0
is_atmvol = False

def __init__(self, sigma, vov=0.0, rho=0.0, beta=None, intr=0.0, divr=0.0, is_fwd=False, atmvol=False):
def __init__(self, sigma, vov=0.0, rho=0.0, beta=None, intr=0.0, divr=0.0, is_fwd=False, is_atmvol=False):
"""
Args:
sigma: model volatility at t=0
@@ -32,11 +33,12 @@ def __init__(self, sigma, vov=0.0, rho=0.0, beta=None, intr=0.0, divr=0.0, is_fw
intr: interest rate (domestic interest rate)
divr: dividend/convenience yield (foreign interest rate)
is_fwd: if True, treat `spot` as forward price. False by default.
is_atmvol: If True, use `sigma` as the ATM normal vol
"""
# Make sure beta = 0
if beta is not None and not np.isclose(beta, 0.0):
print(f'Ignoring beta = {beta}...')
self._atmvol = atmvol
self.is_atmvol = is_atmvol
super().__init__(sigma, vov, rho, beta=0, intr=intr, divr=divr, is_fwd=is_fwd)

def _sig0_from_atmvol(self, texp):
@@ -57,7 +59,7 @@ def price(self, strike, spot, texp, cp=1):
fwd, df, _ = self._fwd_factor(spot, texp)

s_sqrt = self.vov * np.sqrt(texp)
if self._atmvol:
if self.is_atmvol:
sig0 = self._sig0_from_atmvol(texp)
else:
sig0 = self.sigma
12 changes: 8 additions & 4 deletions pyfeng/opt_abc.py
Original file line number Diff line number Diff line change
@@ -482,19 +482,23 @@ def __init__(self, sigma, cor=None, intr=0.0, divr=0.0, is_fwd=False):
Args:
sigma: model volatilities of `n_asset` assets. (n_asset, ) array
cor: correlation. If matrix, used as it is. (n_asset, n_asset)
cor: correlation. If matrix with shape (n_asset, n_asset), used as it is.
If scalar, correlation matrix is constructed with all same off-diagonal values.
intr: interest rate (domestic interest rate)
divr: vector of dividend/convenience yield (foreign interest rate) 0-D or (n_asset, ) array
is_fwd: if True, treat `spot` as forward price. False by default.
"""
sigma = np.array(sigma)
sigma = np.atleast_1d(sigma)
self.n_asset = len(sigma)

super().__init__(sigma, intr, divr, is_fwd)
assert self.n_asset > 1 # if single asset, use BSM

if np.isscalar(cor):
if self.n_asset == 1:
if cor is not None:
print(f'Ignoring cor={cor} for a single asset')
self.rho = None
self.cor_m = np.array([[1.0]])
elif np.isscalar(cor):
self.cor_m = cor * np.ones((self.n_asset, self.n_asset)) + (1 - cor) * np.eye(self.n_asset)
self.rho = cor
else:
20 changes: 11 additions & 9 deletions pyfeng/sabr.py
Original file line number Diff line number Diff line change
@@ -220,7 +220,7 @@ def price(self, strike, spot, texp, cp=1):
def impvol(self, price, strike, spot, texp, cp=1, setval=False):
model = copy.copy(self)

vol = self._m_base().impvol(price, strike, spot, texp, cp=cp)
vol = self._m_base(None).impvol(price, strike, spot, texp, cp=cp)

def iv_func(_sigma):
model.sigma = _sigma
@@ -294,7 +294,7 @@ def vol_for_price(self, strike, spot, texp):
vol *= alpha*hh/pre1 # bsm vol
return vol

def calibrate3(self, price_or_vol3, strike3, spot, texp=None, cp=1, setval=False, is_vol=True):
def calibrate3(self, price_or_vol3, strike3, spot, texp, cp=1, setval=False, is_vol=True):
"""
Given option prices or normal vols at 3 strikes, compute the sigma, vov, rho to fit the data
If prices are given (is_vol=False) convert the prices to vol first.
@@ -307,15 +307,16 @@ def calibrate3(self, price_or_vol3, strike3, spot, texp=None, cp=1, setval=False
if is_vol:
vol3 = price_or_vol3
else:
vol3 = self._m_base().impvol(price_or_vol3, strike3, spot, texp)
vol3 = self._m_base(None).impvol(price_or_vol3, strike3, spot, texp, cp=cp)

def iv_func(x):
model.sigma = np.exp(x[0])
model.vov = np.exp(x[1])
model.rho = np.tanh(x[2])
return model.vol_for_price(strike3, spot, texp=texp) - vol3
err = model.vol_for_price(strike3, spot, texp=texp) - vol3
return err

sol = spop.root(iv_func, np.array([vol3[1], -1, 0.0]))
sol = spop.root(iv_func, np.array([np.log(vol3[1]), -1, 0.0]))
params = {"sigma": np.exp(sol.x[0]), "vov": np.exp(sol.x[1]), "rho": np.tanh(sol.x[2])}

if setval:
@@ -338,9 +339,9 @@ class SabrNorm(SabrVolApproxABC):
array([22.04862858, 14.56226187, 8.74170415, 4.72352155, 2.28891776])
"""
_base_beta = 0 # should not be changed
_atmvol = False
is_atmvol = False

def __init__(self, sigma, vov=0.0, rho=0.0, beta=None, intr=0.0, divr=0.0, is_fwd=False, atmvol=False):
def __init__(self, sigma, vov=0.0, rho=0.0, beta=None, intr=0.0, divr=0.0, is_fwd=False, is_atmvol=False):
"""
Args:
sigma: model volatility at t=0
@@ -350,17 +351,18 @@ def __init__(self, sigma, vov=0.0, rho=0.0, beta=None, intr=0.0, divr=0.0, is_fw
intr: interest rate (domestic interest rate)
divr: dividend/convenience yield (foreign interest rate)
is_fwd: if True, treat `spot` as forward price. False by default.
is_atmvol: If True, use `sigma` as the ATM normal vol
"""
# Make sure beta = 0
if beta is not None and not np.isclose(beta, 0.0):
print(f'Ignoring beta = {beta}...')
self._atmvol = atmvol
self.is_atmvol = is_atmvol
super().__init__(sigma, vov, rho, beta=0, intr=intr, divr=divr, is_fwd=is_fwd)

def vol_for_price(self, strike, spot, texp):
fwd = self.forward(spot, texp)

if self.approx_order == 0 or self._atmvol:
if self.approx_order == 0 or self.is_atmvol:
vol = 1.0
else:
vol = 1.0 + texp*(2 - 3*self.rho**2)/24 * self.vov**2
4 changes: 2 additions & 2 deletions tests/test_sabr.py
Original file line number Diff line number Diff line change
@@ -29,12 +29,12 @@ def test_SabrNorm(self):
def test_SabrNormATM(self):
for k in [22, 23]:
m, df, rv = pf.SabrNorm.init_benchmark(k)
m._atmvol = True
m.is_atmvol = True
np.testing.assert_almost_equal(m.vol_smile(0, 0, texp=0.1), m.sigma)
np.testing.assert_almost_equal(m.vol_smile(0, 0, texp=10), m.sigma)

m, df, rv = pf.Nsvh1.init_benchmark(k)
m._atmvol = True
m.is_atmvol = True
np.testing.assert_almost_equal(m.vol_smile(0, 0, texp=0.1), m.sigma)
np.testing.assert_almost_equal(m.vol_smile(0, 0, texp=10), m.sigma)

0 comments on commit 2d5d92c

Please sign in to comment.