Skip to content

Commit 6c67ca8

Browse files
authored
Merge pull request #11 from PyFE/dev-0.1.3
Dev 0.1.3
2 parents 26d614b + 53568e0 commit 6c67ca8

File tree

12 files changed

+680
-87
lines changed

12 files changed

+680
-87
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
.DS_Store
2+
*_pyfe.py
23

34
# Created by https://www.toptal.com/developers/gitignore/api/python,pycharm
45
# Edit at https://www.toptal.com/developers/gitignore?templates=python,pycharm

README.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ pip install --upgrade pyfeng
2929
import numpy as np
3030
import pyfeng as pf
3131
m = pf.Bsm(sigma=0.2, intr=0.05, divr=0.1)
32-
m.price(np.arange(80, 121, 10), 100, 1.2)
32+
m.price(strike=np.arange(80, 121, 10), spot=100, texp=1.2)
3333
```
3434
`Out [1]:`
3535
```
@@ -38,15 +38,14 @@ array([15.71361973, 9.69250803, 5.52948546, 2.94558338, 1.48139131])
3838

3939
`In [2]:`
4040
```python
41-
sigma = np.array([0.2, 0.3, 0.5])[:, None]
41+
sigma = np.array([0.2], [0.5])
4242
m = pf.Bsm(sigma, intr=0.05, divr=0.1) # sigma in axis=0
43-
m.price(np.array([90, 100, 110]), 100, 1.2, cp=np.array([-1,1,1]))
43+
m.price(strike=np.array([90, 95, 100]), spot=100, texp=1.2, cp=np.array([-1,1,1]))
4444
```
4545
`Out [2]:`
4646
```
47-
array([[ 5.75927238, 5.52948546, 2.94558338],
48-
[ 9.4592961 , 9.3881245 , 6.45745004],
49-
[16.812035 , 17.10541288, 14.10354768]])
47+
array([[ 5.75927238, 7.38869609, 5.52948546],
48+
[16.812035 , 18.83878533, 17.10541288]])
5049
```
5150

5251
## Author

pyfeng/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,6 @@
22
from .bsm import Bsm, BsmDisp
33
from .cev import Cev
44
from .sabr import SabrHagan2002, SabrLorig2017, SabrChoiWu2021H, SabrChoiWu2021P
5+
from .sabr_int import SabrUncorrChoiWu2021
56
from .nsvh import Nsvh1
7+
from .multiasset import BsmSpreadKirk, BsmSpreadBjerksund2014, NormBasket, NormSpread, BsmBasketLevy1992, BsmMax2

pyfeng/cev.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from . import opt_smile_abc as smile
66

77

8-
class Cev(opt.OptAnalyticABC, smile.OptSmileABC):
8+
class Cev(opt.OptAnalyticABC, smile.OptSmileABC, smile.MassZeroABC):
99
"""
1010
Constant Elasticity of Variance (CEV) model.
1111
@@ -41,18 +41,7 @@ def params_kw(self):
4141
return {**params, **extra} # Py 3.9, params | extra
4242

4343
def mass_zero(self, spot, texp, log=False):
44-
"""
45-
Probability mass at zero (absorbing boundary)
46-
47-
Args:
48-
spot: spot or forward
49-
texp: time to expiry
50-
log: log value if True
51-
52-
Returns:
53-
mass at zero.
54-
"""
55-
fwd, _, _ = self._fwd_factor(spot, texp)
44+
fwd = self.forward(spot, texp)
5645

5746
betac = 1.0 - self.beta
5847
a = 0.5 / betac
@@ -68,6 +57,22 @@ def mass_zero(self, spot, texp, log=False):
6857
else:
6958
return spst.gamma.sf(x=x, a=a)
7059

60+
def mass_zero_t0(self, spot, texp):
61+
"""
62+
Limit value of -T log(M_T) as T -> 0, where M_T is the mass at zero.
63+
64+
Args:
65+
spot: spot (or forward) price
66+
67+
Returns:
68+
- lim_{T->0} T log(M_T)
69+
"""
70+
fwd = self.forward(spot, texp)
71+
betac = 1.0 - self.beta
72+
alpha = self.sigma/np.power(fwd, betac)
73+
t0 = 0.5/(betac*alpha)**2
74+
return t0
75+
7176
@staticmethod
7277
def price_formula(strike, spot, texp, cp=1, sigma=None, beta=0.5, intr=0.0, divr=0.0, is_fwd=False):
7378
"""

pyfeng/multiasset.py

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
import scipy.stats as scst
2+
import numpy as np
3+
from . import bsm
4+
from . import norm
5+
from . import opt_abc as opt
6+
7+
8+
class BsmSpreadKirk(opt.OptMaABC):
9+
"""
10+
Kirk's approximation for spread option.
11+
12+
References:
13+
Kirk, E. (1995). Correlation in the energy markets. In Managing Energy Price Risk
14+
(First, pp. 71–78). Risk Publications.
15+
16+
Examples:
17+
>>> import numpy as np
18+
>>> import pyfeng as pf
19+
>>> m = pf.BsmSpreadKirk((0.2, 0.3), cor=-0.5)
20+
>>> m.price(np.arange(-2, 3) * 10, [100, 120], 1.3)
21+
array([22.15632247, 17.18441817, 12.98974214, 9.64141666, 6.99942072])
22+
"""
23+
24+
weight = np.array([1, -1])
25+
26+
def price(self, strike, spot, texp, cp=1):
27+
df = np.exp(-texp * self.intr)
28+
fwd = np.array(spot) * (1.0 if self.is_fwd else np.exp(-texp * np.array(self.divr)) / df)
29+
assert fwd.shape[-1] == self.n_asset
30+
31+
fwd1 = fwd[..., 0] - np.minimum(strike, 0)
32+
fwd2 = fwd[..., 1] + np.maximum(strike, 0)
33+
34+
sig1 = self.sigma[0] * fwd[..., 0] / fwd1
35+
sig2 = self.sigma[1] * fwd[..., 1] / fwd2
36+
sig_spd = np.sqrt(sig1*(sig1 - 2.0*self.rho*sig2) + sig2**2)
37+
price = bsm.Bsm.price_formula(fwd2, fwd1, sig_spd, texp, cp=cp, is_fwd=True)
38+
return df * price
39+
40+
41+
class BsmSpreadBjerksund2014(opt.OptMaABC):
42+
"""
43+
Bjerksund & Stensland (2014)'s approximation for spread option.
44+
45+
References:
46+
Bjerksund, P., & Stensland, G. (2014). Closed form spread option valuation.
47+
Quantitative Finance, 14(10), 1785–1794. https://doi.org/10.1080/14697688.2011.617775
48+
49+
Examples:
50+
>>> import numpy as np
51+
>>> import pyfeng as pf
52+
>>> m = pf.BsmSpreadBjerksund2014((0.2, 0.3), cor=-0.5)
53+
>>> m.price(np.arange(-2, 3) * 10, [100, 120], 1.3)
54+
array([22.13172022, 17.18304247, 12.98974214, 9.54431944, 6.80612597])
55+
"""
56+
57+
weight = np.array([1, -1])
58+
59+
def price(self, strike, spot, texp, cp=1):
60+
df = np.exp(-texp * self.intr)
61+
fwd = np.array(spot) * (1.0 if self.is_fwd else np.exp(-texp * np.array(self.divr)) / df)
62+
assert fwd.shape[-1] == self.n_asset
63+
64+
fwd1 = fwd[..., 0]
65+
fwd2 = fwd[..., 1]
66+
67+
std11 = self.sigma[0]**2 * texp
68+
std12 = self.sigma[0]*self.sigma[1] * texp
69+
std22 = self.sigma[1]**2 * texp
70+
71+
aa = fwd2 + strike
72+
bb = fwd2/aa
73+
std = np.sqrt(std11 - 2*bb*self.rho*std12 + bb**2*std22)
74+
75+
d3 = np.log(fwd1/aa)
76+
d1 = (d3 + 0.5*std11 - bb*(self.rho*std12 - 0.5*bb*std22)) / std
77+
d2 = (d3 - 0.5*std11 + self.rho*std12 + bb*(0.5*bb - 1)*std22) / std
78+
d3 = (d3 - 0.5*std11 + 0.5*bb**2*std22) / std
79+
80+
price = cp*(fwd1*scst.norm.cdf(cp*d1) - fwd2*scst.norm.cdf(cp*d2)
81+
- strike*scst.norm.cdf(cp*d3))
82+
83+
return df * price
84+
85+
86+
class NormBasket(opt.OptMaABC):
87+
"""
88+
Basket option pricing under the multiasset Bachelier model
89+
"""
90+
91+
weight = None
92+
93+
def __init__(self, sigma, cor=None, weight=None, intr=0.0, divr=0.0, is_fwd=False):
94+
"""
95+
Args:
96+
sigma: model volatilities of `n_asset` assets. (n_asset, ) array
97+
cor: correlation. If matrix, used as it is. (n_asset, n_asset)
98+
If scalar, correlation matrix is constructed with all same off-diagonal values.
99+
weight: asset weights, If None, equally weighted as 1/n_asset
100+
If scalar, equal weights of the value
101+
If 1-D array, uses as it is. (n_asset, )
102+
intr: interest rate (domestic interest rate)
103+
divr: vector of dividend/convenience yield (foreign interest rate) 0-D or (n_asset, ) array
104+
is_fwd: if True, treat `spot` as forward price. False by default.
105+
"""
106+
107+
super().__init__(sigma, cor=cor, intr=intr, divr=divr, is_fwd=is_fwd)
108+
if weight is None:
109+
self.weight = np.ones(self.n_asset) / self.n_asset
110+
elif np.isscalar(weight):
111+
self.weight = np.ones(self.n_asset) * weight
112+
else:
113+
assert len(weight) == self.n_asset
114+
self.weight = np.array(weight)
115+
116+
def price(self, strike, spot, texp, cp=1):
117+
df = np.exp(-texp * self.intr)
118+
fwd = np.array(spot) * (1.0 if self.is_fwd else np.exp(-texp * np.array(self.divr)) / df)
119+
assert fwd.shape[-1] == self.n_asset
120+
121+
fwd_basket = fwd @ self.weight
122+
vol_basket = np.sqrt(self.weight @ self.cov_m @ self.weight)
123+
124+
price = norm.Norm.price_formula(
125+
strike, fwd_basket, vol_basket, texp, cp=cp, is_fwd=True)
126+
return df * price
127+
128+
129+
class NormSpread(opt.OptMaABC):
130+
"""
131+
Spread option pricing under the Bachelier model.
132+
This is a special case of NormBasket with weight = (1, -1)
133+
134+
Examples:
135+
>>> import numpy as np
136+
>>> import pyfeng as pf
137+
>>> m = pf.NormSpread((20, 30), cor=-0.5, intr=0.05)
138+
>>> m.price(np.arange(-2, 3) * 10, [100, 120], 1.3)
139+
array([17.95676186, 13.74646821, 10.26669936, 7.47098719, 5.29057157])
140+
"""
141+
weight = np.array([1, -1])
142+
143+
price = NormBasket.price
144+
145+
146+
class BsmBasketLevy1992(NormBasket):
147+
"""
148+
Basket option pricing with the log-normal approximation of Levy & Turnbull (1992)
149+
150+
References:
151+
Levy, E., & Turnbull, S. (1992). Average intelligence. Risk, 1992(2), 53–57.
152+
153+
Krekel, M., de Kock, J., Korn, R., & Man, T.-K. (2004). An analysis of pricing methods for basket options.
154+
Wilmott Magazine, 2004(7), 82–89.
155+
156+
Examples:
157+
>>> import numpy as np
158+
>>> import pyfeng as pf
159+
>>> strike = np.arange(50, 151, 10)
160+
>>> m = pf.BsmBasketLevy1992(sigma=0.4*np.ones(4), cor=0.5)
161+
>>> m.price(strike, spot=100*np.ones(4), texp=5)
162+
array([54.34281026, 47.521086 , 41.56701301, 36.3982413 , 31.92312156,
163+
28.05196621, 24.70229571, 21.800801 , 19.28360474, 17.09570196,
164+
15.19005654])
165+
"""
166+
def price(self, strike, spot, texp, cp=1):
167+
df = np.exp(-texp * self.intr)
168+
fwd = np.array(spot) * (1.0 if self.is_fwd else np.exp(-texp * np.array(self.divr)) / df)
169+
assert fwd.shape[-1] == self.n_asset
170+
171+
fwd_basket = fwd * self.weight
172+
m1 = np.sum(fwd_basket, axis=-1)
173+
m2 = np.sum(fwd_basket @ np.exp(self.cov_m * texp) * fwd_basket, axis=-1)
174+
175+
sig = np.sqrt(np.log(m2/(m1**2))/texp)
176+
price = bsm.Bsm.price_formula(
177+
strike, m1, sig, texp, cp=cp, is_fwd=True)
178+
return df * price
179+
180+
181+
class BsmMax2(opt.OptMaABC):
182+
"""
183+
Option on the max of two assets.
184+
Payout = max( max(F_1, F_2) - K, 0 ) for all or max( K - max(F_1, F_2), 0 ) for put option
185+
186+
References:
187+
Rubinstein, M. (1991). Somewhere Over the Rainbow. Risk, 1991(11), 63–66.
188+
189+
Examples:
190+
>>> import numpy as np
191+
>>> import pyfeng as pf
192+
>>> m = pf.BsmMax2(0.2*np.ones(2), cor=0, divr=0.1, intr=0.05)
193+
>>> m.price(strike=[90, 100, 110], spot=100*np.ones(2), texp=3)
194+
array([15.86717049, 11.19568103, 7.71592217])
195+
"""
196+
197+
m_switch = None
198+
199+
def __init__(self, sigma, cor=None, weight=None, intr=0.0, divr=0.0, is_fwd=False):
200+
super().__init__(sigma, cor=cor, intr=intr, divr=divr, is_fwd=is_fwd)
201+
self.m_switch = BsmSpreadKirk(sigma, cor, is_fwd=True)
202+
203+
def price(self, strike, spot, texp, cp=1):
204+
sig = self.sigma
205+
df = np.exp(-texp * self.intr)
206+
fwd = np.array(spot) * (1.0 if self.is_fwd else np.exp(-texp * np.array(self.divr)) / df)
207+
assert fwd.shape[-1] == self.n_asset
208+
209+
sig_std = sig * np.sqrt(texp)
210+
spd_rho = np.sqrt(np.dot(sig, sig) - 2*self.rho*sig[0]*sig[1])
211+
spd_std = spd_rho * np.sqrt(texp)
212+
213+
# -x and y as rows
214+
# supposed to be -log(fwd/strike) but strike is added later
215+
xx = -np.log(fwd)/sig_std - 0.5*sig_std
216+
217+
fwd_ratio = fwd[0]/fwd[1]
218+
yy = np.log([fwd_ratio, 1/fwd_ratio])/spd_std + 0.5*spd_std
219+
220+
rho12 = np.array([self.rho*sig[1]-sig[0], self.rho*sig[0]-sig[1]])/spd_rho
221+
222+
mu0 = np.zeros(2)
223+
cor_m1 = rho12[0] + (1-rho12[0])*np.eye(2)
224+
cor_m2 = rho12[1] + (1-rho12[1])*np.eye(2)
225+
226+
strike_isscalar = np.isscalar(strike)
227+
strike = np.atleast_1d(strike)
228+
cp = cp * np.ones_like(strike)
229+
n_strike = len(strike)
230+
231+
# this is the price of max(S1, S2) = max(S1-S2, 0) + S2
232+
# Used that Kirk approximation strike = 0 is Margrabe's switch option price
233+
parity = 0 if np.all(cp > 0) else self.m_switch.price(0, fwd, texp) + fwd[1]
234+
price = np.zeros_like(strike, float)
235+
for k in range(n_strike):
236+
xx_ = xx + np.log(strike[k])/sig_std
237+
term1 = fwd[0] * (scst.norm.cdf(yy[0]) - scst.multivariate_normal.cdf(np.array([xx_[0], yy[0]]), mu0, cor_m1))
238+
term2 = fwd[1] * (scst.norm.cdf(yy[1]) - scst.multivariate_normal.cdf(np.array([xx_[1], yy[1]]), mu0, cor_m2))
239+
term3 = strike[k] * np.array(scst.multivariate_normal.cdf(xx_ + sig_std, mu0, self.cor_m))
240+
241+
assert(term1 + term2 + term3 >= strike[k])
242+
price[k] = (term1 + term2 + term3 - strike[k])
243+
244+
if cp[k] < 0:
245+
price[k] += strike[k] - parity
246+
price *= df
247+
248+
return price[0] if strike_isscalar else price

0 commit comments

Comments
 (0)