|
4 | 4 | from fypy.model.levy.LevyModel import FourierModel
|
5 | 5 | from fypy.pricing.fourier.ProjPricer import ProjPricer, Impl, CubicImpl, LinearImpl, HaarImpl
|
6 | 6 |
|
7 |
| - |
8 | 7 | class ProjEuropeanPricer(ProjPricer):
|
9 | 8 | def __init__(self, model: FourierModel, N: int = 2 ** 9, L: float = 10., order: int = 3,
|
10 | 9 | 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 |
| - """ |
25 | 10 | super().__init__(model, N, L, order, alpha_override)
|
26 |
| - |
27 | 11 | self._efficient_multi_strike = [1]
|
28 | 12 |
|
29 | 13 | if order not in (0, 1, 3):
|
30 | 14 | raise NotImplementedError("Only cubic, linear and Haar implemented so far")
|
31 | 15 |
|
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): |
37 | 17 | """
|
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) |
46 | 19 | """
|
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()) |
50 | 22 |
|
51 | 23 | 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 |
56 | 25 | alph = max(alph, 1.15 * max(np.abs(lws_vec)) + cumulants.c1)
|
57 | 26 |
|
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 | + } |
76 | 34 |
|
77 |
| - # ============== |
78 |
| - # Price Strikes |
79 |
| - # ============== |
| 35 | + impl = self._get_implementation(self._order, T, grid['max_nbar'], grid['dx']) |
80 | 36 |
|
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 |
85 | 38 |
|
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 | + } |
88 | 47 |
|
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) |
93 | 49 |
|
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 | + """ |
104 | 54 |
|
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']) |
109 | 61 |
|
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) |
111 | 63 |
|
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) |
113 | 92 |
|
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) |
122 | 93 |
|
123 | 94 | @staticmethod
|
124 | 95 | def _beta_computation(impl: Impl = None, xmin: float = None):
|
|
0 commit comments