Skip to content

Commit 27897e2

Browse files
authored
Merge pull request #13 from GaetanoCodes/BGIG
Adding the BGIG model
2 parents 52f15b9 + 5e8052d commit 27897e2

File tree

3 files changed

+318
-27
lines changed

3 files changed

+318
-27
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
*.idea
66
*.cache
77

8+
#temp develop
9+
temp_analyse/
10+
811
# Byte-compiled / optimized / DLL files
912
__pycache__/
1013
*.py[cod]

fypy/model/levy/BGIG.py

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
"""Implementation of the BGIG model for PROJ framework"""
2+
3+
from typing import List, Tuple, Optional, Union
4+
5+
import numpy as np
6+
import scipy
7+
8+
from fypy.model.levy.LevyModel import LevyModel
9+
from fypy.model.FourierModel import Cumulants
10+
from fypy.termstructures.ForwardCurve import ForwardCurve
11+
from fypy.termstructures.DiscountCurve import DiscountCurve
12+
13+
14+
class BGIG(LevyModel):
15+
"""
16+
Implementation of the BGIG model as introduced in:
17+
18+
The bilateral generalized inverse Gaussian process with applications
19+
to financial modeling, G. AGAZZOTTI, JP. Aguilar
20+
"""
21+
22+
def __init__(
23+
self,
24+
forwardCurve: ForwardCurve,
25+
discountCurve: DiscountCurve,
26+
a_p: float = 500,
27+
b_p: float = 0.05,
28+
p_p: float = 2,
29+
a_m: float = 300,
30+
b_m: float = 0.03,
31+
p_m: float = 2,
32+
):
33+
"""
34+
BGIG model
35+
36+
Args:
37+
forwardCurve (ForwardCurve): fwd
38+
discountCurve (DiscountCurve): discount
39+
a_p (float, optional): Defaults to 500.
40+
b_p (float, optional): Defaults to 0.05.
41+
p_p (float, optional): Defaults to 2.
42+
a_m (float, optional): Defaults to 300.
43+
b_m (float, optional): Defaults to 0.03.
44+
p_m (float, optional): Defaults to 2.
45+
"""
46+
super().__init__(
47+
forwardCurve=forwardCurve,
48+
discountCurve=discountCurve,
49+
params=np.asarray([a_p, b_p, p_p, a_m, b_m, p_m]),
50+
)
51+
52+
####################################
53+
####### MODEL PARAMETERS ##########
54+
####################################
55+
56+
@property
57+
def a_p(self) -> float:
58+
"""Model Parameter"""
59+
return self._params[0]
60+
61+
@property
62+
def b_p(self) -> float:
63+
"""Model Parameter"""
64+
return self._params[1]
65+
66+
@property
67+
def p_p(self) -> float:
68+
"""Model Parameter"""
69+
return self._params[2]
70+
71+
@property
72+
def a_m(self) -> float:
73+
"""Model Parameter"""
74+
return self._params[3]
75+
76+
@property
77+
def b_m(self) -> float:
78+
"""Model Parameter"""
79+
return self._params[4]
80+
81+
@property
82+
def p_m(self) -> float:
83+
"""Model Parameter"""
84+
return self._params[5]
85+
86+
####################################
87+
####### CUMULANTS HELPERS ##########
88+
####################################
89+
90+
def ratio_bessel(self, omega: float, p: float) -> float:
91+
"""
92+
ratio of bessel function
93+
94+
Args:
95+
omega (float): omega
96+
p (float): p params
97+
98+
Returns:
99+
float: ratio of bessel
100+
"""
101+
return scipy.special.kv(p + 1, omega) / scipy.special.kv(p, omega)
102+
103+
def c1(self, omega: float, eta: float, p: float) -> float:
104+
"""
105+
cumulants of order 1 of a one sided BIG distribution
106+
107+
Args:
108+
omega (float): omega
109+
eta (float): eta
110+
p (float): p
111+
112+
Returns:
113+
float: c1
114+
"""
115+
return self.ratio_bessel(omega, p) * eta
116+
117+
def c2(self, omega: float, eta: float, p: float) -> float:
118+
"""
119+
cumulants of order 2 of a one sided BIG distribution
120+
121+
Args:
122+
omega (float): omega
123+
eta (float): eta
124+
p (float): p
125+
126+
Returns:
127+
float: c2
128+
"""
129+
polynom = (
130+
-(self.ratio_bessel(omega, p) ** 2)
131+
+ (2 * (p + 1) / omega) * self.ratio_bessel(omega, p)
132+
+ 1
133+
)
134+
return polynom * eta**2
135+
136+
def c3(self, omega: float, eta: float, p: float):
137+
"""
138+
cumulants of order 3 of a one sided BIG distribution
139+
140+
Args:
141+
omega (float): omega
142+
eta (float): eta
143+
p (float): p
144+
145+
Returns:
146+
float: c3
147+
"""
148+
polynom = (
149+
2 * self.ratio_bessel(omega, p) ** 3
150+
- (6 * (p + 1) / omega) * self.ratio_bessel(omega, p) ** 2
151+
+ ((4 * (p + 1) * (p + 2) / omega**2) - 2) * self.ratio_bessel(omega, p)
152+
+ 2 * (p + 1) / omega
153+
)
154+
return polynom * eta**3
155+
156+
def c4(self, omega: float, eta: float, p: float):
157+
"""
158+
cumulants of order 4 of a one sided BIG distribution
159+
160+
Args:
161+
omega (float): omega
162+
eta (float): eta
163+
p (float): p
164+
165+
Returns:
166+
float: c4
167+
"""
168+
polynom = (
169+
2 * self.ratio_bessel(omega, p) ** 3
170+
- (6 * (p + 1) / omega) * self.ratio_bessel(omega, p) ** 2
171+
+ ((4 * (p + 1) * (p + 2) / omega**2) - 2) * self.ratio_bessel(omega, p)
172+
+ 2 * (p + 1) / omega
173+
)
174+
return polynom * eta**4
175+
176+
def cumulants_gen(
177+
self,
178+
order: int,
179+
) -> float:
180+
"""
181+
compute cumulants of order "order"
182+
183+
Args:
184+
order (int): order of the cumulant
185+
186+
Raises:
187+
NotImplementedError: if order > 4
188+
189+
Returns:
190+
cumulant (float)
191+
"""
192+
a_p, b_p, p_p = self.a_p, self.b_p, self.p_p
193+
a_m, b_m, p_m = self.a_m, self.b_m, self.p_m
194+
195+
omega_p = (a_p * b_p) ** 0.5
196+
omega_m = (a_m * b_m) ** 0.5
197+
eta_p = (a_p / b_p) ** (-0.5)
198+
eta_m = (a_m / b_m) ** (-0.5)
199+
200+
match order:
201+
case 1:
202+
return self.c1(omega_p, eta_p, p_p) - self.c1(omega_m, eta_m, p_m)
203+
case 2:
204+
return self.c2(omega_p, eta_p, p_p) + self.c2(omega_m, eta_m, p_m)
205+
case 3:
206+
return self.c3(omega_p, eta_p, p_p) - self.c3(omega_m, eta_m, p_m)
207+
case 4:
208+
return self.c4(omega_p, eta_p, p_p) + self.c4(omega_m, eta_m, p_m)
209+
case _:
210+
raise NotImplementedError
211+
212+
def cumulants(self, T: float) -> Cumulants:
213+
"""
214+
Evaluate the cumulants of the model at a given time.
215+
This is useful e.g. to figure out integration bounds etc
216+
during pricing
217+
:param T: float, time to maturity (time at which cumulants are evaluated)
218+
:return: Cumulants object
219+
"""
220+
rn_drift = self.risk_neutral_log_drift()
221+
222+
return Cumulants(
223+
T=T,
224+
rn_drift=rn_drift,
225+
c1=T * (rn_drift + self.cumulants_gen(1)),
226+
c2=T * self.cumulants_gen(2),
227+
c4=T * self.cumulants_gen(4),
228+
)
229+
230+
def symbol(self, xi: Union[float, np.ndarray]):
231+
"""
232+
Levy symbol, uniquely defines Characteristic Function via:
233+
chf(T,xi) = exp(T*symbol(xi)), for all T>=0
234+
:param xi: np.ndarray or float, points in frequency domain
235+
:return: np.ndarray or float, symbol evaluated at input points in frequency domain
236+
"""
237+
a_p, b_p, p_p = self.a_p, self.b_p, self.p_p
238+
a_m, b_m, p_m = self.a_m, self.b_m, self.p_m
239+
rn_drift = self.risk_neutral_log_drift()
240+
241+
return 1j * xi * rn_drift + np.log(
242+
(a_p / (a_p - 2j * xi)) ** (p_p / 2)
243+
* scipy.special.kv(p_p, (b_p * (a_p - 2j * xi)))
244+
/ scipy.special.kv(p_p, (b_p * a_p))
245+
* (a_m / (a_m + 2j * xi)) ** (p_m / 2)
246+
* scipy.special.kv(p_m, (b_m * (a_m - 2j * xi)))
247+
/ scipy.special.kv(p_m, (b_m * a_m))
248+
)
249+
250+
def convexity_correction(self) -> float:
251+
"""
252+
Computes the convexity correction for the Levy model,
253+
added to log process drift to ensure
254+
risk neutrality
255+
"""
256+
a_p, b_p, p_p = self.a_p, self.b_p, self.p_p
257+
a_m, b_m, p_m = self.a_m, self.b_m, self.p_m
258+
259+
return -np.log(
260+
(a_p / (a_p - 2)) ** (p_p / 2)
261+
* scipy.special.kv(p_p, (b_p * (a_p - 2)))
262+
/ scipy.special.kv(p_p, (b_p * a_p))
263+
* (a_m / (a_m + 2)) ** (p_m / 2)
264+
* scipy.special.kv(p_m, (b_m * (a_m + 2)))
265+
/ scipy.special.kv(p_m, (b_m * a_m))
266+
)
267+
268+
#############################################
269+
### Calibration Interface Implementation ###
270+
#############################################
271+
272+
def num_params(self) -> int:
273+
return 6
274+
275+
def param_bounds(self) -> Optional[List[Tuple]]:
276+
return [
277+
(2, np.inf),
278+
(0, np.inf),
279+
(-np.inf, np.inf),
280+
(0, np.inf),
281+
(0, np.inf),
282+
(-np.inf, np.inf),
283+
]
284+
285+
def default_params(self) -> Optional[np.ndarray]:
286+
return np.asarray([500, 0.05, 2, 300, 0.03, 2])

0 commit comments

Comments
 (0)