├── pyqfin ├── __init__.py ├── models │ ├── __init__.py │ ├── hullwhite │ │ ├── __init__.py │ │ └── globally_constant.py │ ├── model.py │ ├── black_scholes.py │ └── heston.py └── tests │ ├── __init__.py │ └── test_models │ ├── __init__.py │ ├── test_heston.py │ └── test_black_scholes.py ├── README.md ├── notebooks ├── AMC │ ├── american_option_pricing.png │ └── conditional_expectation_orthogonal_projection.png ├── Heston │ └── heston_df_prices.pickle └── Backtesting │ ├── mystatsutils.py │ └── test_mystatsutils.py └── .gitignore /pyqfin/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pyqfin/models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pyqfin/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # quantfinance-examples -------------------------------------------------------------------------------- /pyqfin/models/hullwhite/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pyqfin/tests/test_models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /notebooks/AMC/american_option_pricing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niknow/quantfinance-examples/main/notebooks/AMC/american_option_pricing.png -------------------------------------------------------------------------------- /notebooks/Heston/heston_df_prices.pickle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niknow/quantfinance-examples/main/notebooks/Heston/heston_df_prices.pickle -------------------------------------------------------------------------------- /notebooks/AMC/conditional_expectation_orthogonal_projection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niknow/quantfinance-examples/main/notebooks/AMC/conditional_expectation_orthogonal_projection.png -------------------------------------------------------------------------------- /pyqfin/models/model.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | 3 | 4 | class ParametersBase(ABC): 5 | pass 6 | 7 | 8 | class ParameterDependant(ABC): 9 | 10 | def __init__(self, params) -> None: 11 | super().__init__() 12 | self.params = params 13 | self._add_getters() 14 | 15 | def _add_getters(self): 16 | for k, v in vars(self.params).items(): 17 | setattr(self, k, lambda v=v: v) 18 | 19 | 20 | class AnalyticBase(ParameterDependant): 21 | 22 | def __init__(self, params) -> None: 23 | super().__init__(params) 24 | 25 | 26 | class SimulationBase(ParameterDependant): 27 | def __init__(self, params) -> None: 28 | super().__init__(params) 29 | -------------------------------------------------------------------------------- /notebooks/Backtesting/mystatsutils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import scipy.stats as ss 3 | 4 | 5 | def independent_samples(sigma, n, nsims, seed=1): 6 | np.random.seed(seed) 7 | return ss.norm.rvs(loc=0, scale=sigma, size=(n, nsims)) 8 | 9 | 10 | def _create_windows(y, m, r): 11 | n = y.shape[0] 12 | return np.array([y[i:i + m, :] for i in range(0, n - m + 1, r)]) 13 | 14 | 15 | def overlap_samples(y, m, r=1): 16 | return np.sum(_create_windows(y, m, r), axis=1) 17 | 18 | 19 | def threshold(sigma, q, m): 20 | return ss.norm.ppf(q, loc=0, scale=sigma) * np.sqrt(m) 21 | 22 | 23 | def empirical_quantile(x, q): 24 | return ss.scoreatpercentile(x, q * 100, interpolation_method='lower') 25 | 26 | 27 | def exceedance_count(sigma, n, nsims, seed, m, r, h): 28 | y = independent_samples(sigma, n, nsims, seed) 29 | x = overlap_samples(y, m, r) 30 | return np.sum(x > h, axis=0) 31 | 32 | 33 | def observed_frequencies(x, h): 34 | o = np.zeros((h.shape[0] - 1, x.shape[1])) 35 | nsims = x.shape[1] 36 | for w in range(nsims): 37 | o[:, w] = np.histogram(x[:, w], bins=h)[0] 38 | return o 39 | 40 | 41 | def chi_squared_samples(sigma, n, m, nsims, gamma, h, r=1, seed=1): 42 | y = independent_samples(sigma, n, nsims, seed) 43 | x = overlap_samples(y, m, r) 44 | e = (gamma[1:] - gamma[:-1]) * x.shape[0] 45 | o = observed_frequencies(x, h) 46 | return np.sum((o - e[:, np.newaxis]) ** 2 / e[:, np.newaxis], axis=0) 47 | -------------------------------------------------------------------------------- /notebooks/Backtesting/test_mystatsutils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import mystatsutils 3 | from unittest import TestCase 4 | 5 | 6 | class TestMyStatsUtils(TestCase): 7 | 8 | def test_create_windows(self): 9 | self.n = 100 10 | self.nsims = 7 11 | self.m = 10 12 | y = mystatsutils.independent_samples(sigma=0.1, n=self.n, nsims=self.nsims, seed=1) 13 | self.assertEqual(y.shape, (self.n, self.nsims)) 14 | for r in range(1, 11): 15 | windows = mystatsutils._create_windows(y, m=self.m, r=r) 16 | nr = int(np.ceil((self.n - self.m + 1) / r)) 17 | self.assertEqual(windows.shape, (nr, self.m, self.nsims)) 18 | for i in range(nr): 19 | for j in range(self.m): 20 | np.testing.assert_array_almost_equal( 21 | y[i * r + j, :], 22 | windows[i, j, :] 23 | ) 24 | 25 | def test_overlap_samples(self): 26 | self.n = 100 27 | self.nsims = 7 28 | self.m = 10 29 | y = mystatsutils.independent_samples(sigma=0.1, n=self.n, nsims=self.nsims, seed=1) 30 | self.assertEqual(y.shape, (self.n, self.nsims)) 31 | for r in range(1, 11): 32 | o = mystatsutils.overlap_samples(y, self.m, r) 33 | nr = int(np.ceil((self.n - self.m + 1) / r)) 34 | self.assertEqual(o.shape, (nr, self.nsims)) 35 | for i in range(nr): 36 | np.testing.assert_array_almost_equal(o[i, :], np.sum(y[i * r:i * r + self.m, :], axis=0)) 37 | 38 | def test_observer_frequencies(self): 39 | self.x = np.array([[1, 2, 2, 3, 3, 3, 4, 4, 4, 4], 40 | [2, 3, 4, 4, 4, 4, 5, 5, 5, 5]]).T 41 | self.h = np.array([0.5, 1.5, 2.5, 3.5, 4.5, 5.5]) 42 | self.o = mystatsutils.observed_frequencies(self.x, self.h) 43 | self.assertEqual(self.o.shape, (self.h.shape[0] - 1, self.x.shape[1])) 44 | np.testing.assert_array_almost_equal( 45 | self.o[:, 0], 46 | np.array([1, 2, 3, 4, 0]) 47 | ) 48 | np.testing.assert_array_almost_equal( 49 | self.o[:, 1], 50 | np.array([0, 1, 1, 4, 4]) 51 | ) 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # vscode 132 | .vscode/ 133 | 134 | # pycharm 135 | .idea/ 136 | -------------------------------------------------------------------------------- /pyqfin/tests/test_models/test_heston.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | import numpy as np 3 | import pyqfin.models.heston as h 4 | from numpy.testing import assert_almost_equal 5 | 6 | 7 | class TestAnalytic(TestCase): 8 | 9 | def test_ap_vs_alan(self): 10 | """ 11 | Compare vs reference prices from 12 | https://financepress.com/2019/02/15/heston-model-reference-prices/ 13 | """ 14 | params = h.Params(v0=0.04, kappa=4, theta=0.25, 15 | sigma=1, rho=-0.5, r=0.01, q=0.02) 16 | a = h.Analytic(params) 17 | tau = 1 18 | s0 = 100 19 | decimals = 10 20 | assert_almost_equal(a.price_option(tau, sk=80, s0=s0, pc='p'), 21 | 7.958878113256768285213263077598987193482161301733, 22 | decimal=decimals) 23 | assert_almost_equal(a.price_option(tau, sk=80, s0=s0, pc='c'), 24 | 26.774758743998854221382195325726949201687074848341, 25 | decimal=decimals) 26 | assert_almost_equal(a.price_option(tau, sk=90, s0=s0, pc='p'), 27 | 12.017966707346304987709573290236471654992071308187, 28 | decimal=decimals) 29 | assert_almost_equal(a.price_option(tau, sk=90, s0=s0, pc='c'), 30 | 20.933349000596710388139445766564068085476194042256, 31 | decimal=decimals) 32 | assert_almost_equal(a.price_option(tau, sk=100, s0=s0, pc='p'), 33 | 17.055270961270109413522653999411000974895436309183, 34 | decimal=decimals) 35 | assert_almost_equal(a.price_option(tau, sk=100, s0=s0, pc='c'), 36 | 16.070154917028834278213466703938231827658768230714, 37 | decimal=decimals) 38 | assert_almost_equal(a.price_option(tau, sk=110, s0=s0, pc='p'), 39 | 23.017825898442800538908781834822560777763225722188, 40 | decimal=decimals) 41 | assert_almost_equal(a.price_option(tau, sk=110, s0=s0, pc='c'), 42 | 12.132211516709844867860534767549426052805766831181, 43 | decimal=decimals) 44 | assert_almost_equal(a.price_option(tau, sk=120, s0=s0, pc='p'), 45 | 29.811026202682471843340682293165857439167301370697, 46 | decimal=decimals) 47 | assert_almost_equal(a.price_option(tau, sk=120, s0=s0, pc='c'), 48 | 9.024913483457835636553375454092357136489051667150, 49 | decimal=decimals) 50 | 51 | 52 | class TestSimulation(TestCase): 53 | 54 | def test_simulation(self): 55 | h_sim = h.Simulation( 56 | params=h.Params(v0=0.2, kappa=0.3, theta=0.04, sigma=0.4, rho=-0.6, 57 | r=0.03, q=0.01), 58 | time_grid=np.linspace(0, 1, 100), 59 | npaths=100000) 60 | h_sim.simulate(s0=100) 61 | decimals = 2 62 | # moments 63 | assert_almost_equal(h_sim.analytic.s_log_mean(h_sim.time_grid, 64 | np.log(h_sim.s0)), 65 | np.log(h_sim.s_).mean(axis=0), decimal=decimals) 66 | assert_almost_equal(h_sim.analytic.s_log_var(h_sim.time_grid), 67 | np.log(h_sim.s_).var(axis=0), decimal=decimals) 68 | assert_almost_equal(h_sim.analytic.v_mean(h_sim.time_grid), 69 | h_sim.v_.mean(axis=0), decimal=decimals) 70 | assert_almost_equal(h_sim.analytic.v_var(h_sim.time_grid), 71 | h_sim.v_.var(axis=0), decimal=decimals) 72 | assert_almost_equal(h_sim.analytic.s_log_v_cov(h_sim.time_grid), 73 | np.array([np.cov(np.log(h_sim.s_[:, i]), 74 | h_sim.v_[:, i])[0, 1] for i in 75 | range(h_sim.time_grid.shape[0])]), 76 | decimal=decimals) 77 | # pricing 78 | 79 | strikes = np.linspace(90, 110, 10) 80 | ti = round(h_sim.time_grid.shape[0] / 2) 81 | maturities = h_sim.time_grid[ti:] 82 | strikes_, maturities_ = np.meshgrid(strikes, maturities) 83 | call_analytic = np.array([[h_sim.analytic.price_option_cui(tau, k, h_sim.s0, 'c') 84 | for k in strikes] for tau in maturities]) 85 | put_analytic = np.array([[h_sim.analytic.price_option_cui(tau, k, h_sim.s0, 'p') 86 | for k in strikes] for tau in maturities]) 87 | call_mc = np.array([[h_sim.price_option(0, t, k, 'c').mean() for k in strikes] 88 | for t in range(ti, h_sim.time_grid.shape[0])]) 89 | put_mc = np.array([[h_sim.price_option(0, t, k, 'p').mean() for k in strikes] 90 | for t in range(ti, h_sim.time_grid.shape[0])]) 91 | 92 | assert_almost_equal(put_analytic, put_mc) 93 | assert_almost_equal(call_analytic, call_mc) 94 | -------------------------------------------------------------------------------- /pyqfin/models/black_scholes.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import scipy.stats as stats 3 | from pyqfin.models.model import ParametersBase, AnalyticBase, SimulationBase 4 | 5 | 6 | class Params(ParametersBase): 7 | 8 | def __init__(self, sigma: float, r: float, q: float = 0) -> None: 9 | """ 10 | :param sigma: volatility 11 | :param r: risk-free rate 12 | :param q: dividend rate 13 | """ 14 | self.sigma = sigma 15 | self.r = r 16 | self.q = q 17 | super().__init__() 18 | 19 | 20 | class Analytic(AnalyticBase): 21 | 22 | @classmethod 23 | def fromParamValues(cls, sigma, r, q=0): 24 | return cls(Params(sigma, r, q)) 25 | 26 | def _dp(self, s0: float, tau: float, k: float) -> float: 27 | """ 28 | Computes the auxilliary quantity 29 | 30 | $$d_+ := \frac{1}{\sqrt{\sigma \tau}}(\ln(\tfrac{s0}{k} + (r-q - \tfrac{1}{2} \sigma^2) \tau))$$ 31 | 32 | from the Black-Scholes formula. 33 | 34 | param s0: the current spot price of the stock 35 | param tau: the remaining time to maturity of the option 36 | param k: the strike of the option 37 | 38 | returns: d1 as per Black-Scholes formula (scalar) 39 | """ 40 | return 1 / (self.sigma() * np.sqrt(tau)) * (np.log(s0 / k) 41 | + (self.r() - self.q() + self.sigma() ** 2 / 2) * tau) \ 42 | 43 | 44 | def stock_mean(self, s0: float, t: float) -> float: 45 | return s0 * np.exp((self.r() - self.q()) * t) 46 | 47 | def stock_var(self, s0: float, t: float) -> float: 48 | return s0**2 * (np.exp(self.sigma()**2 * t) - 1) * np.exp(2*(self.r() - self.q() - 0.5 * self.sigma()**2)*t + self.sigma()**2 * t) 49 | 50 | def stock_autocov(self, s0: float, t1: float, t2: float) -> float: 51 | if t1 <= t2: 52 | return np.exp(self.sigma() ** 2 * (t1 + t2) / 2) * (np.exp(self.sigma()**2 * t1) - 1) * s0**2 * np.exp((self.r() - self.q() - 0.5 * self.sigma()**2) * (t1 + t2)) 53 | else: 54 | return self.stock_autocov(s0, t2, t1) 55 | 56 | def price(self, s0: float, tau: float, k: float, pc: chr = 'c') -> float: 57 | """ 58 | Computes the price of a call option in the Black/Scholes model. 59 | 60 | param s0: the current spot price of the stock 61 | param tau: time to maturity of the option 62 | param k: the strike of the option 63 | param pc: put call flag: 'c' for call, 'p' for put 64 | 65 | returns: price of call option maturing after tau 66 | """ 67 | 68 | Phi = stats.norm(loc=0, scale=1).cdf 69 | dp = self._dp(s0, tau, k) 70 | dm = dp - self.sigma() * np.sqrt(tau) 71 | fwd = np.exp((self.r() - self.q()) * tau) * s0 72 | df = np.exp(-self.r() * tau) 73 | if pc == 'c': 74 | return df * (fwd * Phi(dp) - k * Phi(dm)) 75 | elif pc == 'p': 76 | return df * (k * Phi(-dm) - fwd * Phi(-dp)) 77 | else: 78 | raise ValueError( 79 | "black_scholes:Analytic:price_option: flag %s is invalid." % pc) 80 | 81 | def delta(self, s0: float, tau: float, k: float, pc: chr = 'c') -> float: 82 | Phi = stats.norm(loc=0., scale=1.).cdf 83 | dp = self._dp(s0, tau, k) 84 | delta_call = np.exp(-self.q() * tau) * Phi(dp) 85 | if pc == 'c': 86 | return delta_call 87 | elif pc == 'p': 88 | return delta_call - np.exp(-self.q() * tau) 89 | else: 90 | raise ValueError( 91 | "black_scholes:Analytic:delta: flag %s is invalid." % pc) 92 | 93 | def gamma(self, s0: float, tau: float, k: float) -> float: 94 | phi = stats.norm(loc=0, scale=1).pdf 95 | dp = self._dp(s0, tau, k) 96 | return np.exp(-self.q() * tau) * phi(dp) / ( 97 | s0 * self.sigma() * np.sqrt(tau)) 98 | 99 | def vega(self, s0: float, tau: float, k: float) -> float: 100 | phi = stats.norm(loc=0, scale=1).pdf 101 | dp = self._dp(s0, tau, k) 102 | return np.exp(-self.q() * tau) * s0 * phi(dp) * np.sqrt(tau) 103 | 104 | def theta(self, s0: float, tau: float, k: float, pc: chr = 'c') -> float: 105 | Phi = stats.norm(loc=0, scale=1).cdf 106 | phi = stats.norm(loc=0, scale=1).pdf 107 | dp = self._dp(s0, tau, k) 108 | dm = dp - self.sigma() * np.sqrt(tau) 109 | theta_call = self.q() * np.exp(-self.q() * tau) * s0 * Phi(dp) \ 110 | - self.r() * np.exp(-self.r() * tau) * k * Phi(dm) \ 111 | - np.exp(-self.q() * tau) * self.sigma() * s0 * phi(dp) / ( 112 | 2 * np.sqrt(tau)) 113 | if pc == 'c': 114 | return theta_call 115 | elif pc == 'p': 116 | return theta_call - self.q() * np.exp(-self.q() * tau) * s0 + self.r() * np.exp(-self.r() * tau) * k 117 | else: 118 | raise ValueError( 119 | "black_scholes:Analytic:theta: flag %s is invalid." % pc) 120 | 121 | def rho(self, s0: float, tau: float, k: float, pc: chr = 'c') -> float: 122 | Phi = stats.norm(loc=0, scale=1).cdf 123 | dp = self._dp(s0, tau, k) 124 | dm = dp - self.sigma() * np.sqrt(tau) 125 | if pc == 'c': 126 | return tau * np.exp(-self.r() * tau) * k * Phi(dm) 127 | elif pc == 'p': 128 | return - tau * np.exp(-self.r() * tau) * k * Phi(-dm) 129 | else: 130 | raise ValueError( 131 | "black_scholes:Analytic:rho: flag %s is invalid." % pc) 132 | 133 | 134 | def implied_volatility(price, r, s0, tm, sk, call, pc: chr = 'c') -> float: 135 | """ 136 | Computes the implied volatility of a European option. 137 | 138 | :param r: risk-free rate 139 | :param s0: value of underlying stock price at t=0 140 | :param tm: time to maturity of the option 141 | :param sk: strike of the option 142 | param pc: put call flag: 'c' for call, 'p' for put 143 | """ 144 | from py_vollib.black_scholes.implied_volatility import \ 145 | implied_volatility as pyvimp 146 | return pyvimp(price, s0, sk, tm, r, pc) 147 | 148 | 149 | class Simulation(SimulationBase): 150 | 151 | def __init__(self, params, time_grid, npaths) -> None: 152 | super().__init__(params) 153 | self.analytic = Analytic(params) 154 | self.time_grid = time_grid 155 | self.ntimes = self.time_grid.shape[0] 156 | self.npaths = npaths 157 | self.s0 = None 158 | self.s_ = None 159 | 160 | def simulate(self, s0, seed=1, z=None): 161 | np.random.seed(seed) 162 | self.s0 = s0 163 | if z is None: 164 | z = np.random.standard_normal((self.npaths, self.ntimes - 1)) 165 | self._simulate_with(z) 166 | return self 167 | 168 | def _simulate_with(self, z): 169 | delta = self.time_grid[1:] - self.time_grid[:-1] 170 | paths = self.s0 * np.cumprod(np.exp((self.params.r - self.params.q - self.params.sigma ** 2 / 2) * delta + self.params.sigma * np.sqrt(delta) * z), axis=1) 171 | self.s_ = np.c_[np.ones(self.npaths) * self.s0, paths] 172 | 173 | def price(self, t: int, t_mat: int, k: float, pc: chr = 'c') -> float: 174 | """ 175 | Computes the price distribution of a European option. 176 | 177 | param t: time index as of which we want to price 178 | param t_mat: time index of option maturity 179 | param k: the strike of the option 180 | param pc: put call flag: 'c' for call, 'p' for put 181 | 182 | returns: price of call option maturing after tau 183 | """ 184 | df = np.exp(- self.params.r * (self.time_grid[t_mat] - self.time_grid[t])) 185 | if pc == 'c': 186 | return df * np.maximum(self.s_[:, t_mat] - k, 0) 187 | elif pc == 'p': 188 | return df * np.maximum(k - self.s_[:, t_mat], 0) 189 | else: 190 | raise ValueError( 191 | "black_scholes:Simulation:price: flag %s is invalid." % pc) 192 | 193 | def stock_means(self): 194 | return np.array([self.analytic.stock_mean(self.s0, t) for t in self.time_grid]) 195 | 196 | def stock_vars(self): 197 | return np.array([self.analytic.stock_var(self.s0, t) for t in self.time_grid]) 198 | 199 | def stock_autocovs(self): 200 | return np.array([[self.analytic.stock_autocov(self.s0, t0, t1) 201 | for t0 in self.time_grid] 202 | for t1 in self.time_grid]) 203 | -------------------------------------------------------------------------------- /pyqfin/tests/test_models/test_black_scholes.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | from unittest import TestCase 3 | import numpy as np 4 | import sympy as sy 5 | from sympy.stats import Normal, cdf 6 | 7 | import pyqfin.models.black_scholes as bs 8 | 9 | 10 | class BlackScholesSympy: 11 | 12 | def __init__(self): 13 | sigma, r, q, s0, k, T, t = sy.symbols('sigma r q s0 k T t') 14 | self.sigma = sigma 15 | self.r = r 16 | self.q = q 17 | self.s0 = s0 18 | self.k = k 19 | self.T = T 20 | self.t = t 21 | self.tau = self.T - self.t 22 | 23 | def call(self): 24 | Phi = cdf(Normal('x', 0.0, 1.0)) 25 | dp = 1 / (self.sigma * sy.sqrt(self.tau)) * ( 26 | sy.log(self.s0 / self.k) + ( 27 | self.r - self.q + self.sigma ** 2 / 2) * self.tau) 28 | dm = dp - sy.sqrt(self.tau) * self.sigma 29 | fwd = sy.exp((self.r - self.q) * self.tau) * self.s0 30 | df = sy.exp(-self.r * self.tau) 31 | c = df * (fwd * Phi(dp) - self.k * Phi(dm)) 32 | return c 33 | 34 | def put(self): 35 | c = self.call() 36 | return c - sy.exp(-self.q * self.tau) * self.s0 + sy.exp( 37 | -self.r * self.tau) * self.k 38 | 39 | 40 | class TestAnalytic(TestCase): 41 | 42 | def setUp(self) -> None: 43 | self.sigmas = np.array([0.01, 0.05, 0.2, 0.5, 1.5]) 44 | self.rs = np.array([-0.03, -0.01, 0, 0.01, 0.03]) 45 | self.qs = np.array([0., 0.01, 0, 0.5, 0.10]) 46 | self.taus = np.array([3 / 12, 9 / 12, 1., 5., 10.]) 47 | self.ks = np.array([80., 95., 105., 120.3]) 48 | self.s0s = np.array([80., 90., 100., 110., 120]) 49 | return super().setUp() 50 | 51 | def test_regression(self): 52 | self.bsa = bs.Analytic.fromParamValues(0.3, 0.01) 53 | c = self.bsa.price(100, 1, 95, 'c') 54 | p = self.bsa.price(100, 1, 95, 'p') 55 | np.testing.assert_almost_equal(c, 14.780463438292863) 56 | np.testing.assert_almost_equal(p, 8.835197644463832) 57 | 58 | def test_put_call_parity(self): 59 | for sigma, r, q, tau, k, s0 in itertools.product(self.sigmas, self.rs, 60 | self.qs, self.taus, 61 | self.ks, self.s0s): 62 | with self.subTest(): 63 | self.bsa = bs.Analytic.fromParamValues(sigma, r, q) 64 | c = self.bsa.price(s0, tau, k, 'c') 65 | p = self.bsa.price(s0, tau, k, 'p') 66 | df = np.exp(- r * tau) 67 | np.testing.assert_almost_equal(c - p, 68 | np.exp(-q * tau) * s0 - df * k) 69 | 70 | def test_vs_sympy(self): 71 | self.bss = BlackScholesSympy() 72 | c = self.bss.call() 73 | p = self.bss.put() 74 | c_price_sym = sy.lambdify([self.bss.sigma, 75 | self.bss.r, 76 | self.bss.q, 77 | self.bss.s0, 78 | self.bss.T, 79 | self.bss.t, 80 | self.bss.k], c) 81 | p_price_sym = sy.lambdify([self.bss.sigma, 82 | self.bss.r, 83 | self.bss.q, 84 | self.bss.s0, 85 | self.bss.T, 86 | self.bss.t, 87 | self.bss.k], p) 88 | T = 11 89 | for sigma, r, q, tau, k, s0 in itertools.product(self.sigmas, self.rs, 90 | self.qs, self.taus, 91 | self.ks, self.s0s): 92 | with self.subTest(): 93 | t = T - tau 94 | self.bsa = bs.Analytic.fromParamValues(sigma, r, q) 95 | c_price = self.bsa.price(s0, tau, k, 'c') 96 | p_price = self.bsa.price(s0, tau, k, 'p') 97 | np.testing.assert_almost_equal( 98 | c_price_sym(sigma, r, q, s0, T, t, k), 99 | c_price) 100 | np.testing.assert_almost_equal( 101 | p_price_sym(sigma, r, q, s0, T, t, k), 102 | p_price) 103 | 104 | def test_delta_sympy(self): 105 | self.bss = BlackScholesSympy() 106 | c = self.bss.call() 107 | p = self.bss.put() 108 | c_delta_sym = sy.lambdify([self.bss.sigma, 109 | self.bss.r, 110 | self.bss.q, 111 | self.bss.s0, 112 | self.bss.T, 113 | self.bss.t, 114 | self.bss.k], sy.diff(c, self.bss.s0)) 115 | p_delta_sym = sy.lambdify([self.bss.sigma, 116 | self.bss.r, 117 | self.bss.q, 118 | self.bss.s0, 119 | self.bss.T, 120 | self.bss.t, 121 | self.bss.k], sy.diff(p, self.bss.s0)) 122 | T = 11 123 | for sigma, r, q, tau, k, s0 in itertools.product(self.sigmas, self.rs, 124 | self.qs, self.taus, 125 | self.ks, self.s0s): 126 | with self.subTest(): 127 | t = T - tau 128 | self.bsa = bs.Analytic.fromParamValues(sigma, r, q) 129 | c_delta = self.bsa.delta(s0, tau, k, 'c') 130 | p_delta = self.bsa.delta(s0, tau, k, 'p') 131 | np.testing.assert_almost_equal( 132 | c_delta_sym(sigma, r, q, s0, T, t, k), 133 | c_delta) 134 | np.testing.assert_almost_equal( 135 | p_delta_sym(sigma, r, q, s0, T, t, k), 136 | p_delta) 137 | 138 | def test_gamma_sympy(self): 139 | self.bss = BlackScholesSympy() 140 | c = self.bss.call() 141 | p = self.bss.put() 142 | c_gamma_sym = sy.lambdify([self.bss.sigma, 143 | self.bss.r, 144 | self.bss.q, 145 | self.bss.s0, 146 | self.bss.T, 147 | self.bss.t, 148 | self.bss.k], 149 | sy.diff(sy.diff(c, self.bss.s0), self.bss.s0)) 150 | p_gamma_sym = sy.lambdify([self.bss.sigma, 151 | self.bss.r, 152 | self.bss.q, 153 | self.bss.s0, 154 | self.bss.T, 155 | self.bss.t, 156 | self.bss.k], 157 | sy.diff(sy.diff(p, self.bss.s0), self.bss.s0)) 158 | T = 11 159 | for sigma, r, q, tau, k, s0 in itertools.product(self.sigmas, self.rs, 160 | self.qs, self.taus, 161 | self.ks, self.s0s): 162 | with self.subTest(): 163 | t = T - tau 164 | self.bsa = bs.Analytic.fromParamValues(sigma, r, q) 165 | c_gamma = self.bsa.gamma(s0, tau, k) 166 | p_gamma = c_gamma 167 | np.testing.assert_almost_equal( 168 | c_gamma_sym(sigma, r, q, s0, T, t, k), 169 | c_gamma) 170 | np.testing.assert_almost_equal( 171 | p_gamma_sym(sigma, r, q, s0, T, t, k), 172 | p_gamma) 173 | 174 | def test_vega_sympy(self): 175 | self.bss = BlackScholesSympy() 176 | c = self.bss.call() 177 | p = self.bss.put() 178 | c_vega_sym = sy.lambdify([self.bss.sigma, 179 | self.bss.r, 180 | self.bss.q, 181 | self.bss.s0, 182 | self.bss.T, 183 | self.bss.t, 184 | self.bss.k], sy.diff(c, self.bss.sigma)) 185 | p_vega_sym = sy.lambdify([self.bss.sigma, 186 | self.bss.r, 187 | self.bss.q, 188 | self.bss.s0, 189 | self.bss.T, 190 | self.bss.t, 191 | self.bss.k], sy.diff(p, self.bss.sigma)) 192 | T = 11 193 | for sigma, r, q, tau, k, s0 in itertools.product(self.sigmas, self.rs, 194 | self.qs, self.taus, 195 | self.ks, self.s0s): 196 | with self.subTest(): 197 | t = T - tau 198 | self.bsa = bs.Analytic.fromParamValues(sigma, r, q) 199 | c_vega = self.bsa.vega(s0, tau, k) 200 | p_vega = c_vega 201 | np.testing.assert_almost_equal( 202 | c_vega_sym(sigma, r, q, s0, T, t, k), 203 | c_vega) 204 | np.testing.assert_almost_equal( 205 | p_vega_sym(sigma, r, q, s0, T, t, k), 206 | p_vega) 207 | 208 | def test_theta_sympy(self): 209 | self.bss = BlackScholesSympy() 210 | c = self.bss.call() 211 | p = self.bss.put() 212 | c_theta_sym = sy.lambdify([self.bss.sigma, 213 | self.bss.r, 214 | self.bss.q, 215 | self.bss.s0, 216 | self.bss.T, 217 | self.bss.t, 218 | self.bss.k], sy.diff(c, self.bss.t)) 219 | p_theta_sym = sy.lambdify([self.bss.sigma, 220 | self.bss.r, 221 | self.bss.q, 222 | self.bss.s0, 223 | self.bss.T, 224 | self.bss.t, 225 | self.bss.k], sy.diff(p, self.bss.t)) 226 | T = 11 227 | for sigma, r, q, tau, k, s0 in itertools.product(self.sigmas, self.rs, 228 | self.qs, self.taus, 229 | self.ks, self.s0s): 230 | with self.subTest(): 231 | t = T - tau 232 | self.bsa = bs.Analytic.fromParamValues(sigma, r, q) 233 | c_theta = self.bsa.theta(s0, tau, k, 'c') 234 | p_theta = self.bsa.theta(s0, tau, k, 'p') 235 | np.testing.assert_almost_equal( 236 | c_theta_sym(sigma, r, q, s0, T, t, k), 237 | c_theta) 238 | np.testing.assert_almost_equal( 239 | p_theta_sym(sigma, r, q, s0, T, t, k), 240 | p_theta) 241 | 242 | def test_rho_sympy(self): 243 | self.bss = BlackScholesSympy() 244 | c = self.bss.call() 245 | p = self.bss.put() 246 | c_rho_sym = sy.lambdify([self.bss.sigma, 247 | self.bss.r, 248 | self.bss.q, 249 | self.bss.s0, 250 | self.bss.T, 251 | self.bss.t, 252 | self.bss.k], sy.diff(c, self.bss.r)) 253 | p_rho_sym = sy.lambdify([self.bss.sigma, 254 | self.bss.r, 255 | self.bss.q, 256 | self.bss.s0, 257 | self.bss.T, 258 | self.bss.t, 259 | self.bss.k], sy.diff(p, self.bss.r)) 260 | T = 11 261 | for sigma, r, q, tau, k, s0 in itertools.product(self.sigmas, self.rs, 262 | self.qs, self.taus, 263 | self.ks, self.s0s): 264 | with self.subTest(): 265 | t = T - tau 266 | print(sigma, r, q, tau, k, s0) 267 | self.bsa = bs.Analytic.fromParamValues(sigma, r, q) 268 | c_rho = self.bsa.rho(s0, tau, k, 'c') 269 | p_rho = self.bsa.rho(s0, tau, k, 'p') 270 | np.testing.assert_almost_equal( 271 | c_rho_sym(sigma, r, q, s0, T, t, k), 272 | c_rho) 273 | np.testing.assert_almost_equal( 274 | p_rho_sym(sigma, r, q, s0, T, t, k), 275 | p_rho) 276 | 277 | 278 | class TestImpliedVolatility(TestCase): 279 | 280 | def setUp(self) -> None: 281 | self.sigmas = np.array([0.01, 0.05, 0.2, 0.5, 1.5]) 282 | self.rates = np.array([-0.03, -0.01, 0, 0.01, 0.03]) 283 | self.spots = np.array([80., 90., 100., 110., 120]) 284 | self.maturities = np.array([3/12, 9/12, 1., 5., 10.]) 285 | self.strikes = np.array([80., 95., 105., 120.3]) 286 | self.pc = ['c', 'p'] 287 | return super().setUp() 288 | 289 | def test_implied_volatility(self): 290 | for sigma, r, s0, tm, sk, pc in itertools.product(self.sigmas, self.rates, self.spots, self.maturities, self.strikes, self.call): 291 | with self.subTest(): 292 | a = bs.Analytic.fromParamValues(sigma, r) 293 | price = a.price(s0, tm, sk, pc) 294 | iv = bs.implied_volatility(price, r, s0, tm, sk, pc) 295 | np.testing.assert_almost_equal(sigma, iv) 296 | -------------------------------------------------------------------------------- /pyqfin/models/hullwhite/globally_constant.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.stats import norm 3 | from pyqfin.models.model import ParametersBase, AnalyticBase, SimulationBase 4 | 5 | 6 | class Params(ParametersBase): 7 | 8 | def __init__(self, kappa, sigma, f0) -> None: 9 | self.kappa = kappa 10 | self.sigma = sigma 11 | self.f0 = f0 12 | 13 | 14 | class Analytic(AnalyticBase): 15 | 16 | def kappa(self): 17 | return self.params.kappa 18 | 19 | def sigma(self): 20 | return self.params.sigma 21 | 22 | def f0(self): 23 | return self.params.f0 24 | 25 | def c(self, t): 26 | """ 27 | Implements the auxilliary function 28 | $$ c(t) = f(0,t) + \frac{e^{-\kappa t} |\sigma|^2}{\kappa^2} \Big( e^{\kappa t} - 1 - \sinh(\kappa t) \Big) $$ 29 | """ 30 | return self.f0() + np.exp(-self.kappa() * t) / (self.kappa() ** 2) * \ 31 | (np.exp(self.kappa() * t) - 1 - np.sinh(self.kappa() * t)) 32 | 33 | def g(self, s, t): 34 | """ 35 | Implements the auxilliary function 36 | $$ G(s,t) &= \frac{1}{\kappa}(1 - e^{-\kappa(t-s)}) $$ 37 | """ 38 | return 1 / self.kappa() * (1 - np.exp(-self.kappa() * (t - s))) 39 | 40 | def y(self, t): 41 | """ 42 | Implements the auxilliary function 43 | $$ y(t) = \frac{\sigma^2}{2\kappa}( 1 - e^{-2 \kappa t}) $$ 44 | """ 45 | return self.sigma()**2 / (2 * self.kappa()) * (1 - np.exp(-2 * self.kappa() * t)) 46 | 47 | def r_mean_cond(self, rs, s, t): 48 | """ 49 | Implements the analytic conditional mean of the short rate 50 | $$ \mathbb{E}[r(t) \mid \mathcal{F}_s] = r(s)e^{-\kappa(t-s)} + c(t) - e^{-\kappa(t-s)}c(s) $$ 51 | """ 52 | return rs * np.exp(-self.kappa() * (t-s)) + self.c(t) - np.exp(-self.kappa() * (t-s)) * self.c(s) 53 | 54 | def r_var_cond(self, s, t): 55 | """ 56 | Implements the analytic conditional variance of the short rate 57 | $$ \mathbb{V}[r(t) \mid \mathcal{F}_s] = e^{-2 \kappa t} \frac{\sigma^2}{2\kappa} (e^{2 \kappa t} - e^{2 \kappa s}) $$ 58 | """ 59 | return np.exp(- 2 * self.kappa() * t) / (2 * self.kappa()) * self.sigma() ** 2 * \ 60 | (np.exp(2 * self.kappa() * t) - np.exp(2 * self.kappa() * s)) 61 | 62 | def x_mean_cond(self, xs, s, t): 63 | """ 64 | Implements the conditional mean of the state variable 65 | $$ \mathbb{E}[x(t) \mid \mathcal{F}_s] = e^{-\kappa (t-s)} x(s) + e^{- \kappa t} \frac{|\sigma|^2}{\kappa^2}\Big(\cosh(\kappa t) - \cosh(\kappa s) \Big) $$ 66 | """ 67 | return np.exp(-self.kappa() * (t-s)) * xs + \ 68 | np.exp(-self.kappa() * t) * self.sigma()**2 / self.kappa()**2 * \ 69 | (np.cosh(self.kappa() * t) - np.cosh(self.kappa() * s)) 70 | 71 | def x_mean(self, t): 72 | """ 73 | Implements the mean of the state variable 74 | $$ \mathbb{E}[x(t)] = \frac{\sigma^2}{2 \kappa^2}\Big(1 - e^{- \kappa t} \Big)^2 $$ 75 | """ 76 | return self.sigma() ** 2 * (1 - np.exp(- self.kappa() * t)) ** 2 / ( 2 *self.kappa()**2 ) 77 | 78 | def r_mean(self, t): 79 | return self.x_mean(t) + self.f0() 80 | 81 | def x_var_cond(self, s, t): 82 | """ 83 | Computes the conditional variance of the state variable 84 | $$ \mathbb{V}[x(t)]} = \frac{\sigma^2}{2 \kappa} (1 - e^{-2 \kappa (t-s)})) $$ 85 | """ 86 | return self.sigma()**2 / (2 * self.kappa()) * (1 - np.exp(-2 * self.kappa() * (t-s))) 87 | 88 | def x_var(self, t): 89 | """ 90 | Computes the variance of the state variable 91 | $$ \mathbb{V}[x(t)]} = \frac{\sigma^2}{2 \kappa} (1 - e^{-2 \kappa t)})) $$ 92 | """ 93 | return self.sigma()**2 * (1 - np.exp(- 2 * self.kappa() * t)) / (2*self.kappa()) 94 | 95 | def r_var(self, t): 96 | return self.x_var(t) 97 | 98 | def x_cov(self, s, t): 99 | """ 100 | Computes the autocovariance of the state variable 101 | $$ \operatorname{Cov}[x(s),x(t)] = \frac{\sigma^2}{\kappa} e^{-\kappa t} \sinh(\kappa s) $$ 102 | """ 103 | return self.sigma()**2 / self.kappa() * np.exp(-self.kappa() * max(s, t)) * np.sinh(min(s, t) * self.kappa()) 104 | 105 | def I_aux(self, s, t): 106 | """ 107 | Computes the auxilliary quantity 108 | $$ m_I(s,t) = \frac{\sigma^2}{4 \kappa^3} \Big( 2 \kappa (t-s) - (e^{-2 \kappa t} - e^{-2 \kappa s}) + 4 \cosh(\kappa s) (e^{- \kappa t} - e^{- \kappa s}) \Big) $$ 109 | """ 110 | return self.sigma()**2 / (4 * self.kappa()**3) * ( 2 * self.kappa() * (t-s) - (np.exp(-2 * self.kappa() * t) - np.exp(-2 * self.kappa() * s)) + 4 * np.cosh(self.kappa() * s) * (np.exp(- self.kappa() * t) - np.exp(- self.kappa() * s)) ) 111 | 112 | def I_mean(self, t): 113 | """ 114 | Computes the mean 115 | $$ \operatorname{E}[I(t)] = \frac{\sigma^2}{4 \kappa^3} \Big( 2 \kappa t - (e^{-2 \kappa t} - 1) + 4 (e^{- \kappa t} - 1) \Big) $$ 116 | """ 117 | return self.I_aux(0, t) 118 | 119 | def I_mean_cond(self, Is, xs, s, t): 120 | """ 121 | Computes the conditional mean 122 | $$ \mathbb{E}[I(t) \mid I(s), x(s)] = I(s) + x(s) G(s,t) + m_I(s,t) $$ 123 | """ 124 | return Is + xs * self.g(s,t) + self.I_aux(s, t) 125 | 126 | def I_var_cond(self, s, t): 127 | """ 128 | Computes the conditional variance 129 | $$ \mathbb{V}[I(t) \mid I(s), x(s)] = 2 m_I(s,t) - y(s) G(s,t)^2 $$ 130 | """ 131 | return 2 * self.I_aux(s, t) - self.y(s) * self.g(s, t)**2 132 | 133 | def I_var(self, t): 134 | """ 135 | Computes the variance 136 | $$ \mathbb{V}[I(t)] = 2 \mathbb{E}[I(t)] $$ 137 | """ 138 | return 2 * self.I_mean(t) 139 | 140 | def I_cov(self, s, t): 141 | """ 142 | Computes the covariance 143 | $$ \operatorname{Cov}[I(s), I(t)] = \frac{\sigma^2}{\kappa^3} \Big( e^{-\kappa t} \sinh(\kappa s) - 1 +e^{-\kappa s} - e^{-\kappa t}(e^{\kappa s}-1) + \kappa s \Big) 144 | """ 145 | if s <= t: 146 | return self.sigma()**2 / self.kappa()**3 * ( np.exp(-self.kappa() * t) * np.sinh(self.kappa() * s) - 1 + np.exp(-self.kappa() * s) - np.exp(-self.kappa() * t) * (np.exp(self.kappa() * s)-1) + self.kappa() * s ) 147 | else: 148 | return self.I_cov(t, s) 149 | 150 | def Ix_cov(self, s, t): 151 | """ 152 | Computes the conditional covariance 153 | $$ \operatorname{Cov}[x(t), I(t) \mid I(s), x(s)] = \frac{\sigma^2}{2 \kappa^2} \Big(1 - e^{- \kappa (t-s)}\Big)^2 $$ 154 | """ 155 | return self.sigma()**2 / (2 * self.kappa()**2) * (1 - np.exp(- self.kappa() * (t-s)))**2 156 | 157 | def Ix_cross_cov(self, s, t): 158 | """ 159 | Computes the cross covariance 160 | $$ \operatorname{Cov}[x(s), I(t)] = e^{-s \kappa} \frac{\sigma^2}{\kappa^2}(1 -e^{\kappa (s\wedge t)} - \kappa e^{-\kappa t}(s \wedge t)) 161 | """ 162 | return np.exp(-self.kappa() * s) * self.sigma()**2 / self.kappa()**2 * (1 - np.exp(-self.kappa() * min(s,t)) - self.kappa() * np.exp(-self.kappa() * t) * min(s,t)) 163 | 164 | def bond_reconstitution(self, xs, s, t): 165 | """ 166 | Computes the bond reconstitution for $s \leq t$ 167 | $$ P(s,t) = \frac{P(0,t)}{P(0,s)} \exp(-x(s) G(s,t) - \frac{1}{2} y(s) G(s,t)^2)$$ 168 | """ 169 | return np.exp(-self.params.f0 * (t-s)) * np.exp(-xs * self.g(s,t) - 0.5 * self.y(s) * self.g(s,t)**2) 170 | 171 | def _v(self, t, t_e, t_m): 172 | return self.sigma()**2 * self.g(t_e,t_m)**2 * (1 - np.exp(-2 * self.kappa() * (t_e-t))) / (2 * self.kappa()) 173 | 174 | def _capfloorlet(self, xt, t, t_e, t_m, k): 175 | v = self._v(t, t_e, t_m) 176 | tau = t_m - t_e 177 | P_t_te = self.bond_reconstitution(xt, t, t_e) 178 | P_t_tm = self.bond_reconstitution(xt, t, t_m) 179 | d = 1/np.sqrt(v) * np.log( (1 + tau * k) * P_t_tm / P_t_te) 180 | dp = d + np.sqrt(v) / 2 181 | dm = d - np.sqrt(v) / 2 182 | return (P_t_te, P_t_tm, dp, dm) 183 | 184 | def caplet(self, xt, t, t_e, t_m, k): 185 | P_t_te, P_t_tm, dp, dm = self._capfloorlet(xt, t, t_e, t_m, k) 186 | return P_t_te * norm.cdf(-dm) - (1 + k * (t_m - t_e)) * P_t_tm * norm.cdf(-dp) 187 | 188 | def floorlet(self, xt, t, t_e, t_m, k): 189 | P_t_te, P_t_tm, dp, dm = self._capfloorlet(xt, t, t_e, t_m, k) 190 | return P_t_tm * (1 + k * (t_m - t_e)) * norm.cdf(dp) - P_t_te * norm.cdf(dm) 191 | 192 | def bond_option(self, t_e, t_m, k, call=True): 193 | v = self._v(0, t_e, t_m) 194 | P_t_t_e = self.bond_reconstitution(0, 0, t_e) 195 | P_t_t_m = self.bond_reconstitution(0, 0, t_m) 196 | d = 1 / np.sqrt(v) * np.log(P_t_t_m / (P_t_t_e * k)) 197 | dp = d + np.sqrt(v) / 2 198 | dm = d - np.sqrt(v) / 2 199 | if call: 200 | return P_t_t_m * norm.cdf(dp) - P_t_t_e * k * norm.cdf(dm) 201 | else: # put 202 | return P_t_t_e * k * norm.cdf(-dm) - P_t_t_m * norm.cdf(-dp) 203 | 204 | 205 | class Simulation(SimulationBase): 206 | 207 | def __init__(self, params, time_grid, npaths) -> None: 208 | self.params = params 209 | self.analytic = Analytic(params) 210 | self.time_grid = time_grid 211 | self.ntimes = self.time_grid.shape[0] 212 | self.npaths = npaths 213 | self.method = None 214 | self.x_ = None 215 | self.w_ = None 216 | self.r_ = None 217 | self.I_ = None 218 | self.I_disc_ = np.zeros((self.npaths, self.ntimes)) 219 | self.df_ = None 220 | self.df_disc = None 221 | 222 | def simulate(self, seed=1, zx=None, zI=None, method='inc'): 223 | self.method = method 224 | np.random.seed(seed) 225 | if self.method == 'inc': 226 | if zx is None: 227 | zx = np.random.standard_normal((self.npaths, self.ntimes - 1)) 228 | if zI is None: 229 | zI = np.random.standard_normal((self.npaths, self.ntimes - 1)) 230 | self._simulate_with_inc(zx, zI) 231 | elif self.method == 'fcov': 232 | self._simulate_fcov() 233 | elif self.method == 'ql': 234 | return self._simulate_with_ql() 235 | else: 236 | raise ValueError("Method %s is not valid." % self.method) 237 | self.df_ = np.exp(-self.I_) * np.exp(-self.params.f0 * self.time_grid) 238 | return self 239 | 240 | def _simulate_with_inc(self, zx, zI): 241 | self.x_ = np.zeros((self.npaths, self.ntimes)) 242 | self.I_ = np.zeros((self.npaths, self.ntimes)) 243 | for i in range(1, self.ntimes): 244 | x_mean = self.x_mean_cond(i) 245 | x_std = np.sqrt(self.x_var_cond(i-1, i)) 246 | I_mean_cond = self.I_mean_cond(i-1, i) 247 | I_std_cond = np.sqrt(self.I_var_cond(i-1, i)) 248 | Ix_cov = self.Ix_cov(i-1, i) 249 | rho = Ix_cov / x_std / I_std_cond 250 | zxI = rho * zx[:, i-1] + np.sqrt(1-rho**2) * zI[:,i-1] 251 | self.x_[:, i] = x_mean + x_std * zx[:, i-1] 252 | self.I_[:, i] = I_mean_cond + I_std_cond * zxI 253 | self.r_ = self.x_ + self.params.f0 254 | 255 | def _simulate_fcov(self): 256 | x_means = self.x_means() 257 | I_means = self.I_means() 258 | means = np.hstack((x_means[1:],I_means[1:])) 259 | x_cov = self.x_covs() 260 | I_cov = self.I_covs() 261 | Ix_cross = self.Ix_cross() 262 | cov = np.vstack((np.hstack((x_cov[1:,1:], Ix_cross[1:,1:])), 263 | np.hstack((Ix_cross[1:,1:].T, I_cov[1:,1:])))) 264 | sim = np.random.multivariate_normal(mean=means, cov=cov, size=self.npaths) 265 | self.x_ = np.insert(sim[:,:self.ntimes-1], 0, 0, axis=1) 266 | self.I_ = np.insert(sim[:,self.ntimes-1:], 0, 0, axis=1) 267 | self.r_ = self.x_ + self.params.f0 268 | 269 | def _simulate_with_ql(self): 270 | try: 271 | import QuantLib as ql 272 | except ImportError: 273 | raise ImportError("QuantLib not installed. Skipping reconciliation.") 274 | 275 | def generate_paths(num_paths, timestep): 276 | arr = np.zeros((num_paths, timestep+1)) 277 | for i in range(num_paths): 278 | sample_path = seq.next() 279 | path = sample_path.value() 280 | time = [path.time(j) for j in range(len(path))] 281 | value = [path[j] for j in range(len(path))] 282 | arr[i, :] = np.array(value) 283 | return np.array(time), arr 284 | 285 | timestep = self.time_grid.shape[0] - 1 286 | length = self.time_grid[-1] 287 | forward_rate = self.params.f0 288 | day_count = ql.Thirty360() 289 | todays_date = ql.Date(15, 1, 2015) 290 | ql.Settings.instance().evaluationDate = todays_date 291 | spot_curve = ql.FlatForward(todays_date, ql.QuoteHandle(ql.SimpleQuote(forward_rate)), day_count) 292 | spot_curve_handle = ql.YieldTermStructureHandle(spot_curve) 293 | hw_process = ql.HullWhiteProcess(spot_curve_handle, self.params.kappa, self.params.sigma) 294 | rng = ql.GaussianRandomSequenceGenerator(ql.UniformRandomSequenceGenerator(timestep, ql.UniformRandomGenerator())) 295 | seq = ql.GaussianPathGenerator(hw_process, length, timestep, rng, False) 296 | time, paths = generate_paths(self.npaths, timestep) 297 | self.r_ = paths 298 | self.x_ = self.r_ - self.params.f0 299 | 300 | def discretize(self): 301 | delta = self.time_grid[1:] - self.time_grid[:-1] 302 | for i in range(1, self.ntimes): 303 | self.I_disc_[:, i] = self.I_disc_[:, i-1] + self.x_[:, i-1] * delta[i-1] 304 | self.df_disc = np.exp(-self.I_disc_) * np.exp(-self.params.f0 * self.time_grid) 305 | 306 | def p(self, j, i): 307 | return self.analytic.bond_reconstitution(self.x_[:,j], self.time_grid[j], self.time_grid[i]) 308 | 309 | def price_mc_caplet(self, i, i_e, i_m, k): 310 | tau = self.time_grid[i_m] - self.time_grid[i_e] 311 | return self.df_[:, i_m] / self.df_[:, i] * np.maximum(1. / self.p(i_e, i_m) - (1 + k * tau), 0) 312 | 313 | def price_caplet(self, i, i_e, i_m, k): 314 | return self.analytic.caplet(self.x_[:, i], self.time_grid[i], self.time_grid[i_e], self.time_grid[i_m], k) 315 | 316 | def x_means(self): 317 | return self.analytic.x_mean(self.time_grid) 318 | 319 | def r_means(self): 320 | return self.analytic.x_mean(self.time_grid) + self.params.f0 321 | 322 | def x_vars(self): 323 | return self.analytic.x_var(self.time_grid) 324 | 325 | def r_vars(self): 326 | return self.analytic.r_var(self.time_grid) 327 | 328 | def x_covs(self): 329 | return np.array([[self.analytic.x_cov(s, t) for t in self.time_grid] for s in self.time_grid]) 330 | 331 | def I_means(self): 332 | return self.analytic.I_mean(self.time_grid) 333 | 334 | def I_vars(self): 335 | return self.analytic.I_var(self.time_grid) 336 | 337 | def Ix_covs(self): 338 | return self.analytic.Ix_cov(0, self.time_grid) 339 | 340 | def I_covs(self): 341 | return np.array([[self.analytic.I_cov(s,t) for t in self.time_grid] for s in self.time_grid]) 342 | 343 | def Ix_cross(self): 344 | return np.array([[self.analytic.Ix_cross_cov(s,t) for t in self.time_grid] for s in self.time_grid]) 345 | 346 | def x_mean_cond(self, i): 347 | return self.analytic.x_mean_cond(self.x_[:, i-1], self.time_grid[i-1], self.time_grid[i]) 348 | 349 | def x_var_cond(self, j, i): 350 | return self.analytic.x_var_cond(self.time_grid[j], self.time_grid[i]) 351 | 352 | def I_mean_cond(self, j, i): 353 | return self.analytic.I_mean_cond(self.I_[:, j], self.x_[:, j], self.time_grid[j], self.time_grid[i]) 354 | 355 | def I_var_cond(self, j, i): 356 | return self.analytic.I_var_cond(self.time_grid[j], self.time_grid[i]) 357 | 358 | def Ix_cov(self, j, i): 359 | return self.analytic.Ix_cov(self.time_grid[j], self.time_grid[i]) 360 | -------------------------------------------------------------------------------- /pyqfin/models/heston.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.integrate import quad, trapz 3 | from scipy.stats import norm 4 | 5 | from pyqfin.models.model import ParametersBase, AnalyticBase, SimulationBase 6 | import pyqfin.models.black_scholes as bs 7 | 8 | 9 | 10 | """ 11 | An implementation of the Heston model using numpy. We use the 12 | follwing formulation of the Heston model: 13 | \begin{align*} 14 | dS_t &=0 (r-q) S_t dt + \sqrt{v_t} S_t dW \\ 15 | dv_t &= \kappa(\theta - v_t)dt + \sigma \sqrt{v_t}dW, 16 | \end{align*} 17 | where $dW dZ = \rho dt$, $v$ represents a stochastic volatility, 18 | $S_t$ is the stock price with spot s0, $r$ is the $r$, $\kappa$ 19 | is the rate of mean reversion, $\theta$ is the long-term mean of 20 | the volatility and $\sigma$ is the volatility of volatility. 21 | 22 | These implementations are based off various publications: 23 | 24 | [1] Andersen. "Efficient Simulation of the Heston Stochastic 25 | Volatility Model", SSRN 2007, http://ssrn.com/abstract=946405 26 | 27 | [2] Cui et al. "Full and fast calibration of the Heston stochastic volatility model", 28 | https://doi.org/10.1016/j.ejor.2017.05.018 29 | 30 | [3] Le Floch. "An adaptive Filon quadrature for stochastic volatility model", 31 | https://ssrn.com/abstract=3304016" 32 | """ 33 | 34 | 35 | class Params(ParametersBase): 36 | 37 | def __init__(self, v0: float, kappa: float, theta: float, sigma: float, rho: float, r: float, q: float = 0) -> None: 38 | """ 39 | :param v0: spot variance 40 | :param kappa: mean reversion of variance 41 | :param theta: long term variance 42 | :param sigma: volatility of variance 43 | :param rho: correlation of the driving BMs 44 | :param r: the assumed risk-free rate 45 | :param q: the dividend rate 46 | """ 47 | self.v0 = v0 48 | self.kappa = kappa 49 | self.theta = theta 50 | self.sigma = sigma 51 | self.rho = rho 52 | self.r = r 53 | self.q = q 54 | super().__init__() 55 | 56 | 57 | class Analytic(AnalyticBase): 58 | """ 59 | This class contains analytic functions needed for the Heston model. 60 | """ 61 | def characteristic_function(self, st: float, tau: float, u: float) -> float: 62 | """ 63 | Computes the Heston characteristic function 64 | $$ \Psi_{\log(S_T/S_0)}(u) = E[e^{i u \log(S_T/S_0)}] $$ 65 | for valuing an option at $t$ with maturity $T$, hence time 66 | to maturity $\tau := T - t$, and stock spot value $S_t$, 67 | based off [2, Eq. (18)]. 68 | 69 | :param st: spot stock at t 70 | :param tau: time to maturity 71 | :param u: function argument 72 | """ 73 | xi = self.kappa() - self.sigma() * self.rho() * 1j * u # [2, Eq. (11a)] 74 | d = np.sqrt(xi ** 2 + self.sigma() ** 2 * (u ** 2 + 1j * u)) # [2, Eq. (11b)] 75 | a1 = (u ** 2 + 1j * u) * np.sinh(d * tau / 2) # Eq. (15b) 76 | a2 = (d / self.v0()) * np.cosh(d * tau / 2) + xi / self.v0() * np.sinh(d * tau / 2) # [2, Eq. (15c)] 77 | a = a1 / a2 # [2, Eq. (15a)] 78 | fwd = st * np.exp((self.r() - self.q()) * tau) 79 | # [2, Eq. (17.b)]: 80 | dd = np.log(d / self.v0()) + (self.kappa() - d) * tau / 2 81 | dd -= np.log((d + xi) / (2 * self.v0()) + (d - xi) / (2 * self.v0()) * np.exp(- d * tau)) 82 | # [2, Eq. (18):] 83 | res = 1j * u * np.log(fwd / st) 84 | res -= self.kappa() * self.theta() * self.rho() * tau * 1j * u / self.sigma() 85 | res -= a 86 | res += 2 * self.kappa() * self.theta() / self.sigma() ** 2 * dd 87 | return np.exp(res) 88 | 89 | def v_mean(self, t: float, s: float = 0, vs: float = None) -> float: 90 | """ 91 | Computes the conditional mean of the variance process $V_t$ given 92 | $V_s$, $s \leq t$, c.f. [1, Corrolary 1]. 93 | 94 | :param t: future time 95 | :param s: current time 96 | :param vs: variance at vs 97 | :return: $\mathbb{E}[V_t | V_s]$ 98 | """ 99 | if vs is None: 100 | vs = self.v0() 101 | return self.theta() + np.exp(-self.kappa() * (t-s)) * (vs - self.theta()) 102 | 103 | def v_var(self, t: float, s: float = 0, vs: float = None) -> float: 104 | """ 105 | Computes the conditional variance of the variance process $V_t$ given 106 | $V_s$, $s \leq t$. c.f. [1, Corrolary 1]. 107 | 108 | :param t: future time 109 | :param s: current time 110 | :param vs: variance at vs 111 | :return: $\mathbb{V}[V_t | V_s]$ 112 | """ 113 | if vs is None: 114 | vs = self.v0() 115 | e = 1 - np.exp(-self.kappa() * (t-s)) 116 | res = vs * self.sigma()**2 * np.exp(-self.kappa() * (t-s)) / self.kappa() * e 117 | res += self.theta() * self.sigma()**2 / (2 * self.kappa()) * e**2 118 | return res 119 | 120 | def _omega1(self, t: float) -> float: 121 | """ 122 | Auxiliary function, see [1, Appendix A]. 123 | 124 | :param t: time parameter 125 | :return: $\Omega_1(t)$ 126 | """ 127 | res = (1 + self.kappa() * t) * self.sigma()**2 128 | res -= 2 * self.rho() * self.kappa() * self.sigma() * (2 + self.kappa() * t) 129 | res += 2 * self.kappa() ** 2 130 | res *= 4 * np.exp(-self.kappa() * t) 131 | res += np.exp(-2 * self.kappa() * t) * self.sigma()**2 132 | res += (2 * self.kappa() * t - 5) * self.sigma()**2 133 | res -= 8 * self.rho() * self.kappa() * self.sigma() * (self.kappa() * t - 2) 134 | res += 8 * self.kappa()**2 * (self.kappa() * t - 1) 135 | return res 136 | 137 | def _omega2(self, t: float) -> float: 138 | """ 139 | Auxiliary function, see [1, Appendix A]. 140 | 141 | :param t: time parameter 142 | :return: $\Omega_2(t)$ 143 | """ 144 | res = - self.kappa() * t * self.sigma()**2 145 | res += 2 * self.rho() * self.sigma() * self.kappa() * (1 + self.kappa() * t) 146 | res -= 2 * self.kappa() ** 2 147 | res *= 2 * np.exp(-self.kappa() * t) 148 | res -= np.exp(-2 * self.kappa() * t) * self.sigma() **2 149 | res += self.sigma()**2 + 4 * self.kappa()**2 150 | res -= 4 * self.kappa() * self.rho() * self.sigma() 151 | return res 152 | 153 | def _omega3(self, t: float) -> float: 154 | """ 155 | Auxiliary function, see [1, Appendix A]. 156 | 157 | :param t: time parameter 158 | :return: $\Omega_3(t)$ 159 | """ 160 | res = t - 2 * self.rho() / self.sigma() * (1 + self.kappa() * t) 161 | res *= 2 * self.kappa() * np.exp(-self.kappa() * t) 162 | res += np.exp(-2 * self.kappa() * t) 163 | res += (4 * self.kappa() * self.rho() - self.sigma()) / self.sigma() 164 | return res 165 | 166 | def _omega4(self, t: float) -> float: 167 | """ 168 | Auxiliary function, see [1, Appendix A]. 169 | 170 | :param t: time parameter 171 | :return: $\Omega_4(t)$ 172 | """ 173 | res = 1 - self.kappa() * t 174 | res += 2 * self.rho() * self.kappa() ** 2 * t / self.sigma() 175 | res *= np.exp(-self.kappa() * t) 176 | res -= np.exp(- 2 * self.kappa() * t) 177 | return res 178 | 179 | def s_log_mean(self, t: float, x0: float) -> float: 180 | """ 181 | Computes the mean of the log of the stock, see [1, Appendix A]. 182 | 183 | :param t: time parameter 184 | :return: $\mathbb{E}[\ln(S_t)]$ 185 | """ 186 | res = (self.theta() - self.v0()) / (2 * self.kappa()) 187 | res *= (1 - np.exp(-self.kappa() * t)) 188 | res += x0 - self.theta() * t / 2 189 | return (self.r() - self.q()) * t + res 190 | 191 | def s_log_var(self, t: float) -> float: 192 | """ 193 | Computes the variance of the log of the stock, see [1, Appendix A]. 194 | 195 | :param t: time parameter 196 | :return: $\mathbb{V}[\ln(S_t)]$ 197 | """ 198 | res = self.theta() * self._omega1(t) / 8 199 | res += self.v0() * self._omega2(t) / 4 200 | res /= self.kappa()**3 201 | return res 202 | 203 | def s_log_v_cov(self, t: float) -> float: 204 | """ 205 | Computes the covariance of the log of the stock and the 206 | variance process, see [1, Appendix A]. 207 | 208 | :param t: time parameter 209 | :return: $\mathbb{Cov}[\ln(S_t), V_t]$ 210 | """ 211 | res = self.theta() * self.sigma() ** 2 * self._omega3(t) / 4 212 | res += self.v0() * self.sigma() ** 2 * self._omega4(t) / 2 213 | res /= self.kappa() ** 2 214 | return res 215 | 216 | 217 | def _price_option_int_ubound(self, tau: float, eps: float) -> float: 218 | """ 219 | Calculates the truncation limit for the integration in the Heston 220 | pricer. 221 | 222 | :param tau: time to maturity 223 | :param eps: relative tolerance 224 | :return: 225 | """ 226 | ltiny = np.log(eps) 227 | cinf = (self.v0() + self.kappa() * self.theta() * tau) / self.sigma() * np.sqrt(1 - self.rho() ** 2) #[3, (24)] 228 | 229 | # for Newton solver 230 | def func1(u): 231 | # [3, (27)] 232 | return -cinf * u - np.log(u) - ltiny, -cinf - 1 / u 233 | 234 | def func2(u): 235 | # [3, (28)] 236 | return - 0.5 * self.v0() * tau * u * u - np.log(u) - ltiny, - self.v0() * tau * u - 1 / u 237 | 238 | # Newton iteration 239 | zero1 = -np.log(eps) / cinf # starting value 240 | zero2 = np.sqrt(-2 * np.log(eps) / self.v0() / tau) 241 | nsteps = 5 242 | 243 | for j in range(nsteps): 244 | nom, denom = func1(zero1) 245 | zero1 = zero1 - nom / denom 246 | nom, denom = func2(zero2) 247 | zero2 = zero2 - nom / denom 248 | return max(zero1, zero2) + 1. 249 | 250 | def _price_option_integrand(self, s0: float, tau: float, sk: float): 251 | """ 252 | Implements the integrand for the Heston call option pricer 253 | $$ \operatorname{Re}\Big( e^{-iux}\frac{\phi_B(u-\tfrac{i}{2}) - \phi(u-\tfrac{i}{2})}{u^2+\tfrac{1}{4}} \Big), $$ 254 | where $\phi_B(z) := \Psi_{\ln(F_T/F_0)}(z) = e^{-\tfrac{1}{2}\sigma_B^2 T(z^2+iz)}$ 255 | is the characteristic function of the normalized log-forward in the Black-Scholes model, 256 | $\phi:=\Psi_{\ln(F_T/F_0)}$ is the characteristic function of the normalized log-forward in the 257 | Heston model and $x := \ln(K/F)$. This is the formulation from [3, Eq. (18)]. 258 | :param s0: spot stock 259 | :param tau: time to maturity 260 | :param sk: strike 261 | :return: 262 | """ 263 | 264 | fwd = s0 * np.exp((self.r() - self.q()) * tau) 265 | 266 | def integrand(u): 267 | u_shifted = u - 0.5 * 1j 268 | psi = self.characteristic_function(s0, tau, u_shifted) 269 | psi *= np.exp(-1j * (self.r()-self.q()) * tau * u_shifted) 270 | res = np.exp(-0.5 * tau * self.v0() * (u ** 2 + 0.25)) - psi 271 | res *= np.exp(-1j * u * np.log(sk / fwd)) / (u ** 2 + 0.25) 272 | return np.real(res) 273 | 274 | return integrand 275 | 276 | def price_option(self, tau: float, sk: float, s0: float, pc: chr = 'c', eps: float =1e-10, num_grid_points: int =1000) -> float: 277 | """ 278 | Computes the price of a European option under the Heston model. The 279 | pricing formula implemented is based off Andersen/Piterbarg as 280 | discussed in [3, (18)]. 281 | 282 | :param s0: spot stock 283 | :param tau: time to maturity 284 | :param sk: strike 285 | :param pc: 'c' if call, 'p' if put option 286 | """ 287 | int_ubound = self._price_option_int_ubound(tau, eps) 288 | int_domain = np.linspace(0, int_ubound, num_grid_points) 289 | int_fun = self._price_option_integrand(s0, tau, sk) 290 | int_value = trapz(int_fun(int_domain), x=int_domain) 291 | price_bs = bs.Analytic(bs.Params(np.sqrt(self.v0()), self.r(), self.q())).price(s0, tau, sk, pc) 292 | fwd = s0 * np.exp((self.r() - self.q()) * tau) 293 | return price_bs + np.sqrt(fwd * sk) / np.pi * np.exp(-self.r() * tau) * int_value 294 | 295 | 296 | class Simulation(SimulationBase): 297 | 298 | def __init__(self, params, time_grid, npaths) -> None: 299 | super().__init__(params) 300 | self.analytic = Analytic(params) 301 | self.time_grid = time_grid 302 | self.ntimes = self.time_grid.shape[0] 303 | self.npaths = npaths 304 | self.s0 = None 305 | self.v_ = np.zeros((self.npaths, self.ntimes)) 306 | self.s_ = np.zeros((self.npaths, self.ntimes)) 307 | 308 | def simulate(self, s0, seed=1, z=None, method='qe'): 309 | """ 310 | 311 | :param s0: spot value of the stock 312 | :param seed: seed of random number generator 313 | :param z: np.array of shape (npaths, ntimes) 314 | :param method: 'qe' for QE-Scheme and 'te' for 'truncated euler' 315 | :return: 316 | """ 317 | np.random.seed(seed) 318 | self.s0 = s0 319 | dt = self.time_grid[1:] - self.time_grid[:-1] 320 | if method == 'te': 321 | if z is None: 322 | rho = self.params.rho 323 | z = np.random.multivariate_normal( 324 | np.array([0, 0]), 325 | np.array([[1, rho], [rho, 1]]), 326 | (self.npaths, self.ntimes - 1)) 327 | self._simulate_variance_te(z[:, :, 1], dt) 328 | self._simulate_stock_te(z[:, :, 0], dt) 329 | elif method == 'qe': 330 | uv = np.random.uniform(0, 1, (self.npaths, self.ntimes - 1)) 331 | self._simulate_variance_qe(uv) 332 | zx = np.random.normal(0, 1, (self.npaths, self.ntimes - 1)) 333 | self._simulate_stock_qe(zx, dt) 334 | 335 | def _simulate_stock_te(self, zx, dt): 336 | vp = np.maximum(self.v_, 0) 337 | a = -0.5 * vp 338 | b = np.sqrt(vp) 339 | self.s_[:, 0] = np.log(self.s0) 340 | for i in range(self.ntimes - 1): 341 | self.s_[:, i + 1] = self.s_[:, i] + a[:, i] * dt[i] + b[:, i] * np.sqrt(dt[i]) * zx[:, i] 342 | self.s_ = np.exp(self.s_) * np.exp((self.params.r-self.params.q) * self.time_grid) 343 | 344 | def _simulate_variance_te(self, zv, dt): 345 | self.v_[:, 0] = self.params.v0 346 | for i in range(self.ntimes - 1): 347 | v_trunc = np.maximum(self.v_[:, i], 0) 348 | v_drift = self.params.kappa * (self.params.theta - v_trunc) * dt[i] 349 | v_diff_c = np.sqrt(v_trunc) * self.params.sigma * np.sqrt(dt[i]) 350 | self.v_[:, i + 1] = self.v_[:, i] + v_drift + v_diff_c * zv[:, i] 351 | 352 | def _simulate_variance_qe(self, uv): 353 | """ 354 | This implements the QE scheme based on [1, Sect. 3.2.4]. 355 | :param zu: 356 | :param dt: 357 | :return: 358 | """ 359 | self.v_[:, 0] = self.params.v0 360 | 361 | psi_c = 1.5 362 | for i in range(self.ntimes - 1): 363 | m = self.analytic.v_mean(self.time_grid[i+1], self.time_grid[i], self.v_[:, i]) 364 | s2 = self.analytic.v_var(self.time_grid[i+1], self.time_grid[i], self.v_[:, i]) 365 | psi = s2 / m ** 2 366 | 367 | idx = psi <= psi_c 368 | 369 | # psi <= psi_c 370 | psi1 = psi[idx] 371 | b2 = 2 / psi1 - 1 + np.sqrt(2 / psi1) * np.sqrt(2 / psi1 - 1) # [1, Eq. (27)] 372 | a = m[idx] / (1 + b2) # [1, Eq. (28)] 373 | b = np.sqrt(b2) 374 | zv = norm.ppf(uv[idx, i]) 375 | self.v_[idx, i+1] = a * (b + zv) ** 2 # [1, Eq. (23)] 376 | 377 | # psi > psi_c 378 | psi2 = psi[~idx] 379 | p = (psi2 - 1) / (psi2 + 1) # [1, Eq. (29)] 380 | beta = 2 / (m[~idx] * (psi2 + 1)) # [1, Eq. (30)] 381 | idy = p < uv[~idx, i] 382 | self.v_[~idx, i+1][idy] = np.log((1-p[idy]) / (1-uv[~idx, i][idy])) / beta[idy] # [1, Eq. (25)] 383 | 384 | def _simulate_stock_qe(self, z, dt): 385 | """ 386 | Implements the simulation of the log stock to combine with the QE 387 | scheme for the variance, see [1, Eq. (33)]. 388 | 389 | :param z: Gaussians of shape (npaths, ntimes - 1) 390 | :param dt: time deltas of shape (ntimes - 1) 391 | :return: 392 | """ 393 | g1 = 0.5 394 | g2 = 0.5 395 | k0 = - self.params.rho * self.params.kappa * self.params.theta / self.params.sigma * dt 396 | h = self.params.kappa * self.params.rho / self.params.sigma - 0.5 397 | k1 = g1 * dt * h - self.params.rho / self.params.sigma 398 | k2 = g2 * dt * h + self.params.rho / self.params.sigma 399 | k3 = g1 * dt * (1 - self.params.rho ** 2) 400 | k4 = g2 * dt * (1 - self.params.rho ** 2) 401 | self.s_[:, 0] = np.log(self.s0) 402 | for i in range(self.ntimes - 1): 403 | drift = k0[i] + k1[i] * self.v_[:, i] + k2[i] * self.v_[:, i+1] 404 | diff = np.sqrt(k3[i] * self.v_[:, i] + k4[i] * self.v_[:, i + 1]) 405 | self.s_[:, i+1] = self.s_[:, i] + drift + diff * z[:, i] 406 | self.s_ = np.exp(self.s_) * np.exp((self.params.r - self.params.q) * self.time_grid) 407 | 408 | def v_means(self): 409 | return self.analytic.v_mean(self.time_grid) 410 | 411 | def v_vars(self): 412 | return self.analytic.v_var(self.time_grid) 413 | 414 | def price_option(self, t: float, t_mat: float, sk: float, pc: chr = 'c'): 415 | """ 416 | Computes the price distribution of a European option. 417 | 418 | param t: time index as of which we want to price 419 | param t_mat: time index of option maturity 420 | param sk: the strike of the option 421 | param pc: put call flag: 'c' for call, 'p' for put 422 | 423 | returns: price of call option maturing after tau 424 | """ 425 | df = np.exp(- self.params.r * (self.time_grid[t_mat] - self.time_grid[t])) 426 | if pc == 'c': 427 | return df * np.maximum(self.s_[:, t_mat] - sk, 0) 428 | elif pc == 'p': 429 | return df * np.maximum(sk - self.s_[:, t_mat], 0) 430 | else: 431 | raise ValueError( 432 | "black_scholes:Simulation:price: flag %s is invalid." % pc) 433 | --------------------------------------------------------------------------------