Skip to content

Commit 4ac7587

Browse files
Test modifications following the last push and initial changes for multisection
1 parent 52f15b9 commit 4ac7587

File tree

7 files changed

+263
-191
lines changed

7 files changed

+263
-191
lines changed

fypy/model/FourierModel.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,11 @@ def chf(self, T: float, xi: Union[float, np.ndarray]) -> Union[float, np.ndarray
101101
"""
102102
raise NotImplementedError
103103

104+
def inhomogeneous_chf(self, T: np.ndarray, xi: Union[float, np.ndarray], thetas: list[np.ndarray]) -> Union[float, np.ndarray]:
105+
"""
106+
Time-inhomogeneous characteristic function
107+
:param T: float, time to maturity
108+
:param xi: np.ndarray or float, points in frequency domain
109+
"""
110+
raise NotImplementedError
111+

fypy/model/levy/LevyModel.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
from fypy.termstructures.ForwardCurve import ForwardCurve
33
from fypy.termstructures.DiscountCurve import DiscountCurve
44
from fypy.model.FourierModel import FourierModel
5-
from typing import Union
5+
from typing import Union, Optional, Dict
66
import numpy as np
7+
from contextlib import contextmanager
78

89

910
class LevyModel(FourierModel, ABC):
@@ -41,6 +42,8 @@ def symbol(self, xi: Union[float, np.ndarray]):
4142
"""
4243
raise NotImplementedError
4344

45+
46+
4447
@abstractmethod
4548
def convexity_correction(self) -> float:
4649
"""
@@ -59,3 +62,56 @@ def set_params(self, params: np.ndarray):
5962

6063
def get_params(self) -> np.ndarray:
6164
return self._params
65+
66+
67+
@contextmanager
68+
def temporary_params(self, new_params: np.ndarray):
69+
"""
70+
This function temporarily sets the model's parameters (`_params`) to `new_params` for the duration of the
71+
context, and then automatically restores the original parameters after exiting the context.
72+
This method could be used for inhomogeneous Levy models, where the model's parameters must be changed frequently.
73+
74+
:param new_params: np.ndarray, new set of parameters to use temporarily.
75+
"""
76+
# Store the original parameters before modifying them
77+
original_params = self.get_params()
78+
try:
79+
# Set the new parameters
80+
self.set_params(new_params)
81+
yield
82+
finally:
83+
# Restore the original parameters after the context is done
84+
self.set_params(original_params)
85+
86+
def frozen_chf(self, xi: float, frozen_params: Dict[float,list]) -> complex:
87+
"""
88+
:param xi: np.ndarray or float, points in the frequency domain
89+
:param thetas: list of np.ndarray, each array represents a set of parameters
90+
:param T: np.ndarray, array of time points T_j corresponding to each set of parameters.
91+
:return: np.ndarray or float, the frozen characteristic function.
92+
"""
93+
frozen_params = dict(sorted(frozen_params.items()))
94+
95+
frozen_factor = 1.0
96+
T_previous = 0
97+
98+
for T, params in frozen_params.items():
99+
delta_T = T - T_previous
100+
T_previous = T
101+
102+
with self.temporary_params(params):
103+
frozen_factor *= np.exp(delta_T * self.symbol(xi))
104+
105+
return frozen_factor
106+
107+
def inhomogeneous_chf(self, T: float, xi: Union[float, np.ndarray], frozen_params: Dict[float, list] = None) -> complex:
108+
"""
109+
Time-inhomogeneous characteristic function
110+
:param T: float, time to maturity
111+
:param xi: np.ndarray or float, points in frequency domain
112+
:param frozen_params: optional dict, parameters for the frozen characteristic function
113+
"""
114+
115+
frozen_params = frozen_params or {}
116+
117+
return self.chf(T=T, xi=xi) * self.frozen_chf(xi=xi, frozen_params=frozen_params) if frozen_params else self.chf( T=T, xi=xi)

fypy/pricing/fourier/ProjEuropeanPricer.py

Lines changed: 62 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -4,121 +4,92 @@
44
from fypy.model.levy.LevyModel import FourierModel
55
from fypy.pricing.fourier.ProjPricer import ProjPricer, Impl, CubicImpl, LinearImpl, HaarImpl
66

7-
87
class ProjEuropeanPricer(ProjPricer):
98
def __init__(self, model: FourierModel, N: int = 2 ** 9, L: float = 10., order: int = 3,
109
alpha_override: float = np.nan):
11-
"""
12-
Price European options using the Frame Projection (PROJ) method of Kirkby (2015)
13-
14-
Ref: JL Kirkby, SIAM Journal on Financial Mathematics, 6 (1), 713-747
15-
16-
:param model: Fourier model
17-
:param N: int (power of 2), number of basis coefficients (increase to increase accuracy)
18-
:param L: float, controls gridwidth of density. A value of L = 10~14 works well... For Black-Scholes,
19-
L = 6 is fine, for heavy tailed processes such as CGMY, may want a larger value to get very high accuracy
20-
:param order: int, the Spline order: 0 = Haar, 1 = Linear, 2 = Quadratic, 3 = Cubic
21-
Note: Cubic is preferred, the others are provided for research purposes. Only 1 and 3 are currently coded
22-
:param alpha_override: float, if supplied, this overrides the rule using L to determine the gridwidth,
23-
allows you to use your own rule to set grid if desired
24-
"""
2510
super().__init__(model, N, L, order, alpha_override)
26-
2711
self._efficient_multi_strike = [1]
2812

2913
if order not in (0, 1, 3):
3014
raise NotImplementedError("Only cubic, linear and Haar implemented so far")
3115

32-
def price_strikes_fill(self,
33-
T: float,
34-
K: np.ndarray,
35-
is_calls: np.ndarray,
36-
output: np.ndarray):
16+
def price_strikes_fill(self, T: float, K: np.ndarray, is_calls: np.ndarray, output: np.ndarray):
3717
"""
38-
Price a set of set of strikes (at same time to maturity, ie one slice of a surface)
39-
Override this method if given a more efficient implementation for multiple strikes.
40-
41-
:param T: float, time to maturity of options
42-
:param K: np.array, strikes of options
43-
:param is_calls: np.ndarray[bool], indicators of if strikes are calls (true) or puts (false)
44-
:param output: np.ndarray[float], the output to fill in with prices, must be same size as K and is_calls
45-
:return: None, this method fills in the output array, make sure its sized properly first
18+
Price a set of strikes (at same time to maturity)
4619
"""
47-
S0 = self._model.spot()
48-
lws_vec = np.log(K / S0)
49-
max_lws = np.log(np.max(K) / S0)
20+
lws_vec = np.log(K / self._model.spot())
21+
max_lws = np.log(np.max(K) / self._model.spot())
5022

5123
cumulants = self._model.cumulants(T)
52-
alph = cumulants.get_truncation_heuristic(L=self._L) \
53-
if np.isnan(self._alpha_override) else self._alpha_override
54-
55-
# Ensure that grid is wide enough to cover the strike
24+
alph = cumulants.get_truncation_heuristic(L=self._L) if np.isnan(self._alpha_override) else self._alpha_override
5625
alph = max(alph, 1.15 * max(np.abs(lws_vec)) + cumulants.c1)
5726

58-
dx = 2 * alph / (self._N - 1)
59-
a = 1. / dx
60-
lam = cumulants.c1 - (self._N / 2 - 1) * dx
61-
62-
max_n_bar = self.get_nbar(a=a, lws=max_lws, lam=lam)
63-
64-
if self._order == 0:
65-
impl = HaarImpl(N=self._N, dx=dx, model=self._model, T=T, max_n_bar=max_n_bar)
66-
67-
elif self._order == 1:
68-
impl = LinearImpl(N=self._N, dx=dx, model=self._model, T=T, max_n_bar=max_n_bar)
69-
70-
else:
71-
impl = CubicImpl(N=self._N, dx=dx, model=self._model, T=T, max_n_bar=max_n_bar)
72-
73-
disc = self._model.discountCurve(T)
74-
fwd = self._model.forwardCurve(T)
75-
cons3 = impl.cons() * disc / self._N
27+
grid = {
28+
'dx': 2 * alph / (self._N - 1),
29+
'a': 1. / (2 * alph / (self._N - 1)),
30+
'lam': cumulants.c1 - (self._N / 2 - 1) * (2 * alph / (self._N - 1)),
31+
'cons3': None, # Verrà popolato successivamente
32+
'max_nbar': self.get_nbar(a=1. / (2 * alph / (self._N - 1)), lws=max_lws, lam=cumulants.c1 - (self._N / 2 - 1) * (2 * alph / (self._N - 1)))
33+
}
7634

77-
# ==============
78-
# Price Strikes
79-
# ==============
35+
impl = self._get_implementation(self._order, T, grid['max_nbar'], grid['dx'])
8036

81-
def price_aligned_grid(index, strike):
82-
lws = lws_vec[index]
83-
nbar = self.get_nbar(a=a, lws=lws, lam=lam)
84-
xmin = lws - (nbar - 1) * dx
37+
grid['cons3'] = impl.cons() * self._model.discountCurve(T) / self._N
8538

86-
beta = ProjEuropeanPricer._beta_computation(impl=impl, xmin=xmin)
87-
coeffs = impl.coefficients(nbar=nbar, W=strike, S0=S0, xmin=xmin)
39+
option = {
40+
'disc': self._model.discountCurve(T),
41+
'fwd': self._model.forwardCurve(T),
42+
'lws_vec': lws_vec,
43+
'is_calls': is_calls,
44+
'max_lws': max_lws,
45+
'K': K
46+
}
8847

89-
# price the put
90-
price = cons3 * np.dot(beta[:len(coeffs)], coeffs)
91-
if is_calls[index]: # price using put-call parity
92-
price += (fwd - strike) * disc
48+
self.price_computation(grid, option, impl, output)
9349

94-
output[index] = max(0, price)
95-
96-
# Prices method adapted to multi-strike
97-
# with Quadrature Adjustment for Grid Misalignment
98-
def price_misaligned_grid(index, strike):
99-
closest_nbar = self.get_nbar(a=a, lws=lws_vec[index], lam=xmin)
100-
rho = lws_vec[index] - (xmin + (closest_nbar - 1) * dx)
101-
102-
coeffs = impl.coefficients(nbar=closest_nbar, W=strike, S0=S0, xmin=xmin, rho=rho,
103-
misaligned_grid=True)
50+
def price_computation(self, grid: dict, option: dict, impl: Impl, output: np.ndarray):
51+
"""
52+
Compute prices for multiple strikes, handling both aligned and misaligned grids.
53+
"""
10454

105-
# price the put
106-
price = cons3 * np.dot(beta[:len(coeffs)], coeffs)
107-
if is_calls[index]: # price using put-call parity
108-
price += (fwd - strike) * disc
55+
if len(option['K']) > 1 and self._order in self._efficient_multi_strike:
56+
xmin = option['max_lws'] - (grid['max_nbar'] - 1) * grid['dx']
57+
option['beta'] = ProjEuropeanPricer._beta_computation(impl=impl, xmin=xmin)
58+
price_vectorized = np.vectorize(self.price_misaligned_grid, excluded=['grid', 'option', 'impl', 'output'])
59+
else:
60+
price_vectorized = np.vectorize(self.price_aligned_grid, excluded=['grid', 'option', 'impl', 'output'])
10961

110-
output[index] = max(0, price)
62+
price_vectorized(np.arange(0, len(option['K'])), option['K'], grid=grid, option=option, impl=impl, output=output)
11163

112-
# Prices computation
64+
def price_aligned_grid(self, index, strike, grid: dict, option: dict, impl: Impl, output: np.ndarray):
65+
"""
66+
Price computation for aligned grid.
67+
"""
68+
lws = option['lws_vec'][index]
69+
nbar = self.get_nbar(a=grid['a'], lws=lws, lam=grid['lam'])
70+
xmin = lws - (nbar - 1) * grid['dx']
71+
beta = ProjEuropeanPricer._beta_computation(impl=impl, xmin=xmin)
72+
coeffs = impl.coefficients(nbar=nbar, W=strike, S0=self._model.spot(), xmin=xmin)
73+
price = grid['cons3'] * np.dot(beta[:len(coeffs)], coeffs)
74+
if option['is_calls'][index]:
75+
price += (option['fwd'] - strike) * option['disc']
76+
output[index] = max(0, price)
77+
78+
def price_misaligned_grid(self, index, strike, grid: dict, option: dict, impl: Impl, output: np.ndarray):
79+
"""
80+
Price computation for misaligned grid.
81+
"""
82+
lws = option['lws_vec'][index]
83+
closest_nbar = self.get_nbar(a=grid['a'], lws=lws, lam=grid['lam'])
84+
xmin = lws - (closest_nbar - 1) * grid['dx']
85+
rho = lws - (xmin + (closest_nbar - 1) * grid['dx'])
86+
beta = ProjEuropeanPricer._beta_computation(impl=impl, xmin=xmin)
87+
coeffs = impl.coefficients(nbar=closest_nbar, W=strike, S0=self._model.spot(), xmin=xmin, rho=rho, misaligned_grid=True)
88+
price = grid['cons3'] * np.dot(beta[:len(coeffs)], coeffs)
89+
if option['is_calls'][index]:
90+
price += (option['fwd'] - strike) * option['disc']
91+
output[index] = max(0, price)
11392

114-
if len(K) > 1 and self._order in self._efficient_multi_strike:
115-
xmin = max_lws - (max_n_bar - 1) * dx
116-
beta = ProjEuropeanPricer._beta_computation(impl=impl, xmin=xmin)
117-
price_vectorized = np.vectorize(price_misaligned_grid)
118-
price_vectorized(np.arange(0, len(K)), K)
119-
else:
120-
price_vectorized = np.vectorize(price_aligned_grid)
121-
price_vectorized(np.arange(0, len(K)), K)
12293

12394
@staticmethod
12495
def _beta_computation(impl: Impl = None, xmin: float = None):

fypy/pricing/fourier/ProjPricer.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,17 @@ def copy_original_arrays(*arrays):
9191
def _beta_computation(self):
9292
raise NotImplementedError
9393

94+
def _get_implementation(self, order:int, T:float, max_n_bar:int, dx:float):
95+
"""
96+
Returns the appropriate implementation based on the spline order.
97+
"""
98+
if order == 0:
99+
return HaarImpl(N=self._N, dx=dx, model=self._model, T=T, max_n_bar=max_n_bar)
100+
elif order == 1:
101+
return LinearImpl(N=self._N, dx=dx, model=self._model, T=T, max_n_bar=max_n_bar)
102+
else:
103+
return CubicImpl(N=self._N, dx=dx, model=self._model, T=T, max_n_bar=max_n_bar)
104+
94105

95106
# ===================================
96107
# Private
@@ -123,7 +134,7 @@ def integrand(self, xmin: float) -> np.ndarray:
123134
raise NotImplementedError
124135

125136
@abstractmethod
126-
def coefficients(self, nbar: int, W: float, S0: float, xmin: float) -> np.ndarray:
137+
def coefficients(self, nbar: int, W: float, S0: float, xmin: float, rho:float=None, misaligned_grid:bool=False) -> np.ndarray:
127138
raise NotImplementedError
128139

129140
@abstractmethod
@@ -164,7 +175,7 @@ def num_coeffs(self, nbar: int) -> int:
164175
return nbar + 1
165176

166177
def coefficients(self,
167-
nbar: int, W: float, S0: float, xmin: float) -> np.ndarray:
178+
nbar: int, W: float, S0: float, xmin: float, rho:float=None, misaligned_grid:bool=False) -> np.ndarray:
168179
self.G[nbar] = W * self.g1
169180
self.G[nbar - 1] = W * self.g2
170181
self.G[nbar - 2] = W * self.g3
@@ -341,7 +352,7 @@ def integrand(self, xmin: float) -> np.ndarray:
341352
grand[0] = 1 / self.cons()
342353
return grand
343354

344-
def coefficients(self, nbar: int, W: float, S0: float, xmin: float) -> np.ndarray:
355+
def coefficients(self, nbar: int, W: float, S0: float, xmin: float, rho:float=None, misaligned_grid:bool=False) -> np.ndarray:
345356
a = self.a
346357
dx = 1 / a
347358
self.G[nbar - 1] = W * (.5 - a * (1 - np.exp(-.5 * dx)))

0 commit comments

Comments
 (0)