Coverage for rivapy/models/stoch_local_vol.py: 98%

50 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-06-05 14:27 +0000

1import numpy as np 

2import bisect 

3from rivapy.models.local_vol import LocalVol 

4import rivapy.numerics.kernel_regression as kernel_regression 

5 

6class StochasticLocalVol: 

7 def __init__(self, stoch_vol_model): 

8 """ Stochastic Local Volatility model 

9 

10 Args: 

11 stochastic_vol_model (StochasticVolModel): The underlying stochastic vol model 

12 """ 

13 self._stoch_vol_model = stoch_vol_model 

14 self._stoch_local_variance = None #np.ones(shape=(time_grid.shape[0], x_strikes.shape[0])) 

15 self._x_strikes = None 

16 self._time_grid = None 

17 

18 def calibrate_MC(self, 

19 vol_param, 

20 x_strikes: np.ndarray, 

21 time_grid: np.ndarray, 

22 n_sims, 

23 local_var: np.ndarray=None, 

24 call_prices: np.ndarray=None): 

25 """Calibrate the Heston Local Volatility Model using kernel regression. 

26 

27 This method calibrates the local volatility part of the Heston Model given a volatility parametrization so that the  

28 respective implied volatilities from the given vol parametrization are reproduced by the Heston Local Volatility model. 

29 The calibration is based on kernel regression as described in `Applied Machine Learning for Stochastic Local Volatility Calibration <https://www.frontiersin.org/articles/10.3389/frai.2019.00004/full>`_. 

30 

31 Args: 

32 vol_param ([type]): [description] 

33 x_strikes (np.array): [description] 

34 time_grid (np.array): [description] 

35 n_sims ([type]): [description] 

36 local_var (np.ndarray, optional): [description]. Defaults to None. 

37 call_prices (np.ndarray, optional): Defaults to None. 

38 """ 

39 if local_var is None: 

40 local_var = LocalVol.compute_local_var(vol_param, x_strikes, time_grid, call_prices) 

41 self._stoch_local_variance = StochasticLocalVol._calibrate_MC(self._stoch_vol_model, 

42 x_strikes, time_grid, n_sims, local_var) 

43 self._x_strikes = x_strikes 

44 self._time_grid = time_grid 

45 

46 

47 

48 def apply_mc_step(self, x: np.ndarray, t0: float, t1: float, rnd: np.ndarray, inplace: bool = True): 

49 """Apply a MC-Euler step for the Heston Local Vol Model for n different paths. 

50 

51 Args: 

52 x (np.ndarray): 2-d array containing the start values for the spot and variance. The first column contains the spot, the second the variance values. 

53 t0 ([type]): [description] 

54 t1 ([type]): [description] 

55 rnd ([type]): [description] 

56 """ 

57 t0_index = bisect.bisect_left(self._time_grid, t0) 

58 if t0_index == 0:# or t0_index == self._time_grid.shape[0]: 

59 slv = self._stoch_local_variance[0] 

60 elif t0_index == self._time_grid.shape[0]: 

61 slv = self._stoch_local_variance[-1] 

62 else: 

63 dt = self._time_grid[t0_index] - self._time_grid[t0_index-1] 

64 w1 = (t0-self._time_grid[t0_index-1])/dt 

65 w2 = (self._time_grid[t0_index] - t0)/dt 

66 slv = w1*self._stoch_local_variance[t0_index] + w2*self._stoch_local_variance[t0_index-1] 

67 slv = np.interp(x[:,0], self._x_strikes, slv) 

68 return self._stoch_vol_model.apply_mc_step(x, t0, t1, rnd, inplace, slv) 

69 # if False: 

70 # rnd_S = rnd[:,0] 

71 # rnd_V = rnd[:,1] 

72 # rnd_corr_S = np.sqrt(1.0-self._stoch_vol_model._correlation**2)*rnd_S + self._stoch_vol_model._correlation*rnd_V 

73 # S = x_[:,0] 

74 # v = x_[:,1] 

75 # dt = t1-t0 

76 # sqrt_dt = np.sqrt(dt) 

77 # S *= np.exp(-0.5*v*slv*dt + np.sqrt(v*slv)*rnd_corr_S*sqrt_dt) 

78 # v += self._stoch_vol_model._mean_reversion_speed*(self._stoch_vol_model._long_run_variance-v)*dt + self._stoch_vol_model._vol_of_vol*np.sqrt(v)*rnd_V*sqrt_dt 

79 # x_[:,1] = np.maximum(v,0) 

80 # return x_ 

81 def get_initial_value(self)->np.ndarray: 

82 """Return the initial value (x0, v0) 

83 

84 Returns: 

85 np.ndarray: Initial value. 

86 """ 

87 return self._stoch_vol_model.get_initial_value() 

88 

89 @staticmethod 

90 def _calibrate_MC(stoch_vol, 

91 x_strikes: np.array, 

92 time_grid: np.array, 

93 n_sims, 

94 local_var: np.ndarray, 

95 x0 = 1.0): 

96 

97 # def apply_mc_step( x, t0, t1, rnd, stoch_local_var): 

98 # slv = np.interp(x[:,0], x_strikes, stoch_local_var) 

99 # rnd_S = rnd[:,0] 

100 # rnd_V = rnd[:,1] 

101 # rnd_corr_S = np.sqrt(1.0-stoch_vol._correlation**2)*rnd_S + stoch_vol._correlation*rnd_V 

102 # S = x[:,0] 

103 # v = x[:,1] 

104 # dt = t1-t0 

105 # sqrt_dt = np.sqrt(dt) 

106 # S *= np.exp((0.5*v*slv)*dt + np.sqrt(v*slv)*rnd_corr_S*sqrt_dt) 

107 # v += stoch_vol._mean_reversion_speed*(stoch_vol._long_run_variance-v)*dt + stoch_vol._vol_of_vol*np.sqrt(v)*rnd_V*sqrt_dt 

108 # x[:,1] = np.maximum(v,0) 

109 

110 stoch_local_variance = np.empty(local_var.shape) 

111 stoch_local_variance[0] = local_var[0]/stoch_vol._initial_variance 

112 #now apply explicit euler to get new values for v and S and then apply kernel regression to estimate new local variance 

113 x = np.empty((n_sims,2)) 

114 initial_value = stoch_vol.get_initial_value() 

115 x[:,0] = initial_value[0] 

116 x[:,1] = initial_value[1] 

117 for i in range(1,time_grid.shape[0]): 

118 rnd = np.random.normal(size=(n_sims,2)) 

119 slv = np.interp(x[:,0], x_strikes, stoch_local_variance[i-1]) 

120 stoch_vol.apply_mc_step(x, time_grid[i-1], time_grid[i], rnd, True, slv) 

121 gamma = ( (4.0*np.std(x[:,0])**5) / (3.0*x.shape[0]) )**(-1.0/5.0) 

122 kr = kernel_regression.KernelRegression(gamma = gamma).fit(x[:,0:1],x[:,1]) 

123 stoch_local_variance[i] = local_var[i]/kr.predict(x_strikes.reshape((-1,1))) 

124 # overwrite all values at strikes that are outside the simulated spot range 

125 min_spot = np.min(x[:,0]) 

126 stoch_local_variance[i][x_strikes<min_spot] = stoch_local_variance[i][x_strikes>min_spot][0] 

127 max_spot = np.max(x[:,0]) 

128 stoch_local_variance[i][x_strikes>max_spot] = stoch_local_variance[i][x_strikes<max_spot][-1] 

129 return stoch_local_variance